1장에서 4장까지 차례대로 만들어 왔던  UserDao는 User오브젝트에 담겨 있는 사용자 정보를 등록,조회,수정, 삭제하는 가장 기초적인 작업만 가능하다. 지금 부터는 간단한 비지니스 로직을 추가해보자.

구현해야할 비지니스 로직은 아래와 같다.

  • 사용자의 레벨은 BASIC,SILVER,GOLD중 하나다.
  • 사용자가 처음 가입하면 BASIC,이후 활동하면서 한단계씩 업그레이드된다.
  • 가입 후 50회 이상 로그인을 하면 BASIC에서 SILVER레벨이 된다.
  • SILVER레벨이면서 30번 이상 추천을 받으면 GOLD레벨이 된다.
  • 사용자 레벨의 변경 작업은 일정한 주기를 가지고 일괄적으로 진행된다. 변경 작업 전에는 조건을 충족하더라도 레벨의 변경이 일어나지 않는다.

비지니스 로직을 알았으니 이제 구현을 시작해보자. 우선, 사용자 레벨을 저장할 필드가 필요하다. 그럼 사용자 레벨을 어떤 타입으로 저장하는게 좋을까? DB에 varchar타입으로 "GOLD","SILVER","BASIC"으로 저장하는 방법도 있겠지만 , DB용량도 많이  차지하고 좋아 보이지 않는다. 그렇다면  범위가 작은 숫자로 관리하면 어떨까? DB용량도 많이 차지않고 가벼워서 좋다. 하지만, 의미없는 숫자를 property로 사용하면 타입이 안전하지 않아 위험할 수 있다. 예를 들어 , GOLD을 1, SILVER를 2, BASIC을 3으로 정해좋고 사용한다고 해보자. 실수로 1,2,3 의 숫자외에  값을 저장할 경우가 발생한다면???  컴파일러가 체크해 주지 못한다는 점이다. 잘못하다가는 레벨이 엉뚱하게 바뀌는 심각한 버그가 발생할 가능성이 잠재하게 된다. 따라서, 이럴경우는 enum을 이용하는것이 좋다. 

1. BASIC,SILVER,GOLD를 enum을 이용하여 정의해보자.

public enum Level {
	BASIC(1),SILVER(2),GOLD(3); //-> 세 개의 이늄 오브젝트 정의 
    
	private final int value;
	
	private Level(int value) { //-> DB에 저장할 값을 넣어줄 생성자를 만들어 준다. 
		this.value = value;
	}
	public int initValue() { //-> 값을 가져오는 메소스 
		return value;
	}	
	public static Level valueOf(int value) //-> 값으로 부터 Level타입 오브젝트를 가져오도록 
    {									   // 	만든 스태틱 메소스 
		switch(value) {
			case 1: return BASIC;
			case 2: return SILVER;
			case 3: return GOLD;	
			default: throw new AssertionError("Unknown value: "+ value);
		}
	}
}

enum을 이용하게 되면 DB에 저장할 int 타입의 값을 가지고 있지만, 겉으로는 Level타입의 오브젝트 이므로 안전하게 사용할 수 있는 장점이 있다. 예를 들어 user1.setLevel(1000) 같은 코드는 컴파일러가 알아서 error로 걸러주게 된다. 

2. User 클래스에 Level 필드를 추가해보자

public class User {
	String id;
	String name;
	String password;
	Level level;
	int login;
	int recommend;
	
	public User() {
		
	}	
	public User(String id, String name, String password, Level level, int login, int recommend) {
		super();
		this.id = id;
		this.name = name;
		this.password = password;
		this.level = level;
		this.login = login;
		this.recommend = recommend;
	}		
	public void upgradeLevel() {
		Level nextLevel = this.level.nextLevel();
		if(nextLevel ==null) {
			throw new IllegalStateException(this.level +"은 업그레이드가 불가능합니다."); 
		}else {
			this.level = nextLevel;
		}
	}
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	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;
	}

	
	public void setLevel(Level level) {
		this.level = level;
	}

	public Level getLevel() {
		return level;
	}
}

3. 필드가 추가 되었으니  UserDaoJdbc도 수정해 보겠다.

public class UserDaoJdbc implements UserDao{
	JdbcTemplate jdbcTemplate;	
		
