본문 바로가기

Spring/토비의 스프링 3.1

5.1 사용자 레벨 관리 기능 추가


사용자의 활동 내역을 참고해 레벨을 조정해주는 기능 추가

  • 사용자의 레벨 : BASIC, SILVER, GOLD
  • 처음 가입할 경우 : BASIC
  • 가입 후 50회 이상의 로그인 : SILVER
  • SILVER + 30회 이상의 추천 : GOLD
  • 레벨은 일정한 주기를 가지고 일괄적으로 변경됨

5.1.1 필드 추가

Lvl 이늄

  1. DB에서 varchar로 관리하는 것보다 숫자로 관리하는 것이 좋음
  2. User의 프로퍼티 타입
    • 숫자로 할 경우, 타입의 안정성이 보장되지 않아 위험할 수 있음
    • BASIC(1), SILVER(2), GOLD(3) 일 때 1,2,3 외의 숫자를 넣어도 컴파일 에러가 나지 않음
    • 이늄(enum)을 사용하는 것이 안전하고 편리함

Oracle에서는 Level을 필드명으로 사용이 불가능하므로 Level을 Lvl로, level은 lvl로 수정한다.

public enum Lvl {
    // 세 개의 이늄 오브젝트 정의
    BASIC(1), SILVER(2), GOLD(3);

    private final int value;

    // DB에 저장할 값을 넣어줄 생성자를 만들어둔다.
    Lvl(int value) {
        this.value = value;
    }

    // 값을 가져오는 메서드
    public int intValue() {
        return value;
    }

    // 값으로부터 Lvl 타입 오브젝트를 가져오도록 만든 스태틱 메소드
    public static Lvl valueOf(int value) {
        switch(value) {
            case 1: return BASIC;
            case 2: return SILVER;
            case 3: return GOLD;
            default: throw new AssertionError("Unknown value : " + value);
        }
    }
}

Lvl 이늄

  • 내부에는 DB에 저장할 int 타입의 값을 갖고 있음
  • 겉으로는 Lvl 타입의 오브젝트
  • 안전하게 사용이 가능
  • ex) user1.setLvl(1000); // 컴파일 에러 발생

User 필드 추가

로그인 횟수와 추천수 추가

  • 단순 int 타입으로 만들어도 무관
public class User {
    // ...
    Lvl lvl;
    int login;
    int recommend;

    public User() {
    }

    public User(String id, String name, String password, Lvl lvl, int login, int recommend) {
        // ...
        this.lvl = lvl;
        this.login = login;
        this.recommend = recommend;
    }

    // ...

    public Lvl getLvl() {
        return lvl;
    }

    public void setLvl(Lvl lvl) {
        this.lvl = lvl;
    }

    public int getLogin() {
        return login;
    }

    public void setLogin(int login) {
        this.login = login;
    }

    public int getRecommend() {
        return recommend;
    }

    public void setRecommend(int recommend) {
        this.recommend = recommend;
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", name=" + name + ", password=" + password + ", lvl=" + lvl + ", login=" + login
                + ", recommend=" + recommend + "]";
    }
}

UserDaoTest 수정

테스트 픽스처 수정

  • 테스트에서 유용하게 사용하기 위해서는 각각 다른 값을 넣어주는 것이 좋다.
public class UserDaoTest {
    @Before
    public void setUp() {
        // ...
        this.user1 = new User("gyumee", "박성철", "springno1", Lvl.BASIC, 1, 0);
        this.user2 = new User("leegw700", "이길원", "springno2", Lvl.SILVER, 55, 10);
        this.user3 = new User("bumjin", "박범진", "springno3", Lvl.GOLD, 100, 40);
    }
}

checkSameUser() 메소드 수정

public class UserDaoTest { 
    private void checkSameUser(User user1, User user2) {
        assertThat(user1.getId(), is(user2.getId()));
        assertThat(user1.getName(), is(user2.getName()));
        assertThat(user1.getPassword(), is(user2.getPassword()));
        assertThat(user1.getLvl(), is(user2.getLvl()));
        assertThat(user1.getLogin(), is(user2.getLogin()));
        assertThat(user1.getRecommend(), is(user2.getRecommend()));
    }
}

addAndGet() 메소드 수정

public class UserDaoTest {
    @Test
    public void addAndGet() {
        // ...

        User userget1 = dao.get(user1.getId());
        checkSameUser(userget1, user1);

        User userget2 = dao.get(user2.getId());
        checkSameUser(userget2, user2);
    }
}

