본문 바로가기
Spring/Toby's Spring Reading Club

1-5장. 서비스 추상화

by Soono991 2023. 1. 8.

5장은 서비스 추상화로 기존의 초난감 DAO에서 기능을 추가하고 리팩토링 과정을 거쳐 더 나은 구조를 설계하는 과정을 소개합니다.

 

5장에서는 초난감 DAO에서 사용자의 레벨 관리 기능을 가지고 기능 추가 및 리팩토링 과정을 학습합니다.

public class User {

    private String id;
    private String name;
    private String password;

    private static final int BASIC = 1;
    private static final int SILVER = 2;
    private static final int GOLD = 3;

    private int level;
    /*

    사용자 레벨 관리 기능 추가로 level 필드를 추가했다.
    level 필드는 단순히 int 형이기 때문에 의도하지 않은 다른 값을 넣어도 에러가 발생하지 않는다.

    설계 의도는 BASIC, SILVER, GOLD 세 레벨만 관리하려고 했는데, 1000을 넣어도 에러가 발생하지 않는다.
    user.setLevel(1000);

     */

}

 

먼저 사용자의 레벨을 User 객체에 int 형으로 추가했으며, 각 레벨을 상수로 관리합니다.

하지만 level 필드에 의도하지 않은 값을 넣어도 int 형이라면 에러가 발생하지 않게 됩니다.

따라서 이 경우에는 int 형이 아닌 Enum으로 분리하는 것이 좋습니다.

 

public enum Level {
    BASIC(1), SILVER(2), GOLD(3);

    private final int value;

    Level(int value) {
        this.value = value;
    }

    public int intValue() {
        return value;
    }

    public static Level valueOf(int value) {
        switch (value) {
            case 1:
                return BASIC;
            case 2:
                return SILVER;
            case 3:
                return GOLD;
            default:
                throw new AssertionError("Unknown value: " + value);
        }
    }
}

Enum을 사용하면 명시적으로 허용할 값의 범위를 제한할 수 있습니다. 그리고 각 레벨을 문자열로 표시하기 때문에 1, 2, 3의 값이 각각 어떤 레벨에 해당하는지 보다 쉽게 파악할 수 있습니다.

사용자의 레벨 업그레이드 기능 추가

사용자의 레벨을 업그레이드하는 기능을 추가해야 하는데, 문제는 이 기능을 어디에 추가해야 하는 것인가?입니다.

이때 생각해봐야 할 것은, 사용자의 레벨을 업그레이드를 누가 책임져야 하냐로 볼 수 있다. 다시 말하면 사용자의 레벨을 업그레이드하기 위해 필요한 정보를 누가 가장 많이 알고 있느냐로 볼 수 있습니다.

아래 로직을 살펴보자.

 

public class UserService {

    private final UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public void upgradeLevels() {
        List<User> users = userDao.getAll();

        for (User user : users) {
            boolean changed;
            if (user.getLevel() == Level.BASIC && user.getLogin() >= 50) {
                user.setLevel(Level.SILVER);
                changed = true;
            } else if (user.getLevel() == Level.SILVER && user.getRecommend() >= 30) {
                user.setLevel(Level.GOLD);
                changed = true;
            } else if (user.getLevel() == Level.GOLD) {
                changed = false;
            } else {
                changed = false;
            }

            if (changed) {
                userDao.update(user);
            }
        }
    }
}

UserService 영역에서 User 목록을 가져온 후 직접 사용자의 레벨이 업그레이드 가능한지 판단 한 후 업그레이드 처리를 하고 있습니다.

 

UserService는 단순히 사용자의 레벨을 업그레이드 처리하는 것이지 직접 Service 영역 자체에서 업그레이드 가능 여부를 판단할 필요는 없습니다.

 

따라서 사용자의 레벨을 업그레이드 처리하는 별도의 객체를 생성하는 것이 좋습니다.

이때 사용자의 레벨을 업그레이드하는 조건을 하나의 정책이라고 보고 UserLevelUpgradePolicy 인터페이스를 추가 할 수 있습니다.

public interface UserLevelUpgradePolicy {
    boolean canUpgradeLevel(User user);

    void upgradeLevel(User user);
}
public class LoginAndRecommendCountPolicy implements UserLevelUpgradePolicy {

    private final UserDao userDao;

    public LoginAndRecommendCountPolicy(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public boolean canUpgradeLevel(User user) {
        Level currentLevel = user.getLevel();

        switch (currentLevel) {
            case BASIC:
                return user.getLogin() >= 50;
            case SILVER:
                return user.getRecommend() >= 30;
            case GOLD:
                return false;
            default:
                throw new IllegalArgumentException("Unknown Level: " + currentLevel);
        }
    }

    @Override
    public void upgradeLevel(User user) {
        user.upgradeLevel();
        userDao.update(user);
    }
}
public class UserService {

    private final UserDao userDao;
    private final UserLevelUpgradePolicy policy;

    public UserService(UserDao userDao, UserLevelUpgradePolicy policy) {
        this.userDao = userDao;
        this.policy = policy;
    }

    public void upgradeLevels() {
        List<User> users = userDao.getAll();

        for (User user : users) {
            if (policy.canUpgradeLevel(user)) {
                upgradeLevel(user);
            }
        }
    }

    public void upgradeLevel(User user) {
        policy.upgradeLevel(user);
    }
}

 

이렇게 작성하면 UserService는 업그레이드 가능 여부 / 업그레이드 처리를 UserLevelUpgradePolicy 객체에게 위임할 수 있습니다.

 

이렇게 리팩토링 하는 과정이 책임과 역할에 따라 로직을 분리한 것입니다.

위와 같이 기능을 추가하거나 리팩토링 하는 과정에서 항상 책임과 역할을 생각하는 것이 좋습니다.

 

5장에서는 이와 같이 코드를 개선할 때는 아래와 같은 질문을 해볼 필요가 있다고 합니다.

  • 코드에 중복된 부분은 없는가? - 중복
  • 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가? - 가독성
  • 코드가 자신이 있어야 할 자리에 있는가? - 효율성, 책임과 역할
  • 앞으로 변경이 일어난다면 어떤 것이 있을 수 있고, 그 변화에 쉽게 대응할 수 있게 작성되어 있는가? - 확장성

UserService는 User에게 "레벨 업그레이드 작업을 해달라"라고 요청하고, 또 User는 Level에게 "다음 레벨이 무엇인지 알려달라"라고 요청하는 방식으로 동작하게 하는 것이 바람직하다.
(p.343)

적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 다양한 추상화 기법을 도입하고, 애플리케이션 로직과 기술/환경을 분리하는 등의 작업은 갈수록 복잡해지는 엔터프라이즈 애플리케이션에는 반드시 필요하다.
(p.378)

마지막으로 인터페이스로 추상화를 하게 되면 테스트용 객체를 생성하여 테스트시 보다 쉽게 객체들을 바꿔치기할 수 있다는 장점도 있습니다.

'Spring > Toby's Spring Reading Club' 카테고리의 다른 글

1-6. AOP (2)  (0) 2023.01.10
1-6. AOP (1)  (0) 2023.01.09
1-4장. 예외  (0) 2023.01.08
1-3장. 템플릿  (0) 2023.01.07
1-2장. 테스트  (0) 2022.09.19

댓글