	private RowMapper<User> userMapper = new RowMapper<User>() {					
		@Override
		public User mapRow(ResultSet rs, int rowNum)throws SQLException {
			// TODO Auto-generated method stub
			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 setDataSource(DataSource dataSource) {		
		this.jdbcTemplate = new JdbcTemplate(dataSource);	 
	}
	public int deleteAll() {		
		return this.jdbcTemplate.update("delete from users");
	}	
	public int add(final User user) {	
		return this.jdbcTemplate.update("insert into users(id, name, password,level,login,RECOMMEND) values(?,?,?,?,?,?)",
				user.getId(),user.getName(),user.getPassword(),user.getLevel().initValue(),user.getLogin(),user.getRecommend());
	}
	public int update(User user) {
		return this.jdbcTemplate.update("update users set name=?,password=?,level=?,login=?,recommend=? where id=?", 
				user.getName(),user.getPassword(),user.getLevel().initValue(),user.getLogin(),user.getRecommend(),user.getId());
	}
	
	public User get(String id) {		
		//가변 인자 사용하고 싶을때 
		return this.jdbcTemplate.queryForObject("select * from users where id=? ", this.userMapper,id);
	}
	public int getCount()  {
		return this.jdbcTemplate.queryForInt("select count(*) from users");
	}
	public List<User> getAll() {
		return this.jdbcTemplate.query("select * from users",this.userMapper);
	}
}

4. Domain와 Jdbc를 수정해두었으니, 사용자 레벨을 업그레이드 시킬 실제 비즈니스 로직이 들어갈 Service 클래스를 만들어보자.

public class UserService {
	@Autowired
	private UserDao userDao ;
	
	public void setUserDao(UserDao userDao) {
		this.userDao = userDao;
	}
	public void upgradeLevels() {
		List<User> Userlist = userDao.getAll();
		for( User user: Userlist){
			Boolean changed =null;
			if(user.getLevel() == Level.BASIC && user.getLogin() >= 50) {
				user.setLevel(Level.SILVER);
				changed=true;
				
			}else if(user.getLevel() == Level.SILVER && user.getRecommend() >=30) {
				user.setLevel(Level.GOLD);
				changed=true;
			}else  if(user.getLevel() == Level.GOLD){
				changed=false;
			}else {
				changed=false;
			}
			
			if(changed == true) {
				userDao.update(user);
			}
		}		
	}
	public void add(User user) {
		// TODO Auto-generated method stub
		if(user.getLevel() == null) user.setLevel(Level.BASIC);
		this.userDao.add(user);
	}
}

이제 비지니스 로직의 구현을 모두 마쳤다. 하지만 코드에 대한 점검이 필요하다.

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

위에 질문사항들을 체크해 나가다 보니, UserService의 upgradeLevels() 메소스에서 몇가지 문제가 발견되었다.

여러기능의 Logic들이 뒤섞여 있어 가독성이 떨어지는것이 가장 큰 문제 였다. 지금부터 각 기능의 로직들을 분리하여 가독성을 높일 수 있도록 코드 리팩토링을 해보겠다.

리팩토링

우선, upgradeLevels()의 역할이 무엇인지 분명히 할 필요가 있다. 해당 메소드는 이름 그래도 사용자의 레벨을 업그레이드 하는 역할에만 충실할 수 있도록 변경해야 한다.

사용자가 레벨을 변경할 수 있는가? 있다면 레벨을 변경한다.

위의 기본 기능에만 충실한 로직을 만들어보면 아래와 같다.

public void upgradeLevels() {
		List<User> userList = userDao.getAll();
		for(User user:userList) {
			if(canUpgradeLevel(user)) {
				upgradeLevel(user);
			}
		}
	}

한결 코드의 가독성이 좋아졌다.

이제 필요한 메소드를 추가로 더 만들어 Service 클래스를 완성해보자.

public class UserService {
	@Autowired
	private UserDao userDao ;
	
	public void setUserDao(UserDao userDao) {
		this.userDao = userDao;
	}
	public void upgradeLevels() {
		List<User> userList = userDao.getAll();
		for(User user:userList) {
			if(canUpgradeLevel(user)) {
				upgradeLevel(user);
			}
		}
	}
	private void upgradeLevel(User user) {
		// TODO Auto-generated method stub
		if(user.getLevel() == Level.BASIC) user.setLevel(Level.SILVER);
		else if(user.getLevel() == Level.SILVER) user.setLevel(Level.GOLD);
		userDao.update(user);
	}
	private boolean canUpgradeLevel(User user) {
		// TODO Auto-generated method stub
		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("Unknowned Level : " + currentLevel);
		}
	}
	public void add(User user) {
		// TODO Auto-generated method stub
		if(user.getLevel() == null) user.setLevel(Level.BASIC);
		this.userDao.add(user);
	}
}

하지만, 코드 작성 후, 살펴보니, upgradeLevel(User user) 메소드의 문제점이 추가적으로 발견되었다. 

private void upgradeLevel(User user) {
// TODO Auto-generated method stub
if(user.getLevel() == Level.BASIC) user.setLevel(Level.SILVER);
else if(user.getLevel() == Level.SILVER) user.setLevel(Level.GOLD);
userDao.update(user);

위 메소드는 현재 레벨을 체크 후, 다음 단계로 변경하고 DB에 업데이트 하는 두가지 기능이 섞여있다. 이제 이것도 더 분리해보자. 먼저 레벨의 순서와 다음 단계 레벨이 무엇인지를 결정하는 일은 Level에게 맡기자. 레벨의 순서를 굳이 UserService에 담을 이유가 없다. 아래는 변경한 Level 코드이다. Level enum에 다음 단계 레벨을 저장 할 수 있는 next 필드를 추가했다.

public enum Level {
	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 int initValue() {
		return value;
	}	
	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);
		}
	}
	public Level nextLevel() {
		return next;
	}
}

