✅ UserService.add()
처음 가입하는 사용자는 기본적으로 BASIC 레벨이어야 한다는 로직을 어디에 담아야 할까요? UserDaoJdbc는 주어진 User 오브젝트를 DB에 정보를 넣고 읽는 방법에만 관심을 가져야지, 비즈니스적인 의미를 지닌 정보를 설정하는 책임을 지는 것은 바람직 하지 않아 보입니다.
비즈니스 로직을 담당하고 있는 UserSerivce에 add() 메소드를 만들고 이를 통해 User 오브젝트를 받아 처음 가입하는 사용자의 레벨을 BASIC으로 설정하고 DB에 저장하는 로직이 가장 바람직해 보입니다. TDD 방식으로 테스트 코드를 먼저 생성한 후 관련 로직을 만들어 보겠습니다.
- UserServiceTest, add()
@Test public void add() { userDao.deleteAll(); User userWithLevel = users.get(4); //GOLD User userWithOutLevel = users.get(0); //BASIC userWithOutLevel.setLevel(null); //BASIC -> NULL, 비어있으면 다시 BASIC으로 설정되어야함. //GOLD -> GOLD 그대로 유지 userService.add(userWithLevel); //Null -> BASIC 처음 가입 유저는 BASIC으로 설정 userService.add(userWithOutLevel); //DB에 저장된 것을 불러와서 저장한 값이랑 비교함. Optional<User> optionalLevelUser = userDao.get(userWithLevel.getId()); if(optionalLevelUser != null) { User userWithLevelRead = optionalLevelUser.get(); assertEquals(userWithLevelRead.getLevel(), userWithLevel.getLevel()); } Optional<User> optionalOutLevelUser = userDao.get(userWithOutLevel.getId()); if(optionalOutLevelUser != null) { User userWithOutLevelRead = optionalOutLevelUser.get(); assertEquals(userWithOutLevelRead.getLevel(), userWithOutLevel.getLevel()); } }
users는 List<User>로 여러 Level을 가진 사용자 객체가 들어있습니다. 그 중에서 BASIC 사용자 객체를 userWithOutLevel 변수에 대입하여 그 값을 null로 변경하였습니다. 이때
add()
메소드가 제대로 동작한다면 GOLD 사용자 객체는 그대로 그 값을 유지하고, 값이 없는 userWithOutLevel의 값은 BASIC으로 변환되어야 할 것입니다.
- UserService
//== 처음 사용자에게 BASIC Level 부여 ==// public void add(User user) { if (user.getLevel() == null) user.setLevel(Level.BASIC); userDao.add(user); }
if 문을 통해서 사용자의 Level의 값이 null이면 BASIC 값이 할당되도록 하였고, DAO를 통해서 DB에 저장되는 간단한 로직을 구성했습니다.
👉 테스트를 하면 잘 통과하는 것을 볼 수 있습니다. 다만, 테스트가 너무 복잡한 것이 흠입니다. 간단한 비즈니스 로직을 테스트하는데 DAO까지 사용해야 하기 때문에 이를 간단히 하는 법을 알아보겠습니다.
✅ 코드 개선
작성된 코드를 개선시키기 위해서 다음과 같은 질문을 던져볼 수 있습니다.
- 코드에 중복된 부분은 없는가?
- 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가?
- 앞으로 변경이 일어난다면 유지보수에 용이하도록 작성되었는가?
🔹upgradeLevels() 리팩토링
반복적인 if 문을 통해서 Level 이늄이 변경되거나, 기준 값이 변경되었을 경우 모든 조건식을 수정해야 하므로 유지보수적 측면에서 좋지 않음을 알 수 있습니다. 이를 리팩토링해보겠습니다.
- upgradeLevels()
//로그인과 추천수에 따라 사용자의 레벨을 업그레이드하는 비즈니스 코드 public void upgradeLevels() { List<User> users = userDao.getAll(); for(User user : users) { if (canUpgradeLevel(user)) { upgradeLevel(user); } } }
DB에 있는 사용자의 정보를 모두 불러온 후
canUpgradeLevel()
과upgradeLevel()
메소드를 실행해서 Level의 값을 변경하는 간단한 로직으로 바꾸었습니다.
- canUpgradeLevel()
//== 업그레이드가 가능한지 확인하는 코드 ==// private 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); } }
if 문이 아닌 switch-case 문을 사용해서, User 객체로부터 가져온 Level의 값이 case 문의 값과 일치할 때에 다음 Level로 넘어가는 조건이 True인지 False 인지 반환하는 코드입니다.
- upgradeLevel()
//== 업그레이드가 가능할 때 실제로 값을 변경하는 코드 ==// private void upgradeLevel(User user) { if(user.getLevel() == Level.BASIC) user.setLevel(Level.SILVER); else if (user.getLevel() == Level.SILVER) user.setLevel(Level.GOLD); userDao.update(user); }
canUpgradeLevel()
메소드를 통해 True가 반환되면, User 객체의 Level 값을 수정해서 DB에 저장하는 코드입니다.👉 그러나 upgradeLevel() 메소드는 현재 User 객체의 다음 Level이 무엇인지 알 수 있고, 그때 User 객체의 필드 값을 변경해주는 코드가 너무 잘 드러나 있습니다. Level 이늄을 수정해서 이를 조금 더 추상화시켜보겠습니다.
🔹Level 이늄 변경과 코드 개선
- Level
package com.jhcode.spring.ch5.user.domain; public enum Level { GOLD(3, null), SILVER(2, GOLD), BASIC(1, SILVER); private final int value; private final Level next; //생성자 -> DB에 저장할 값인 필드를 초기화하는 역할 Level(int value, Level next){ this.value = value; this.next = next; } //필드 value의 값을 가져오는 메소드 public int intValue() { return value; } //필드 next의 값을 가져오는 메소드 public Level nextLevel() { return this.next; } //인수로 받은 값을 통해 각 상수 반환. 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); } } }
Level 이늄에 next 변수를 추가해서 다음 단계 레벨 정보를 저장할 필드를 추가합니다. 생성자를 통해 Level 이늄을 만들 때 다음 단계에 대한 정보도 저장할 수 있도록 합니다.
이렇게 하면 다음 단계의 Level이 무엇인지 if 문을 통해서 확인하지 않아도 됩니다. 각각의 Level은 next 필드를 통해 다음 단계로 업그레이드할 정보를 가지고 있기 때문입니다.
- User, upgradeLevel()
//== 레벨 업그레이드 로직 ==// public void upgradeLevel() { Level nextLevel = this.level.nextLevel(); if(nextLevel != null) { this.level = nextLevel; } else { throw new IllegalStateException(this.level + "은 업그레이드가 불가능합니다"); } }
User 객체 안에서 위의 메소드를 통해서 그 다음 단계의 Level 값으로 변경하는 로직을 만듭니다. 이렇게 되면 Service에서는 다음 단계로 가는 조건이 충족한지만 확인하면 되고, 그 값을 변경하는 것은 User 객체가 담당하게 됩니다.
- UserService, upgradeLevel()
//== 업그레이드가 가능할 때 실제로 값을 변경하는 코드 ==// private void upgradeLevel(User user) { user.upgradeLevel(); userDao.update(user); }
다음 단계에 대한 정보는 Level 이늄이 가지고 있고, 다음 단계로 업그레이드 하는 로직은 User 객체가 가지고 있기 때문에, 다음 단계의 조건이 충족한지만 Service에서 확인하면 되었습니다. 그 외의 로직은 Level, User가 나누어 담당하므로 코드가 간단하게 됩니다.
- UserService는 User의 다음 단계의 조건이 충족한지 확인하고 User에게 “레벨 업그레이드 작업을 해달라”고 요청합니다.
- User는 Level에게 “다음 레벨의 정보가 무엇인지 알려달라”고 요청하여 동작합니다.
👉 이렇게 서로 간의 책임을 분리하는 것은 유지 보수에 용이해질 수 있습니다. 예를 들어, 레벨 업그레드의 조건에 대해서 수정할 때는 UserService의 canUpgradeLevel()
메소드만 변경하면 되고, Level의 단계를 추가할 때는 Level 이늄만 수정하면 되기 때문입니다.
✅ User 테스트
User 클래스에 새로운 기능을 추가했기 때문에 해당 기능이 제대로 동작하는지 테스트하는 코드를 작성해보겠습니다.
- UserTest
package com.jhcode.spring.ch5.user.domain; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import com.jhcode.spring.ch5.user.dao.DaoFactory; @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {DaoFactory.class}) public class UserTest { User user; @BeforeEach public void setUp() { user = new User(); } @Test public void upgradeLevel() { Level[] levels = Level.values(); for(Level level : levels) { if(level.nextLevel() == null) continue; user.setLevel(level); user.upgradeLevel(); assertEquals(user.getLevel(), level.nextLevel()); } } @Test public void cannotUpgradeLevel() { Level[] levels = Level.values(); for (Level level : levels) { if (level.nextLevel() != null) continue; user.setLevel(level); //해당 예외가 발생하면 테스트 성공 assertThrows(IllegalStateException.class, user::upgradeLevel); } } }
✅ UserServicetTest 개선
- UserServiceTest
//User의 Level과 인수로 받은 Level의 값을 비교하는 메소드 //어떤 레벨로 바뀌는지가 아니라, 다음 레벨로 바뀔 것인지를 확인한다. private void checkLevel(User user, boolean upgraded) { Optional<User> optionalUser = userDao.get(user.getId()); if(optionalUser != null) { User userUpdate = optionalUser.get(); if(upgraded) { //조건에 일치해서 다음 레벨로 업그레이드 되었을 경우 assertEquals(userUpdate.getLevel(), user.getLevel().nextLevel()); //조건에 일치하지 않아서 다음 레벨로 업그레이드 되지 않았을 경우 } else { assertEquals(userUpdate.getLevel(), user.getLevel()); } } } @Test public void upgradeLevels() { userDao.deleteAll(); for(User user : users) { userDao.add(user); } //DB의 모든 User을 가지고와서 Level 등급을 조정함 userService.upgradeLevels(); checkLevel(users.get(0), false); checkLevel(users.get(1), true); checkLevel(users.get(2), false); checkLevel(users.get(3), true); checkLevel(users.get(4), false); }
기존의 테스트 코드는 업그레이드될 조건을 테스트하는 것인지, 업그레이드되고 난 이후의 값이 정확한지 판단하는 것인지 분명하지 않았습니다. 이제는 조건을 확인하고 난 후, 그 값이 일치하는지까지 명확하게 테스트 코드에 드러납니다.
✅ 중복 코드 제거와 상수 사용
- UserService
package com.jhcode.spring.ch5.user.service; import java.util.List; import com.jhcode.spring.ch5.user.dao.UserDao; import com.jhcode.spring.ch5.user.domain.Level; import com.jhcode.spring.ch5.user.domain.User; public class UserService { public static final int MIN_LOGCOUNT_FOR_SILVER = 50; public static final int MIN_RECOMMEND_FOR_GOLD = 30; private UserDao userDao; //UserDao 주입 public void setUserDao(UserDao userDao) { this.userDao = userDao; } //로그인과 추천수에 따라 사용자의 레벨을 업그레이드하는 비즈니스 코드 public void upgradeLevels() { List<User> users = userDao.getAll(); for(User user : users) { if (canUpgradeLevel(user)) { upgradeLevel(user); } } } //== 업그레이드가 가능한지 확인하는 코드 ==// private boolean canUpgradeLevel(User user) { Level currentLevel = user.getLevel(); switch(currentLevel) { 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 : " + currentLevel); } }
- UserServiceTest
package com.jhcode.spring.ch5.user.service; import static com.jhcode.spring.ch5.user.service.UserService.MIN_LOGCOUNT_FOR_SILVER; import static com.jhcode.spring.ch5.user.service.UserService.MIN_RECOMMEND_FOR_GOLD; import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.Arrays; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import com.jhcode.spring.ch5.user.dao.UserDao; import com.jhcode.spring.ch5.user.domain.Level; import com.jhcode.spring.ch5.user.domain.User; @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {TestServiceFactory.class}) public class UserServiceTest { @Autowired private UserService userService; @Autowired private UserDao userDao; List<User> users; @BeforeEach public void setUp() { //배열을 리스트로 만들어주는 메소드 users = Arrays.asList( new User("user1", "user1", "p1", Level.BASIC, MIN_LOGCOUNT_FOR_SILVER -1, 0), new User("user2", "user2", "p2", Level.BASIC, MIN_LOGCOUNT_FOR_SILVER, 0), new User("user3", "user3", "p3", Level.SILVER, 60, MIN_RECOMMEND_FOR_GOLD -1), new User("user4", "user4", "p4", Level.SILVER, 60, MIN_RECOMMEND_FOR_GOLD), new User("user5", "user5", "p5", Level.GOLD, 100, 100) ); }
상단에 직접 static 상수를 선언해주어야지 쓸 수 있습니다. 자동 import가 되지 않습니다. 이렇게 했을 때의 이점은 테스트 코드만 보더라도 해당 값을 무엇을 위해 넣게 되었는지 알 수 있다는 점입니다. 이전에는 그 값이 무엇을 의미하는지 몰랐지만, 상수를 사용함으로써 그 값이 다음 단계로 가기 위한 조건이라는 점을 바로 파악할 수 있습니다.
✅ 혼자서 해보기
챕터 마지막에 사용자 레벨 업그레이드 정책을 DI를 통해서 UserService에 주입하는 형태로 만들어보는 과제를 남겨두었습니다. 이를 한 번 해보겠습니다.
- UserLevelUpgradePolicy
package com.jhcode.spring.ch5.user.service; import com.jhcode.spring.ch5.user.dao.UserDao; import com.jhcode.spring.ch5.user.domain.User; public interface UserLevelUpgradePolicy { boolean canUpgradeLevel(User user); void upgradeLevel(User user, UserDao userDao); }
- UserLevelUpgradeImpl
package com.jhcode.spring.ch5.user.service; import com.jhcode.spring.ch5.user.dao.UserDao; import com.jhcode.spring.ch5.user.domain.Level; import com.jhcode.spring.ch5.user.domain.User; public class UserLevelUpgradeImpl implements UserLevelUpgradePolicy { public static final int MIN_LOGCOUNT_FOR_SILVER = 50; public static final int MIN_RECOMMEND_FOR_GOLD = 30; @Override public boolean canUpgradeLevel(User user) { Level currentLevel = user.getLevel(); switch(currentLevel) { 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 : " + currentLevel); } } @Override public void upgradeLevel(User user, UserDao userDao) { user.upgradeLevel(); userDao.update(user); } }
- UserService
public class UserService { private UserLevelUpgradePolicy userLevleUpgrade; private UserDao userDao; //UserDao 주입 public void setUserDao(UserDao userDao) { this.userDao = userDao; } //UserLevelUpgradePolicy 주입 public void setUserLevelUpgradePolicy(UserLevelUpgradePolicy userLevelUpgradePolicy) { this.userLevleUpgrade = userLevelUpgradePolicy; } //...생략 //== 업그레이드가 가능한지 확인하는 코드 ==// private boolean canUpgradeLevel(User user) { return userLevleUpgrade.canUpgradeLevel(user); } //== 업그레이드가 가능할 때 실제로 값을 변경하는 코드 ==// private void upgradeLevel(User user) { userLevleUpgrade.upgradeLevel(user, userDao); }
UserLevelUpgradePolicy 필드 추가, 이를 주입 받는 setter 추가,
canUpgradeLevel()
,upgradeLevel()
메소드를 주입 받은 객체의 메소드를 호출하도록 변경하였습니다.
- TestServiceFactory
@Bean public UserService userService() { UserService userService = new UserService(); UserLevelUpgradePolicy userLevelUpgradePolicy = new UserLevelUpgradeImpl(); userService.setUserDao(userDao()); userService.setUserLevelUpgradePolicy(userLevelUpgradePolicy); return userService; }
UserService 객체를 생성하여 Bean으로 등록할 때, UserLevelUpgradePolicy 객체를 생성하고, UserService에 주입하였습니다.
- UserServiceTest
//임시 유저를 생성하기 위한 상수값 import 변경 import static com.jhcode.spring.ch5.user.service.UserLevelUpgradeImpl.MIN_LOGCOUNT_FOR_SILVER; import static com.jhcode.spring.ch5.user.service.UserLevelUpgradeImpl.MIN_RECOMMEND_FOR_GOLD;
👉 어렵지 않게 구현할 수 있기 때문에 긴 설명없이 넘어가겠습니다.
📖 토비 스프링 3.1 -p334~349
🚩jhcode33의 toby-spring-study.git으로 이동하기
Uploaded by N2T