UserDaoJdbc 수정

userMapper에 추가된 필드를 넣고, add() 메서드를 수정해준다.

public class UserDaoJdbc {
    private RowMapper<User> userMapper = new RowMapper<User>() {
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User();
            user.setId(rs.getString("id"));
            user.setName(rs.getString("name"));
            user.setPassword(rs.getString("password"));
            user.setLvl(Lvl.valueOf(rs.getInt("lvl")));
            user.setLogin(rs.getInt("login"));
            user.setRecommend(rs.getInt("recommend"));
            return user;
        }
    };

    // ...

    @Override
    public void add(final User user) throws DataAccessException {
        this.jdbcTemplate.update("INSERT INTO users(id, name, password, lvl, login, recommend) "
            + "VALUES (?, ?, ?, ?, ?, ?)", 
            user.getId(), user.getName(), user.getPassword(), 
            user.getLvl().intValue(), user.getLogin(), user.getRecommend());
    }
}

Lvl 타입의 lvl 필드를 사용할 때 주의

  1. DB 저장
    • Lvl 이늄은 오브젝트이므로 DB에 저장할 수 있는 타입이 아님
    • intValue() 메소드를 이용하여 정수로 변환해주어야 함
  2. DB 조회
    • int 타입으로 lvl 정보를 가져오므로 setLvl() 메소드에 전달하면 에러 발생
    • valueOf() 메소드를 이용하여 lvl 타입으로 변환해주어야 함

5.1.2 사용자 수정 기능 추가

사용자 정보는 여러번 수정될 수 있음

  • id를 제외한 나머지 필드는 수정될 가능성이 있음
  • 수정되는 필드의 종류에 따라 각각 여러 개의 DAO 메소드를 만들어야 할 때도 있음
  • 일단은 간단히 수정될 User 오브젝트를 전달하면 id를 참고해서 필드 정보를 UPDATE하는 메소드를 추가

수정 기능 테스트 추가

UserDaoTest.java

public class UserDaoTest { 
    @Test
    public void update() {
        dao.deleteAll();

        dao.add(user1);

        user1.setName("오민규");
        user1.setPassword("spring6");
        user1.setLvl(Lvl.GOLD);
        user1.setLogin(1000);
        user1.setRecommend(999);

        dao.update(user1);

        User user1update = dao.get(user1.getId());
        checkSameUser(user1, user1update);
    }
}

테스트 내용

  1. 픽스처 오브젝트를 하나 등록
  2. id를 제외한 필드의 내용을 변경
  3. update() 호출
  4. id로 조회해서 가져온 User 오브젝트와 수정한 픽스처 오브젝트를 비교

픽스처 오브젝트는 인스턴스 변수인데 직접 변경해도 괜찮은가?

  • 테스트 메소드가 실행될 때마다 UserDaoTest 오브젝트는 새로 만들어지고, setUp() 메소드가 새로 불려서 초기화되므로 내용을 변경해도 상관없다.

UserDao와 UserDaoJdbc 수정

UserDao에 update() 메소드 추가

UserDao.java

public interface UserDao {
    // ...
    void update(User user1);
}

UserDaoJdbc.java

@Override
public class UserDaoJdbc {
    public void update(User user) {
        this.jdbcTemplate.update(
            "UPDATE users SET name = ?, password = ?, lvl = ?, login = ?, recommend = ? where id = ?", 
            user.getName(), user.getPassword(), user.getLvl().intValue(), user.getLogin(), user.getRecommend(), user.getId()
        );
    }
}

수정 테스트 보완

UPDATE SQL을 사용할 때는 항상 WHERE 조건을 확인하여야 한다.

  1. JdbcTemplate update() 메소드의 리턴 값 확인 (실행된 로우의 개수)
  2. 원하는 사용자 외의 정보는 변경되지 않았음을 직접 확인

기존 테스트에서 where 절을 빼도 테스트가 성공한다.

  • 테스트에 결함이 있다는 증거
  • 심각한 문제가 발생할 수 있다.

UserDaoTest.java

