사용자 레벨 관리 기능을 추가하며 배우는 서비스 추상화

정훈희·2022년 11월 5일
0

Spring

목록 보기
17/24
post-thumbnail

참조

  • 토비의 스프링 vol.1 317p~348p

enum

기존 User class에 사용자 레벨 속성을 추가한다고 하고, 사용자의 레벨은 BASIC = 1, SILVER = 2, GOLD = 3 으로 나뉜다고 해보자.

이를 상수 값으로 정해놓고 int타입으로 레벨을 사용한다면 아래와 같다.

class User {
	...
	private static final int BA5IC = 1;
	private static final int SILVER = 2;
	private static final int GOLD = 3;
	...
}

하지만 위와같이 짜는 경우 Level을 정할때 user1.setLevel(User.BASIC) 이런식으로 깔끔하게 넣을 수 있긴 하지만 user1.setLevel(1000) 이런식으로 잘못된 값을 넣을 수 있다.

이럴 때 Enum을 이용할 수 있다.

public enum Level {
	BASIC(1), SILVER(2), GOLD(3); // 세 개의 Enum 오브젝트 정의
	private final int value;
	Level(int value) { //DB에 저장할 값올 넣어줄 생성자를 만들어둔다
		this.value = value;
	}
	public int intValue() { // 값율 가져오는 메소드
		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("Unknown value: " + value);
		}
	}
}

이렇게 Enum을 사용하면 user1.setLevel(Level.BASIC) 이런식으로 깔끔하게 값을 넣을 수 있고, Level형으로 매개변수를 받기 때문에 user1.setLevel(1000) 이런식으로 잘못된 값을 넣을 일도 없다.

다만, Level 은 DB에 저장될 수 있는 타입이 아니므로 정수로 변환하여 DB에 넣어줘야 한다.

→ Level에 만들어둔 intValue() 메소드를 사용하면 된다.


이번엔 사용자 수정 기능을 추가하려 한다. 수정할 정보가 담긴 User 오브젝트를 전달하면 필드 정보를 모두 변경해주는 메소드를 하나 만들어보자.

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 문장이다. UPDATE문은 WHERE가 없어도 아무런 경고 없이 정상적으로 동작한 것으로 보인다.

이러한 문제를 해결하기 위해서는 원하는 사용자 외의 정보는 변경되지 않았음을 확인해줘야 한다.

즉 사용자를 두 명 등록해놓고, 그 중 하나만 수정한 뒤에 수정된 사용자와 수정하지 않은 사용자의 정보를 모두 확인하면 된다.

@Test
public void update() {
	dao.deleteAll();
	dao.add(userl); // 수정할 사용자
	dao.add(user2);	// 수정하지 않을 사용자
	... // user1의 정보 변경	
	dao.update(user1);
	User user1update = dao.get(user1.getId());
	checkSameUser(user1, user1update);
	User user2same = dao.get(user2.getld());
	checkSameUser(user2, user2same);
}

위와 같이 테스트 코드를 작성하였고, 이렇게 하면 where 절을 빼면 모든 컬럼이 업데이트 되므로 테스트가 꼼꼼하게 작동한다.


이번에는 레벨 업그레이드 메소드를 만들어보려고한다. 이러한 사용자 관리 로직을 DAO에 넣는 것은 좋지않다. DAO는 어떻게 데이터를 가져오고 조작할지를 다루는 곳이지 비즈니스 로직을 두는 곳이 아니기 때문이다.

→ UserService 클래스를 새로 만들자

UserService는 UserDao의 update 함수 등 UserDao를 사용하므로 userDao Bean 객체를 DI받아 사용하게 만든다.

DI를 적용하기 위해선 UserService도 빈으로 등록해야한다. 그림으로 보면 아래와 같다.

image

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면 최고레벨 이므로 레벨 변경 X
		else if (user.getLevel() == Level.GOLD) { changed =false; }
		else { changed =false; }
		// 레벨의 변경이 있는 경우에만 update() 호출
		if (changed) { userDao.update(user); }
} 

모든 유저들의 레벨업 조건을 확인하고, 조건에 해당되는 유저의 레벨을 변경하는 메소드이다.


사용자 관리 비즈니스 로직에서 남은 부분이 있다. 처음 가입하는 사용자는 기본적으로 BASIC 레벨이어야 하는 부분이다.

