다음은 토비의 스프링 5.1 사용자 레벨 관리 기능 추가 를 공부하고 정리하였습니다.
요구사항
- 사용자의 레벨은 BASIC, SILVER, GOLD 세 가지 중 하나이다.
- 사용자가 처음 가입하면 BASIC 레벨이며, 이후 활동에 따라 한 단계씩 업그레이드 도니다.
- 가입 후 50 회 이상 로그인하면 BASIC에서 SILVER 레벨이 된다.
- SILVER 레벨이면서 30번 이상 추천을 받으면 GOLD 레벨이 된다.
- 사용자 레벨의 변경 작업은 일정한 주기를 가지고 일괄적으로 진행된다.
Level을 Enum 을 쓰자 !
만약 Enum이 아닌 class 로 쓴다면 ? 아래와 같이 일일히 int type으로 저장한다.이후 DB에서 어떻게 갖다 쓸 것인가?
if (user1.getLevel() == User.BASIC) {
user1.setLevel(User.SILVER);
}
# 여기에서 User.BASIC 은 int 형 1 일 것이다.
하지만 이렇게 int type으로 레벨을 사용하면 만약 다른 종류의 정보를 넣는 실수를 한다면(예를 들어 setLevel(3); → 예를 들어 한 사람의 post 수가 3개라서 잘못 넣었다고 하자.) 이런 식의 잘못된 구현의 실수를 컴파일러가 체크해줄 수 없다.
public enum Level {
BASIC(1), SILVER(2), GOLD(3);
private final int value;
Level(int value) { # 생성자
this.value = value;
}
public int intValue() { # enum -> int
return value;
}
public static Level valueOf(int value) { # int -> enum
switch(value) {
case 1: return BASIC;
case 2: return SILVER;
case 3: return GOLD;
default: throw new AssertionError("Unknown value: " + value);
}
}
}
Level Enum은 내부는 DB에 저장할 int 타입의 값을 가지지만 겉으로는 Level 타입의 오브젝트이므로 안전하게 사용할 수 있다.
public class User {
...
Level level ;
int login ;
int recommend ;
public Level getLevel() {
return level; # Level enum 가져오기
}
public void setLevel(Level level){
this.level = level; # Level enum set 하기
}
}
Test 코드에서는 이늄 타입의 오브젝트를 넣는 것을 볼 수 있다.
# Test code
user1 = new User( .., .., Level.BASIC, 1, 0); #레벨, 로그인, recommend
DB에 넣을 때
하지만 DB에 넣을 때에는 DB에 저장 가능한 정수형 값으로 변환해주어야 한다. 각 Level 이늄의 DB 저장용 값을 얻기 위해 Level에 미리 만들어둔 intValue() 메소드를 사용한다.
user.getLevel().intValue()
DB에서 뺄 때/조회할 때
DB에서 뺄 때, 즉 데이터를 조회할 때에는 다시 int로 level 정보를 가져온 후 valueOf()를 이용해 int 타입의 값을 Level 타입의 이늄 오브젝트로 만들어 setLevel() 메소드로 넣어준다.
user.setLevel(Level.valueOf(rs.getInt("level")));
테스트1 - 사용자 정보 수정용 update() 메소드
public void update(User user){
this.jdbcTemplate.update(
"update users set name = ?, password = ?, level = ?, login = ?, "+
"recommend = ? where id = ? ", user.getName(), user.getPassword(),
user.getLevel().intValue(), user.getLogin(), user.getRecommend(), user.getId()
);
}
실수가 일어날 수 있는 곳?
- SQL 문장에서 가장 많은 실수가 일어날 것이다 !
- Where 절을 빼먹는 실수가 있을 수 있겠다!
어떻게 확인할까?
방법1) 이를 확인하기 위해 JdbcTemplate 의 update()가 돌려주는 리턴 값을 확인해주자. 테이블의 내용에 영향을 주는 로우의 개수를 돌려주는 것.
방법2) 테스트를 보강해서 원하는 사용자 외의 정보는 변경되지 않았음을 직접 확인하자.
방법2 코드
@Test
public void update() {
dao.deleteAll();
dao.add(user1); // 수정 O
dao.add(user2); // 수정 X
user1.setName("오민규");
user1.setPassword("springno6");
user1.setLevel(Level.GOLD);
user1.setLogin(1000);
user1.setRecommend(999);
dao.update(user1);
User user1update = dao.get(user1.getId());
checkSameUser(user1, user1update);
User user2same = dao.get(user2.getId());
checkSameUser(user2, user2same);
}
UserService로 비즈니스 로직 등록하기
아래와 같이 주입하자 ! userService는 userDao를 쓰고, userServiceTest는 userService를 쓴다.
public class UserService {
UserDao userDao;
public void setUserDao(UserDao userDao) { // 주입
this.userDao = userDao;
}
}
비즈니스 로직을 만들어보고 UserServiceTest에서 검증할 것이다. 아래는 비즈니스 로직이다.
publiic class UserServiceTest {
@Autowired
UserService userService;
//1번 테스트 -> UserServiceTest에 UserService가 주입되었는가?
@Test
public void bean() {
assertThat(this.userService, is(notNullValue());
}
//2번 테스트 -> 사용자 레벨 업그레이드 메소드
public void upgradeLevels() {
List<User> users = userDao.getAll();
for (User user : users) {
Boolean changed = null; # flag
if (user.getLevel() == Level.BASIC && user.getLogin() >= 50) {
user.setLevel(Level.SILVER); # user 객체의 Level을 하나 올려서 set
changed = true;
}
else if (user.getLevel() == Level.SILVER && user.getRecommend() >=30 ) {
user.setLevel(Level.GOLD); # user 객체의 Level을 하나 올려서 set
changed = true;
}
else if (user.getLevel() == Level.GOLD) {changed = false;}
else {changed = false; }
if (changed) {userDao.update(user);} // 레벨이 변경되는 조건에 해당하면 user를 DB에 update한다.
}
}
}
User들을 위에서 만든 업그레이드 메소드에 넣는다. 이후 업그레이드 후의 예상 레벨과 같은지 확인한다.
테스트2 - 처음 가입하는 사용자가 기본적으로 BASIC 레벨로 할당되어야 하는 테스트
구현 어떻게 할까?
1) 그냥 맨처음에 User class에서 level 필드를 BASIC으로 초기화 2
) UserService 인 비지니스 로직에서 add() 를 만들어두고 사용자가 등록될 때 적용 → 로직은 “User 비지니스의 레벨이 비어 있다면 BASIC으로 할당시킬 것이다.”
이번에는 TDD 느낌으로 먼저 테스트를 만들어보자 하나는 GOLD 레벨, 하나는 레벨이 비어있는 사용자로 할당한 후 userService의 add() 함수를 수행 → DB 저장 → DB에서 꺼내와서 테스트가 잘 되었는지 확인
add() 메소드의 테스트
@Test
public void add() {
userDao.deleteAll();
User userWithLevel = users.get(4);
User userWithoutLevel - users.get(0);
userWithoutLevel.setLevel(null);
userService.add(userWithLevel);
userService.add(userWithoutLevel);
User userWithLevelRead = userDao.get(userWitheLevel.geId());
User userWithoutLevelRead = userDao.get(UserWithoutLevel.getId()));
asserThat(userWithLevelRead.getLevel(), is(userWithLevel.getLevel()));
asserThat(userWithoutLevelRead.getLevel(), is(Level.BASIC));
}
코드 개선
위에서 만든 upgradeLevel() 함수가 너무 if-else 문이 많다. 이걸 로직 별로 분리해 보겠다 ! → 이후, 역할과 책임을 분리해 보겠다 !
if (user.getLevel() == Level.BASIC && user.getLogin() >= 50) {
user.setLevel(Level.SILVER);
changed = true;
}
if (changed) {userDao.update(user);}
⓵ 현재 레벨이 무엇인지 파악하는 로직
user.getLevel() == Level.BASIC
⓶ 업그레이드 조건을 담은 로직
user.getLogin() >= 50
⓷ 다음 단계의 레벨은 무엇이며 업그레이드를 위한 작업은 어떤 것인지?
user.setLevel(Level.SILVER);
⓸ 의미가 없다. 그냥 멀리 떨어진 5의 작업을 알려주기 위해서 만든 flag
changed = true
이걸 역할과 책임을 분리해보면
- 레벨의 순서와 다음 단계 레벨이 무엇인지 결정하는 일은 userService 가 아닌, enum Level에게 맡기자!
- 사용자 정보가 바뀌는 부분을 userService 가 아닌, User 에게 맡기자 !
그러면 아래와 같이 updateLevel 로직이 간단해지고, userService에서의 updateLevel()은 User에게 레벨 업그레이드 작업을 해달라 라고 요청하고, User은 Level에게 다음 레벨이 무엇인지 알려달라고 요청하는 방식으로 동작하게 된다.
결국 UserService는 아래처럼 간단하게 바뀐다. 다른 로직들은 Level과 User에게 책임을 전달했기 때문!
private void upgradeLevel(User user) {
user.upgradeLevel();
userDao.upgrade(user);
}
아래에 책임을 전달한다. 빨간색 글자에 유의해서 변화사항을 볼 것 !!
이렇게 책임을 할당하면 좋은 점은..
⑴ 만약 이 upgrade 한 시점에 대해서 db나 class에 저장하고 싶다고 한다면 그냥 이 위에 로직에다가 User에 lastUpgraded 라는 필드를 추가하고 해당 upgradeLevel에 그 date를 new Date(); 로 넣어주면 된다.
⑵ 테스트를 짤 때에도 독립적으로 테스트하도록 짤 수 있다.
⑶ 로그를 남기는 로직을 추가할 때, 또는 추가작업 요청이 들어올 때 변화에 대응하기 더 쉬워진다.
각 오브젝트와 메소드가 자기 몫의 책임을 할당할 수 있는 구조로 설계하자 !
상수의 도입
public static final int MIN_LOGCOUNT_FOR_SILVER = 50; 으로 할당하여 다른 곳(특히나 테스트)에서도 userService에 정의해둔 상수를 사용할 수 있도록 만든다.
'BackEnd > JAVA\SPRING' 카테고리의 다른 글
[SPRING] 서비스 추상화 (0) | 2023.10.19 |
---|---|
[SPRING] AOP (0) | 2023.10.16 |
[SPRING] DI, 템플릿과 콜백 (0) | 2023.09.22 |
[SPRING] DI와 XML을 이용한 설정 (0) | 2023.09.05 |
[SPRING] IOC (0) | 2023.09.03 |