public class UserDaoTest {
    @Test
    public void update() {
        dao.deleteAll();

        dao.add(user1); // 수정할 사용자
        dao.add(user2); // 수정하지 않을 사용자

        user1.setName("오민규");
        user1.setPassword("spring6");
        user1.setLvl(Lvl.GOLD);
        user1.setLogin(1000);
        user1.setRecommend(999);

        dao.update(user1);

        User user1update = dao.get(user1.getId());
        checkSameUser(user1, user1update);

        User user2update = dao.get(user2.getId());
        checkSameUser(user2, user2update);
    }
}

5.1.3 UserService.upgradeLevels()

사용자 관리 로직의 위치

  • UserDaoJdbc : DAO는 데이터를 어떻게 가져오고 조작할지를 다루는 곳이므로 비지니스 로직을 두는 곳이 아니다.
  • UserService : 사용자 관리 비지니스 로직을 담을 새로운 클래스

UserService

  1. UserDao 인터페이스 타입으로 userDao 빈을 DI 받아 사용
    • UserDao의 구현 클래스에 영향을 받지 않도록 해야 함
    • DI를 적용하기 위해서는 UserService도 스프링 빈으로 등록하여야 한다.

UserService 클래스와 빈 등록

UserService.java

  • UserDao 오브젝트를 저장할 인스턴스 변수를 선언
  • DI가 가능하도록 수정자 메소드를 추가
public class UserService {
    UserDao userDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

스프링 빈으로 등록

  • 설정파일에 userService 아이디로 빈을 추가
  • userDao 빈을 DI 받도록 프로퍼티를 추가
<bean id="userService" class="com.david.tobysspring.user.service.UserService">
    <property name="userDao" ref="userDao" />
</bean>

UserServiceTest 테스트 클래스

UserServiceTest 테스트 클래스 추가

  • 테스트 대상인 UserService 빈을 제공받을 수 있도록 @Autowired가 붙은 인스턴스 변수로 선언
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserServiceTest {
    @Autowired
    UserService userService;

    @Test
    public void bean() {
        assertThat(this.userService, is(notNullValue()));
    }
}

test-applicationContext가 아닌 수동 DI하려면 어떻게 해야하지?

upgradeLevels() 메소드

UserService.java

  • 사용자 레벨 관리 기능
public class UserService {
    public void upgradeLevels() {
        List<User> users = userDao.getAll();
        for (User user : users) {
            // 레벨 변화가 있는지 확인하는 플래그
            Boolean changed = null;

            // BASIC 레벨 업그레이드
            if (user.getLvl() == Lvl.BASIC && user.getLogin() >= 50) {
                user.setLvl(Lvl.SILVER);
                changed = true;
            // SILVER 레벨 업그레이드
            } else if (user.getLvl() == Lvl.SILVER && user.getRecommend() >= 30) {
                user.setLvl(Lvl.GOLD);
                changed = true;
            // GOLD는 변경이 되지 않음
            } else if (user.getLvl() == Lvl.GOLD) {
                changed = false;
            // 일치하는 조건 없는 경우 변경 없음
            } else {
                changed = false;
            }

            // 변경 있는 경우에만 update() 호출
            if (changed) {
                userDao.update(user);
            }
        }
    }
}

사용자 레벨 관리

  1. 모든 사용자 정보를 DAO에서 가져옴
  2. 한 명씩 레벨 변경 작업을 수행
  3. 레벨 변경 여부를 확인하기 위해 플래그를 선언
  4. 플래그가 참인 경우에만 update() 실행

upgradeLevels() 테스트

테스트 방법

  • 다섯 가지 픽스처를 등록
    1. BASIC 레벨이며, 레벨 변경이 일어나지 않음
    2. BASIC 레벨이며, 레벨 변경이 있음
    3. SILVER 레벨이며, 레벨 변경이 없음
    4. SILVER 레벨이며, 레벨 변경이 있음
    5. GOLD 레벨이며, 레벨 변경이 없음
  • 테스트 픽스처가 많으므로 List를 사용
public class UserServiceTest {
    @Before
    public void setUp() {
        users = Arrays.asList(
            new User("bumjin", "박범진", "p1", Lvl.BASIC, 49, 0),
            new User("joytouch", "강명성", "p2", Lvl.BASIC, 50, 0),
            new User("erwins", "신승한", "p3", Lvl.SILVER, 60, 29),
            new User("madnite1", "이상호", "p4", Lvl.SILVER, 60, 30),
            new User("green", "오민", "p5", Lvl.GOLD, 100, 100)
        );
    }
}

테스트에 사용하는 데이터는 경계가 되는 값의 전후를 선택하는 것이 좋음

5.1.4 UserService.add()

처음 가입하는 사용자는 BASIC 레벨이어야 함

