사용자 관리 기능에서 구현해야 할 비지니스 로직은 다음과 같다.
사용자의 레벨 : BASIC, SILVER, GOLD
최초 가입시 BASIC레벨이며 활동에 따라 한 단계씩 업그레이드 가능
가입 후 50회 이상 로그인을 하면 SILVER레벨로 등업
SILVER레벨에서 30이상 추천 받으면 GOLD레벨로 등업
레벨 변경작업은 일정한 주기로 일괄변경된다.
사용자 레벨 이늄 정의
package springbook.user.domain;
public enum Level {
BASIC(1), SILVER(2), GOLD(3);
private final int value;
private Level(int value) {
this.value = value;
}
public int getValue() {
return value;
}
// 값으로부터 Level 타입 오브젝트를 가져오도록 만든 스태틱 메소드
public static Level valueOf(int value) {
switch (value) {
case 1: return BASIC;
case 2: return SILVER;
case 3: return GOLD;
default : throw new AssertionError("Unkwon value : " + value);
}
}
}
User필드 추가
public class User {
// 생략 ...
Level level;
int login; // 로그인 횟수
int recommend; // 추천수
// 생성자 추가
public User(String id, String name, String password, Level level, int login, int recommend) {
this.id = id;
this.name = name;
this.password = password;
this.level = level;
this.login = login;
this.recommend = recommend;
}
// 세터/게터 메소드 생략
// 이하 생략 ...
}
DB의 USERS테이블에도 필드를 추가한다.
alter table users add level tinyint not null;
alter table users add login int not null;
alter table users add recommend int not null;
UserDaoTest의 수정
@Before
public void setUp() {
// 픽스처 수정
user1 = new User("gyumee", "박상철", "springno1",Level.BASIC,1,0);
user2 = new User("leegw700", "이길원", "springno2",Level.SILVER,55,10);
user3 = new User("bumjin", "박범진", "springno3",Level.GOLD,100,40);
}
User클래스에 다음의 생성자를 삭제한다.
// public User(String id, String name, String password) {
// super();
// this.id = id;
// this.name = name;
// this.password = password;
// }
UserDaoTest의 checkSameUser()메소드 수정
private void checkSameUser(User user1, User user2) {
assertEquals(user1.getId(), user2.getId());
assertEquals(user1.getName(), user2.getName());
assertEquals(user1.getPassword(), user2.getPassword());
//다음을 추가한다.
assertEquals(user1.getLevel(), user2.getLevel());
assertEquals(user1.getLogin(), user2.getLogin());
assertEquals(user1.getRecommend(), user2.getRecommend());
}
addAndGet()메소드 수정
@Test
public void addAndGet() throws ClassNotFoundException, SQLException {
this.dao.deleteAll();
assertEquals(this.dao.getCount(), 0);
this.dao.add(user1);
this.dao.add(user2);
assertEquals(dao.getCount(), 2);
// 주석처리된 부분은 삭제한다.
User userget1 = this.dao.get(user1.getId());
// assertEquals(userget1.getName(), user1.getName());
// assertEquals(userget1.getPassword(), user1.getPassword());
checkSameUser(userget1, user1); // 추가
User userget2 = this.dao.get(user2.getId());
// assertEquals(userget2.getName(), user2.getName());
// assertEquals(userget2.getPassword(), user2.getPassword());
checkSameUser(userget2, user2); // 추가
}
UserDao의 수정
private RowMapper<User> userMapper =
new RowMapper<User>() {
@Override
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.setLevel(Level.valueOf(rs.getInt("level"))); // 추가
user.setLogin(rs.getInt("login")); // 추가
user.setRecommend(rs.getInt("recommend")); // 추가
return user;
}
};
public void add(final User user){
// 쿼리문에 치환자를 3개를 추가하고 바인딩할 값을 update() 파라미터에 추가한다.
String query = "insert into users values(?,?,?,?,?,?)";
this.jdbcTemplate.update(query,user.getId(),user.getName(),user.getPassword(),
user.getLevel().getValue(), user.getLogin(), user.getRecommend());
}
테스트를 수행하면 성공한다.
# 사용자 수정 기능 추가
UserDao에 update()메소드 추가
public void update(User user) {
String query = "update users set name=?, password=?, level=?, login=?, "
+ "recommend=? where id=?";
this.jdbcTemplate.update(query, user.getName(), user.getPassword(),
user.getLevel().getValue(),user.getLogin(), user.getRecommend(), user.getId());
}
이제 UserDaoTest클래스에
update()메소드에 대한 테스트를 추가하자.
@Test
public void update() {
dao.deleteAll();
assertEquals(dao.getCount(), 0);
dao.add(user1);
user1.setName("오민규");
user1.setPassword("");
user1.setLevel(Level.GOLD);
user1.setLogin(1000);
user1.setRecommend(999);
dao.update(user1);
User user1update = dao.get(user1.getId());
checkSameUser(user1, user1update);
}
테스트를 수행하면 성공한다.
수정 테스트 보완
이 테스트의 문제점은
where가 없어도 아무런 경고 없이 동작할 수 있다는 것이다.
즉 update() 테스트는 수정할 로우의 내용이 바뀐 것만 확인할 뿐이지
수정하지 않아야 할 로우의 내용이 그대로 남아 있는지 확인해주지 못한다는 문제가 있다.
사용자를 두 명 등록해놓고, 그중 하나만 수정한 뒤에
수정된 사용자와 수정하지 않은 사용자 정보를 모두 검증해보자.
@Test
public void update() {
dao.deleteAll();
assertEquals(dao.getCount(), 0);
dao.add(user1);
dao.add(user2); // 추가
user1.setName("오민규");
user1.setPassword("");
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.upgradeLevels()
UserService 클래스와 빈 등록
UserService 클래스
package springbook.user.service;
import springbook.user.dao.UserDao;
public class UserService {
UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
빈 설정 : test-applicationContext.xml
<bean class="springbook.user.service.UserService">
<property name="userDao" ref="userDao"/>
</bean>
UserServiceTest 테스트 클래스
package springbook.user.service;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/test-applicationContext.xml")
public class UserServiceTest {
@Autowired
UserService userService;
// 빈의 주입을 확인하는 테스트를 추가한다.
@Test
public void bean() {
assertNotNull(this.userService);
}
}
테스트가 성공하면 UserService빈이 잘 등록됐음을 알 수 있다.
upgradeLevels()메소드
public void upgradeLevels() {
List<User> users = userDao.getAll();
for(User user : users) {
Boolean changed = null; // 레벨에 변화가 있는지 확인하는 플래그
// BASIC 레벨 업그레이드 작업
if(user.getLevel()==Level.BASIC && user.getLogin()>=50) {
user.setLevel(Level.SILVER);
changed = true;
}
// SILVER 레벨 업그레이드 작업
else if(user.getLevel()==Level.SILVER && user.getRecommend()>=30) {
user.setLevel(Level.GOLD);
changed = true;
}
// GOLD레벨은 변경이 일어나지 않음
else if(user.getLevel()==Level.GOLD) {
changed = false;
}
else {changed = false;}
// 레벨의 변경이 있는 경우에만 update()호출
if(changed) userDao.update(user);
}
}
upgradeLevels() 테스트
테스트의 픽스처의 개수가 UserDaoTest에서보다 많아졌으니
이번에는 각각 변수로 등록하는 리스트를 사용해보자.
UserServiceTest
@Before
public void setUp() {
users = Arrays.asList(
new User("bumjin","박범진","p1",Level.BASIC,49,0),
new User("joytouch","강명성","p2",Level.BASIC,50,0),
new User("erwins","신승한","p3",Level.SILVER,60,29),
new User("madnite1","이상호","p4",Level.SILVER,60,30),
new User("green","오민규","p5",Level.GOLD,100,100)
);
}
사용자별 레벨 업그레이드 테스트
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/test-applicationContext.xml")
public class UserServiceTest {
@Autowired
UserService userService;
@Autowired
UserDao userDao; // 추가
List<User> users;
// 생략...
@Test
public void upgradeLevels() {
userDao.deleteAll();
for(User user : users) userDao.add(user);
userService.upgradeLevels();
// 사용자별로 업그레이드 후의 예상 레벨 검증
checkLevel(users.get(0), Level.BASIC);
checkLevel(users.get(1), Level.SILVER);
checkLevel(users.get(2), Level.SILVER);
checkLevel(users.get(3), Level.GOLD);
checkLevel(users.get(4), Level.GOLD);
}
//DB에서 사용자 정보를 가져와 레벨을 확인하는 코드
private void checkLevel(User user, Level expectedLevel) {
User userUpdate = userDao.get(user.getId());
assertEquals(userUpdate.getLevel(), expectedLevel);
}
}
테스트를 수행하여 확인한다.
# UserService.add()
처음 가입하는 사용자는 기본적으로 BASIC레벨이어야 한다.
사용자 관리에 대한 비지니스 로직을 담고 있는
UserService에 이 로직을 넣어보자.
add()를 호출할 때 level값이 비어 있으면
로직을 따라서 BASIC을 부여하고
특별한 이유가 있어서 미리 설정된 레벨을
가진 USER오브젝트인 경우에는 그대로 두기로하자.
UserService클래스
public void add(User user) {
if(user.getLevel()==null) user.setLevel(Level.BASIC);
userDao.add(user);
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/test-applicationContext.xml")
public class UserServiceTest {
//생략 ...
@Autowired
UserDao userDao;
//생략 ...
@Test
public void add() {
userDao.deleteAll();
User userWithLevel = users.get(4); // 오민규 골드 레벨
// 테스트를 위해서 픽스처에 첫 번째 사용자인 박범진을 가져와 레벨을 초기화시킨다.
User userWithoutLevel = users.get(0);
userWithoutLevel.setLevel(null);
// DB에 삽입
userService.add(userWithLevel);
userService.add(userWithoutLevel);
// DB의 저장된 결과를 가져온다.
User userWithLevelRead = userDao.get(userWithLevel.getId());
User userWithoutLevelRead = userDao.get(userWithoutLevel.getId());
//검증
assertEquals(userWithLevelRead.getLevel(), userWithLevel.getLevel());
assertEquals(userWithoutLevelRead.getLevel(), userWithoutLevel.getLevel());
}
// 생략..
}
테스트를 수행하여 확인한다.
# 코드 개선
기존의 upgradeLevels()메소드는 자주 변경될 가능성이 있는
구체적인 내용이 추상적인 로직의 흐름과 함께 섞여있다.
레벨을 업그레이드 하는 작업의 기본흐름부터 만들어보자.
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()>=50);
case SILVER: return (user.getRecommend()>=30);
case GOLD: return false;
default : throw new IllegalArgumentException("Unknown Level : " + currentLevel);
}
}
레벨 업그레이드 작업 메소드를 만들자.
// 업그레이드 작업을 처리하는 메소드
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);
}
테스트를 수행하여 확인한다.
upgradeLevel()메소드는 사용자 오브젝트의 레벨정보를 다음 단계로 변경하고,
변경된 오브젝트를 DB에 업데이트하는 두 가지 작업을 수행한다.
다음 단계가 무엇인가하는 로직과
그 때 사용자 오브젝트의 level필드를 변경해준다는 로직이 함께 있는데다가
예외상황에 대한 처리가 없다.
이것도 더 분리해보자.
다음 단계레벨이 무엇인지 결정하는 일은 Level에 맡기자
Level 이늄을 수정한다.
package springbook.user.domain;
public enum Level {
//이늄 선언에 DB에 저장할 값과 함께 다음 단계의 레벨 정보 추가
GOLD(3, null), SILVER(2, GOLD), BASIC(1, SILVER);
private final int value;
// 다음 레벨 정보를 담는 필드
private final Level next;
private Level(int value, Level next) {
this.value = value;
this.next = next;
}
// 다음 레벨 값을 불러오는 메소드
public Level nextLevel() {
return this.next;
}
// 생략...
}
User의 내보정보가 변경되는 것은 UserService보다는 User가 다루는게 적절하다.
User클래스에 다음의 메소드를 추가하자.
// 사용자 레벨 업그레이드
public void upgradeLevel() {
Level nextLevel = this.level.nextLevel();
if(nextLevel == null) {
throw new IllegalStateException(this.level+"은 업그레이드가 불가능합니다.");
} else {
this.level = nextLevel;
}
}
UserService클래스의 upgradeLevel() 메소드도 수정하자.
// 업그레이드 작업을 처리하는 메소드
private void upgradeLevel(User user) {
user.upgradeLevel();
userDao.update(user);
}
테스트를 수행하여 확인한다.
# User 테스트
User클래스에 추가한 upgradeLevel()메소드에 대한 테스트를 추가하자.
UserTest클래스
package springbook.user.service;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import springbook.user.domain.Level;
import springbook.user.domain.User;
public class UserTest {
User user;
@Before
public void setUp() {
user = new User();
}
@Test
public void upgradeLevel() {
// Level 이늄에 있는 모든 레벨 값들 (즉 BASIC,SILVE,GOLD)
Level[] levels = Level.values();
for(Level level : levels) {
if(level.nextLevel() == null) continue;
// 다음레벨이 null인 레벨값은 GOLD이다. 즉 GOLD에 대하여는 레벨 수정을 하지 않겠다는 뜻
user.setLevel(level);
user.upgradeLevel();
assertEquals(user.getLevel(), level.nextLevel());
}
}
@Test(expected = IllegalStateException.class)
public void cannotUpgradeLevel() {
Level[] levels = Level.values();
for(Level level : levels ) {
if(level.nextLevel() !=null ) continue; // BASIC,SILVER 레벨에 대하여 레벨을 수정하지 않겠다는 뜻
user.setLevel(level); // GOLD
user.upgradeLevel(); // GOLD레벨을 업그레이드 하면 예외가 발생한다.
}
}
}
UserService Test 개선
기존의 테스트 메소드인 upgradeLevels()메소드를 살펴보면
checkLevel()메소드를 호출할 때 다음 단계의 레벨이 무엇인지 넣었다.
@Test
public void upgradeLevels() {
userDao.deleteAll();
for(User user : users) userDao.add(user);
userService.upgradeLevels();
// 다음단계의 레벨을 직접 작성
checkLevel(users.get(0), Level.BASIC);
checkLevel(users.get(1), Level.SILVER);
checkLevel(users.get(2), Level.SILVER);
checkLevel(users.get(3), Level.GOLD);
checkLevel(users.get(4), Level.GOLD);
}
Level이 갖고 있어야 할 다음레벨이 무엇인가 하는 정보는 테스트에 직접 넣어둘 이유가 없다.
레벨이 추가되거나 변경되면 테스트도 따라 수정해야한다.
다음과 같이 수정하자.
@Test
public void upgradeLevels() {
userDao.deleteAll();
for(User user : users) userDao.add(user);
userService.upgradeLevels();
// 다음 레벨을 직접 지정하지 않고 업그레이드가 일어났는지 여부만 지정한다.
checkLevelUpgrade(users.get(0), false);
checkLevelUpgrade(users.get(1), true);
checkLevelUpgrade(users.get(2), false);
checkLevelUpgrade(users.get(3), true);
checkLevelUpgrade(users.get(4), false);
}
// 기존의 checkLevel메소드는 삭제했다.
// 두번째 파라미터 : 다음 레벨로 업그레이드 될 것인지 여부
private void checkLevelUpgrade(User user, boolean upgraded) {
User userUpdate = userDao.get(user.getId());
if(upgraded) {
// 업그레이드가 일어 났는지 확인
assertEquals(userUpdate.getLevel(), user.getLevel().nextLevel());
} else {
// 업그레이드가 일어나지 않았는지 확인
assertEquals(userUpdate.getLevel(), user.getLevel());
}
}
다음은 코드에 나타난 중복을 제거하자
업그레이드 조건인 로그인 횟수와 추천횟수가
애플리케이션 코드와 테스트 코드에 중복되어 나타난다.
UserServiceTest 클래스
// 추가
public static final int MIN_LOGCOUNT_FOR_SILVER = 50;
public static final int MIN_RECCOUNT_FOR_GOLD = 30;
// 업그레이드 가능한 사용자인지 확인하는 메소드
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_RECCOUNT_FOR_GOLD);// 수정
case GOLD: return false;
default : throw new IllegalArgumentException("Unknown Level : " + currentLevel);
}
}
테스트도 UserService에서 정의해둔 상수를 사용하도록 한다.
import static springbook.user.service.UserService.MIN_LOGCOUNT_FOR_SILVER;
import static springbook.user.service.UserService.MIN_RECCOUNT_FOR_GOLD;;
//생략 ...
@Before
public void setUp() {
users = Arrays.asList(
// 테스트에서는 가능한 한 경계값을 사용하는 것이 좋다.
new User("bumjin","박범진","p1",Level.BASIC,MIN_LOGCOUNT_FOR_SILVER-1,0),
new User("joytouch","강명성","p2",Level.BASIC,MIN_LOGCOUNT_FOR_SILVER,0),
new User("erwins","신승한","p3",Level.SILVER,60,MIN_RECCOUNT_FOR_GOLD-1),
new User("madnite1","이상호","p4",Level.SILVER,60,MIN_RECCOUNT_FOR_GOLD),
new User("green","오민규","p5",Level.GOLD,100,Integer.MAX_VALUE)
);
}
테스트를 실행하여 확인한다.
[6.1] 트랜잭션 코드의 분리 (0) | 2020.11.02 |
---|---|
[5.2] 트랜잭션 서비스 추상화 (0) | 2020.10.31 |
[3.6] 스프링의 JdbcTemplate (0) | 2020.10.29 |
[3.5] 템플릿과 콜백 (0) | 2020.10.29 |
[3.4] 컨텍스트와 DI (0) | 2020.10.29 |
댓글 영역