이를 UserDao의 add() 메소드에 넣는것은 적합하지 않다. UserDao는 DB에 정보를 넣고 읽는 방법에만 관심을 둬야 하기 때문이다.

→ UserService에도 add()를 만들어 두고 사용자가 등록될 때 적용할 만한 비즈니스 로직을 담당하게 하면 된다.

public void add(User user) {
	if (user.getLevel() == null) user.setLevel(Level.BASIC);
	userDao.add(user);
}

위와 같이 add() 메소드를 만들었고, 만약 user에 레벨이 없다면 BASIC을 기본값으로 넣어주는 모습이다.


upgradeLevels() 리팩토링

upgradeLevels 메소드를 살펴보면 가독성이 떨어지고, 성격이 다른 여러가지 로직이 섞여있기 때문이다.

첫번째 if문을 보면 user.getLevel() == Level.BASIC 이 부분은 현재 레벨이 무엇인지 파악하는 로직이고, user.getLogin() >= 50 이 부분은 업그레이드 조건을 담은 로직이다.

user.setLevel(Level.SILVER); 이 부분은 다음 단계의 레벨이 무엇이며 업그레이드를 위한 작업까지 함께 담겨있다.

changed =true;if (changed) { userDao.update(user); } 는 업데이트를 할지 여부를 정하고, 여부에 따라 업데이트를 하는 작업이다.

upgradeLevels를 리팩토링 해보자.

먼저 추상적인 레벨에서 로직을 구성해보면 다음과 같다.

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

정리하면, 모든 유저를 확인하며 Level 업그레이드가 가능한지 확인하고 가능하면 업그레이드 한다.

이제 각 메소드를 구현해보자. 먼저 canUpgradeLevel 메소드를 구현해보면 아래와 같다.

private boolean canUpgradeLevel(User user) {
	Level currentLevel = user.getLevel();
	switch(currentLevel) {
		case BASIC: return (user.getLogin() >= 58);
		case SILVER: return (user.getRecommend() >= 38);
		case GOLD: return false;
		default : throw new IllegalArgumentException("Unknown Level: " + currentLevel);
	}
}

유저의 레벨에 따른 레벨업 조건을 확인해서 업그레이드가 가능하면 true, 그렇지 않으면 false를 return한다.

만약 세가지 레벨에 해당하지 않는 레벨이 있다면 예외를 던진다.

이번엔 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);
}

이 메소드는 사용자 오브젝트의 레벨정보를 다음 단계로 변경하고, 변경된 오브젝트를 DB에 업데이트히는 두 가지 작업을 수행한다.

하지만, 이 메소드에도 문제가 있다. 다음단계가 무엇인가 하는 로직과 그때 사용자 오브젝트의 level필드를 변경해주는 로직이 함께 있다. 또한, 레벨이 늘어나면 if문이 점점 길어질 것이다.

→ 다음 레벨이 무엇인지는 Level에 맡기자

public enum Level (
	GOLD(3, null), SILVER(2, GOLD), BASIC(1, SILVER);
	private final int value;
	private final Level next;
	Level(int value , Level next) {
		this.value = value;
		this.next = next;
	}
	public int intValue() {
		return value;
	}
	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 enum에 다음 단계 레벨 정보를 담는 next라는 필드를 추가한다.

이렇게 하면 nextLevel 메소드를 통해 다음 단계 레벨 정보를 얻을 수 있다.

User의 내부 정보가 변경되는 것은 UserService보다는 User가 스스로 다루는 게 적절하므로 이 부분을 User로 옮겨보자.

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

이렇게 레벨 변경 로직을 User 클래스에 넣어서 사용할 경우, 최근에 레벨을 변경한 일자 등의 정보를 남겨두고 싶다면 lastUpgrade 필드를 추가하고 이를 upgradeLevel 메소드에 넣어주기만 하면 된다.

이를 통해서 upgradeLevel은 아래와 같이 간단해졌다.

private void upgradeLevel(User user) {
	user.upgradeLevel();
	userDao.update(user);
}

이렇게 개선한 코드들은 각 오브젝트와 메소드가 각각 자기 몫의 책임을 맡아 일을 하는 구조로 만들어졌다.

객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다.

오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리이기도 하다.

profile
DB를 사랑하는 백엔드 개발자입니다. 열심히 공부하고 열심히 기록합니다.

0개의 댓글