이번엔 사용자 정보가 바뀌는 부부을 UserService에서 User로 바꿔보자. User클래스에 아래 코드만 추가해주면 된다.

public void upgradeLevel() {
		Level nextLevel = this.level.nextLevel();
		if(nextLevel ==null) {
			throw new IllegalStateException(this.level +"은 업그레이드가 불가능합니다."); 
		}else {
			this.level = nextLevel;
		}
	}

Level와 User 클래스에 upgradeLevel(User user)에 뒤섞여 있던 로직을 분리하고 나니 upgradeLevel(User user)의 코드가 한결 간결해졌다.

private void upgradeLevel(User user) {
		// TODO Auto-generated method stub
		user.upgradeLevel();	
		userDao.update(user);
	}

지금까지 개선한 코드를 살펴보면, 각 오브젝트와 메소드가 각각 자기 몫의 책임을 맡아 하는 구조로 만들어 졌음을 알 수있다. 객체 지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다. 오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리이다. 

리팩토링을 완료 하기 전에 한가지만 더 수정해 보도록 하자. 레벨 업그레이드 조건 체크시, 숫자로 체크할 경우 업그레이드 조건이 변경될때 매번 해당 숫자를 일일히 찾아서 수정해야 하는 번거로움이 있다. 이럴경우는 해당 숫자를 상수로 선언해 놓고 사용하는 것이 좋다.

public class UserService implements UserLevelUpgradePolicy{
	@Autowired
	private UserDao userDao ;
	public static final int MIN_LOGCOUNT_FOR_SILVER=50;
	public static final int MIN_RECOMMEND_FOR_GOLD=30;
	
	public void setUserDao(UserDao userDao) {
		this.userDao = userDao;
	}
	public void upgradeLevels() {
		List<User> userList = userDao.getAll();
		for(User user:userList) {
			if(canUpgradeLevel(user)) {
				upgradeLevel(user);
			}
		}
	}
	public void upgradeLevel(User user) {
		// TODO Auto-generated method stub
		user.upgradeLevel();	
		userDao.update(user);
	}
	public boolean canUpgradeLevel(User user) {
		// TODO Auto-generated method stub
		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("Unknowned Level : " + currentLevel);
		}
	}
	public void add(User user) {
		// TODO Auto-generated method stub
		if(user.getLevel() == null) user.setLevel(Level.BASIC);
		this.userDao.add(user);
	}
}

서비스 추상화 공부 정리는 여기까지 하겠다. 다음 글에서 트랜잭션 서비스 추상화에 대해 정리해보도록 하자.

'Spring' 카테고리의 다른 글

log4J.xml 파일을 못찾을때 해결법  (0) 2020.03.14
5장 트랜잭션 서비스 추상화  (0) 2020.01.29
4장 예외  (0) 2020.01.05
3장 템플릿IV - 템플릿/콜백의 응용  (0) 2020.01.04
3장 템플릿 III - 스프링의 JdbcTemplate  (0) 2020.01.04

+ Recent posts