  • UserDaoJdbc의 add() : UserDaoJdbc에는 비지니스 로직이 포함되면 안 됨
  • User 클래스에서 lvl 필드를 lvl.BASIC으로 초기화 : 직접 초기화하는 방법은 썩 좋지 않음
  • UserService : add() 메소드를 추가하여 사용자가 등록될 때 적용할 만한 비지니스 로직을 담당하는 것이 적절

테스트 방법

  1. UserService의 add()를 호출하면 레벨이 BASIC으로 설정 됨
    • 단, lvl 필드 값이 설정되어 있는 경우에ㅔ는 해당 lvl을 그대로 두기로 함(정책)
  2. lvl 필드가 설정되지 않은 user와 설정된 user를 등록
  3. get()으로 각각 가져와 user 정보를 확인
public class UserServiceTest {
    @Test
    public void add() {
        userDao.deleteAll();

        // GOLD 레벨
        User userWithLvl = users.get(4);

        // 레벨이 비어 있는 사용자
        // 로직에 따라 등록 중 BASIC 레벨로 설정되어야 한다.
        User userWithoutLvl = users.get(0);
        userWithoutLvl.setLvl(null);

        userService.add(userWithLvl);
        userService.add(userWithoutLvl);

        // DB에 저장된 결과를 가져와 확인한다.
        User userWithLvlRead = userDao.get(userWithLvl.getId());
        User userWithoutLvlRead = userDao.get(userWithoutLvl.getId());

        checkLvl(userWithLvlRead, Lvl.GOLD);
        checkLvl(userWithoutLvlRead, Lvl.BASIC);
    }
}

UserService.java

public class UserService {
    public void add(User user) {
        if (user.getLvl() == null) {
            user.setLvl(Lvl.BASIC);
        }
        userDao.add(user);
    }
}

테스트가 성공하긴 하지만 조금 복잡하다.

  • 간단한 비즈니스 로직을 테스트하기 위해 DAO와 DB가 모두 동원되어야 한다.

5.1.5 코드 개선

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

upgradeLvls() 메소드 코드의 문제점

  1. for 루프 속의 if/elseif/else 블록이 읽기 불편함
    • 레벨의 변화 단계 / 업그레이드 조건 / 조건 충족 시 작업이 모두 섞여 있음
  2. if 조건 블록이 레벨 개수만큼 반복됨
  3. 현재 레벨과 업그레이드 조건을 도시에 비교하는 부분도 문제가 될 수 있음

upgradeLevels() 리팩토링

추상적인 레벨에서 로직을 작성한 upgradeLvls()

  • 모든 사용자를 가져와 한 명씩 업그레이드가 가능한 지 확인하고, 가능하면 업그레이드를 한다.
public class UserService { 
    public void upgradeLvls() {
        List<User> users = userDao.getAll();
        for (User user : users) {
            if (canUpgradeLvl(user)) {
                upgradeLvl(user);
            }
        }
    }
}

canUpgradeLvl()

  • switch 문으로 레벨을 구분하고, 각 레벨에 대한 업그레이드 조건을 만족하는지를 확인
  • 로직에서 처리할 수 없는 경우 예외를 던짐
public class UserService {
    private boolean canUpgradeLvl(User user) {
        Lvl currentLvl = user.getLvl();
        switch(currentLvl) {
            // 레벨로 구분해서 조건을 판단
            case BASIC: return (user.getLogin() >= 50);
            case SILVER: return (user.getRecommend() >= 30);
            case GOLD: return false;
            // 현재 로직에서 처리할 수 없는 레벨일 경우 예외 발생
            // 새로운 레벨이 추가되고 로직이 수정되지 않으면 확인 가능
            default: throw new IllegalArgumentException("Unknown Level : " + currentLvl);
        }
    }
}

upgradeLvl()

  • 레벨을 다음 단계로 바꿔주고, 변경사항을 DB에 업데이트함
public class UserService {
    private void upgradeLvl(User user) {
        if (user.getLvl() == Lvl.BASIC) {
            user.setLvl(Lvl.SILVER);
        } else if (user.getLvl() == Lvl.SILVER) {
            user.setLvl(Lvl.GOLD);
        }

        userDao.update(user);
    }
}

upgradeLvl()

