토비의 스프링 5장
사용자 레벨 관리 기능 추가
- 사용용자자의 레벨은 BASIC, SILVER, GOLD 세 가지 중 하나
- 처음 가입 시 BASIC, 이후 활동에 따라 한 단계씩 업그레이드
- 가입 후 50회 이상 로그인을 하면 BASIC에서 SILVER 레벨로 등업
- SILVER 레벨이면서 30번 이상 추천을 받으면 GOLD 레벨로 등업
- 사용자 레벨 변경 작업은 일정한 주기를 가지고 일괄적으로 진행
User Entity
public class User {
String id;
String name;
String password;
...
}
Level Enum
사용자 레벨을 저장할 필드의 DB 타입과 자바 클래스 타입
- 문자열 - 일정한 종류의 정보를 문자열로 넣는 것은 별로
- 숫자 - 각 레벨을 코드화하여 숫자로
- 범위가 작은 숫자로 관리하면 DB 용량도 적고 가볍다
- 자바의 User에 추가할 프로퍼티 타입으로 의미 없는 숫자를 프로퍼티에 사용하면 타입이 안전하지 않아 위험
- 상수 값으로 정해두고 사용? level 타입이 int이기 때문에 다른 종류의 정보(엉뚱한 레벨로 바뀌는 버그가 생김)를 넣거나 범위를 벗어나는 값을 넣을 위험이 있다.
public enum Level {
BASIC(1),
SILVER(2),
GOLD(3);
private final int value;
Level(int value) {
this.value = value;
}
public int intValue() {
return value;
}
public static Level valueOf(int value) {
return Arrays.stream(values()).filter(key -> key.intValue() == value).findFirst()
.orElseThrow(() -> new AssertionError("Unknown value: " + value));
}
}
- 겉으로는 Level 타입의 오브젝트이기 때문에 안전하게 사용 가능
- setLevel(1000) 같은 코드는 타입 에러 발생
UserService
사용자 별로 레벨 업그레이드 작업을 진행할 때 UserDao의 update()를 호출해 DB에 결과를 넣는다.
사용자 관리 로직은 DAO에 넣기 적합하지 않기 때문에 사용자 관리 비즈니스 로직을 담당하는 UserService 클래스를 추가하여 인터페이스 userDao 빈을 DI 받아 사용한다.
UserService.add()
처음 가입하는 사용자는 기본적으로 BASIC 레벨이어야 한다. → 이 로직은 어디에 담는 것이 좋을까?
레벨이 비어있는 경우 BASIC으로 설정, 레벨이 미리 설정된 경우 해당 레벨 사용
UserService.upgradeLevel()
레벨 조건이 만족하는 경우 등업을 해주는 기능. → 한번에 한 단계씩 레벨을 업그레이드 할 수 있다.
GOLD 레벨은 더이상 업그레이드가 불가능하다
public void upgradeLevels() {
List<User> users = userDao.getAll();
for (User user : users) {
if(canChangeLevel(user)) {
upgradeLevel(user);
}
}
}
private boolean canChangeLevel(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) {
user.upgradeLevel();
userDao.update(user);
}
- 처음 구현에서 for문 안에 if/elseif/else 구문을 넣어 구현 했는데, 가독성과 유지보수성이 떨어져 추상화를 거쳐 객체지향적인 코드로 수정
- 레벨 업그레이드가 가능한지 여부를 확인한 후 등업을 진행
User.upgradeLevel()
User 오브젝트에게 알아서 업그레이드에 필요한 작업 수행을 요청
public void upgradeLevel() {
Level nextLevel = this.level.nextLevel();
if(nextLevel == null) {
throw new IllegalArgumentException(this.level + "은 업그레이드가 불가능합니다");
}
this.level = nextLevel;
}
- User의 내부 정보가 변경되는 것은 User가 스스로 다루는 게 적절하다.
- 스스로 예외 상황에 대한 검증 기능을 가지고 있는 편이 안전하다. → 재사용성 증가
- nextLevel에 대한 정보는 Level enum에서 가지고 있도록 한다.
Level.nextLevel()
레벨의 업그레이드 순서는 Level enum 안에서 관리하여, 다음 레벨이 무엇인지를 일일이 if 조건식으로 비즈니스 로직에 담아둘 필요가 없다.
public enum Level {
GOLD(3, null),
SILVER(2, GOLD),
BASIC(1, SILVER);
private final int value;
private final Level next;
...
public Level nextLevel() {
return this.next;
}
}
- enum 값 안에 enum을 사용하려면 먼저 선언된 enum만 사용할 수 있다. 뒤에 선언된 enum 값을 먼저 사용하게 되면 컴파일 에러가 발생한다.
정리
- 각 오브젝트와 메서드가 각각 자기 몫의 책임을 맡아 일을 하는 구조가 되었다.
- UserService, User, Level이 내부 정보를 다루는 자신의 책임에 충실하면서 필요가 생기면 서로 요청하는 구조로 파악과 변경이 쉽다.
- 잘못된 요청이나 작업을 시도했을 때 각자 확인하고 예외를 던져줄 수 있다.
- 객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업을 하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다.
- UserService.upgradeLevel()은 User에게 “레벨 업그레이드”를 요청하고 User는 Level에게 “다음 레벨”을 요청한다.
사용자 업그레이드 정책
레벨 업그레이드 조건을 상수화 하여 사용할 수 있다. 변경이 있을 때 여러 군데를 고치게 된다면 중복이다.
이벤트로 레벨 업그레이드 정책을 다르게 적용해야하는 경우가 생길 때, UserService 코드를 직접 수정하는 건 번거롭고 위험하다. 이런 경우 사용자 업그레이드 정책을 UserService로 분리하는 방법을 고려할 수 있다. 정책을 담을 오브젝트를 DI를 통해 UserService에 주입하고, 기본 정책/이벤트 정책 등 필요할 때 새로운 업그레이드 정책을 구현한 클래스로 변경하면 된다.
UserLevelUpgradePolicy
public interface UserLevelUpgradePolicy {
boolean canUpgradeLevel(User user);
void upgradeLevel(User user);
}
'개발 > Spring' 카테고리의 다른 글
| 스프링 내장 캐시 사용하기 (2) | 2025.02.07 |
|---|---|
| Spring 서버 로깅 1 - 로그 레벨 (0) | 2024.11.04 |
| [Querydsl] PageableExecutionUtils count query 오류 (0) | 2024.10.20 |
| [SpringBoot] 이메일 전송 2탄 (네이버메일) (0) | 2024.06.05 |
| Swagger 적용(2.3.0) (0) | 2024.04.17 |