상세 컨텐츠

본문 제목

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

토비의스프링

by kwanghyup 2020. 10. 30. 17:11

본문

사용자 관리 기능에서 구현해야 할 비지니스 로직은 다음과 같다.

 

사용자의 레벨 : 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

관련글 더보기

댓글 영역