  • 테스트는 통과하지만 여전히 마음에 들지 않음

    1. 다음 단계가 무엇인가 하는 로직과 그 때 사용자 오브젝트의 lvl 필드를 변경해준다는 로직이 섞여 있음
    2. 노골적으로 드러나 있음
    3. 예외 처리도 되지 않음
      • GOLD 레벨인 사용자를 업그레이드 하려고 해당 메서드를 호출하면 아무것도 하지않고 dao의 update()가 호출됨
    4. 레벨이 늘어나면 if문은 점점 더 길어질 것

레벨의 순서와 다음 단계의 레벨이 무엇인지 결정하는 일을 Lvl에게 맡김

public enum Lvl {
    // 이늄 선언에 DB에 저장할 값과 함께 다음 단계의 레벨 정보도 추가
    GOLD(3, null), SILVER(2, GOLD), BASIC(1, SILVER);

    // 다음 단계의 레벨 정보를 스스로 갖고 있도록 Lvl 타입의 next 변수를 추가
    private final int value;
    private final Lvl next;

    Lvl(int value, Lvl next) {
        this.value = value;
        this.next = next;
    }

    public int intValue() {
        return value;
    }

    public Lvl nextLvl() {
        return next;
    }

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

Lvl 이늄

  • next 변수를 추가하여 다음 단계 레벨 정보를 담음
  • 생성자 파라미터를 추가하여 다음 레벨이 무엇인지 지정
  • 다음 레벨이 무엇인지 알고 싶다면 nextLvl() 호출

사용자 정보가 바뀌는 부분을 User로 옮김

  • User 내부 정보의 변경은 UserService보다 User에 있는 것이 더 적절
  • User에게 레벨 업그레이드를 해야 하니 정보를 변경하라고 요청
public class User {
    public void upgradeLvl() {
        Lvl nextLvl = this.lvl.nextLvl();
        if (nextLvl == null) {
            throw new IllegalArgumentException(this.lvl + "은 업그레이드가 불가능합니다.");
        } else {
            this.lvl = nextLvl;
        }
    }
}
  1. Lvl의 nextLvl()을 이용하여 다음 레벨을 확인하여 현재 레벨을 변경
  2. canUpgrade() 에서 업그레이드 여부를 확인하긴 하지만 스스로 예외상황에 대한 검증 기능을 갖고 있는 것이 안전
    • User 오브젝트를 UserService에서만 사용하는 것도 아님

UserService.java

public class UserService {
    private void upgradeLvl(User user) {
        user.upgradeLvl();
        userDao.update(user);
    }
}

개선된 코드

각 오브젝트와 메소드가 각각 자기 몫의 책임을 맡아 일을 하는 구조

  • UserService, User, Lvl이 자신의 책임에 충실한 기능을 갖고 있음
  • 필요에 따라 작업을 해달라고 요청하는 구조

오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리이다.

User 테스트

User에 로직을 담은 메소드를 추가하였으므로 이에 대한 테스트도 추가

UserTest.java

public class UserTest {
    User user;

    @Before
    public void setUp() {
        user = new User();
    }

    @Test()
    public void upgradeLvl() {
        Lvl[] lvls = Lvl.values();
        for (Lvl lvl : lvls) {
            if (lvl.nextLvl() == null) {
                continue;
            }

            user.setLvl(lvl);
            user.upgradeLvl();
            assertThat(user.getLvl(), is(lvl.nextLvl()));
        }
    }

    @Test(expected=IllegalStateException.class)
    public void cannotUpgradeLvl() {
        Lvl[] lvls = Lvl.values();
        for (Lvl lvl : lvls) {
            if (lvl.nextLvl() != null) {
                continue;
            }

            user.setLvl(lvl);
            user.upgradeLvl();
        }
    }
}

User에 대한 테스트는 굳이 스프링의 테스트 컨텍스트를 사용하지 않아도 된다.

  • RunWith, ContextConfiguration 애노테이션 없어도 무관
  • User 오브젝트는 스프링이 IoC로 관리해주는 오브젝트가 아니기 때문

테스트 방법

  • Lvl에 정의된 모든 레벨을 가져와 upgradeLvl()을 호출
    1. 다음 레벨이 null인 경우를 제외하고 업그레이드가 되는지를 확인
    2. 다음 레벨이 null이 아닌 경우를 제외하고 예외가 잘 던져지는지를 확인

UserServiceTest 개선

테스트 코드 내 다음 단계의 레벨이 무엇인지 넣어주는 것도 중복

  • 기존 테스트 코드보다 테스트 로직도 좀 더 분명하게 드러남
public class UserServiceTest {
    @Test
    public void upgradeLvls() {
        userDao.deleteAll();

        for (User user : users) {
            userDao.add(user);
        }

        userService.upgradeLvls();

        checkLvlUpgraded(users.get(0), false);
        checkLvlUpgraded(users.get(1), true);
        checkLvlUpgraded(users.get(2), false);
        checkLvlUpgraded(users.get(3), true);
        checkLvlUpgraded(users.get(4), false);
    }

    private void checkLvlUpgraded(User user, boolean upgraded) {
        User userUpdate = userDao.get(user.getId());
        if (upgraded) {
            assertThat(userUpdate.getLvl(), is(user.getLvl().nextLvl()));
        } else {
            assertThat(userUpdate.getLvl(), is(user.getLvl()));
        }
    }
}

로그인 횟수와 추천 횟수가 중복해서 나타남

  • 상수의 중복은 바람직하지 못함

UserService.java

public class UserService {
    public static final int MIN_LOGCOUNT_FOR_SILVER = 50;
    public static final int MIN_RECOMMEND_FOR_GOLD = 30;

    private boolean canUpgradeLvl(User user) {
        Lvl currentLvl = user.getLvl();
        switch(currentLvl) {
            case BASIC: return (user.getLogin() >= MIN_LOGCOUNT_FOR_SILVER);
            case SILVER: return (user.getRecommend() >= MIN_RECOMMEND_FOR_GOLD);
            case GOLD: return false;
            default: throw new IllegalArgumentException("Unknown Level : " + currentLvl);
        }
    }
}

UserServiceTest.java

import static com.david.tobysspring.user.service.UserService.MIN_LOGCOUNT_FOR_SILVER;
import static com.david.tobysspring.user.service.UserService.MIN_RECOMMEND_FOR_GOLD;

public class UserServiceTest {
    @Before
    public void setUp() {
        this.dataSource = new SingleConnectionDataSource("jdbc:oracle:thin:@localhost:1521:xe", "springbook_test", "test", true);
        ((UserDaoJdbc) userDao).setDataSource(dataSource);

        users = Arrays.asList(
            new User("bumjin", "박범진", "p1", Lvl.BASIC, MIN_LOGCOUNT_FOR_SILVER-1, 0),
            new User("joytouch", "강명성", "p2", Lvl.BASIC, MIN_LOGCOUNT_FOR_SILVER, 0),
            new User("erwins", "신승한", "p3", Lvl.SILVER, 60, MIN_RECOMMEND_FOR_GOLD-1),
            new User("madnite1", "이상호", "p4", Lvl.SILVER, 60, MIN_RECOMMEND_FOR_GOLD),
            new User("green", "오민", "p5", Lvl.GOLD, 100, 100)
        );
    }
}

상수를 비지니스 로직에 둘 경우

  1. 코멘트를 달거나 설계 문서를 참조해야 함
  2. 업그레이드 조건 값이 변경될 경우 유연한 대처가 가능

추가

레벨을 업그레이드하는 정책을 유연하게 변경할 수 있도록 개선

  • 연말이나 새로운 서비스 홍보기간 등 업그레이드 정책을 다르게 적용해야 하는 경우
  • UserService를 직접 수정하는 것은 위험

사용자 업그레이드 정책을 UserService로부터 분리하는 방법

  • 업그레이드 정책을 담은 오브젝트를 DI를 통해 UserService에 주입
  • 스프링 설정 파일을 통해 평상시에는 일반 정책을 구현한 클래스를 사용하게 하다가 이벤트 때는 새로운 업그레이드 정책을 담은 클래스를 따로 만들어 DI 해주면 됨
public interface UserLevelUpgradePolicy {
    boolean canUpgradeLevel(User user);
    void upgradeLevel(User user);
}

' Spring > 토비의 스프링 3.1' 카테고리의 다른 글

5.3 서비스 추상화와 단일 책임 원칙  (0) 2019.02.06
5.2 트랜잭션 서비스 추상화  (0) 2019.02.06
5장. 서비스 추상화  (0) 2019.02.06
4.3 정리  (0) 2019.01.25
4.1 사라진 SQLException  (0) 2019.01.25