-
5장) 5.1 사용자 레벨 관리 기능 추가Java & Spring/토비의 스프링 3.1 2021. 6. 26. 18:11반응형
5장 서비스 추상화
- 지금까지 만든 DAO에 트랜잭션을 적용하면서 스프링이 어떻게 성격이 비슷한 여러 종류의 기술을 추상화하고 일관된 방법으로 사용하도록 지원하는지 살펴보기
5.1 사용자 레벨 관리 기능 추가
지금까지 만든 UserDao는 비즈니스 로직을 가지고 있지 않다.
사용자 관리 기능을 넣어 활동 내역을 참고한 레벨 조정 기능을 추가
- 사용자 레벨 BASIC, SILVER, GOLD
- 처음 가입 시, BASIC 등급, 활동에 따라 한 단계씩 업그레이드
- 가입 후 50회 이상 로그인 ~> SILVER
- SILVER 레벨이면서 30번 이상 추천 ~> GOLD
- 사용자 레벨은 일정한 주기를 가지고 일괄 진행
- 변경 작업 전에는 조건을 충족해도 변경이 일어나지 않음
필드 추가
LEVEL 이늄
3 단계기 때문에 DB에 tinyInt로 간단하게 값을 넣어줌
level 타입이 int면 다른 종류의 정보를 넣는 실수를 해도 컴파일러가 체크해주지 못함
숫자 타입을 직접 사용하기보다 자바5 이상에서 지원하는 ENUM을 이용해서 안전하고 편리하게 오브젝트로 관리
public enum Level { BASIC(1), SILVER(2), GOLD(3); private final int value; Level(int value) { this.value = value; } // set & get method }
User 클래스에 Level 타입의 변수와 int형 변수인 로그인 횟수와 추천 수를 추가해서 사용자 관리 기능을 대비
UserDaoTest 수정
- 새로운 기능을 추가하면서 우선, 테스트 픽스처로 만든 user 객체들에 새로 추가된 세 필드의 값을 추가
- 생성자 파라메터에도 새로운 세 필드 추가
- 오브젝트 필드 값이 모두 같은지 비교하는 checkSameUser() 메소드에 새로운 필드를 비교하는 코드를 추가
UserDaoJdbc 수정
- add() 메소드의 SQL과 userMapper에 추가된 필드를 적용
- Level
ENUM
은 오브젝트이므로 DB에 저장될 수 있는 SQL 값이 아님- 따라서, 미리 만들어둔 get 메소드를 통해 정수형 값으로 변환
- 조회하는 경우에는 DB의 정수형 값으로 부터 미리 만들어둔 get 메소드를 통해 Level ENUM 오브젝트로 받아서, set 메소드에 넣어줌
사용자 수정 기능 추가
수정 기능 테스트
- 픽스처 오브젝트를 하나 등록하고, 해당 오브젝트의 id를 제외한 필드 내용을 바꾼 후 update를 호출
- id로 조회한 값과 수정한 픽스처 오브젝트를 비교
UserDao와 UserDaoJdbc 수정
update 메소드 추가
public void update(User user) { this.jdbcTemplate.update("update users set name = ?, password = ?, level = ?, login = ?, recommand = ? where id = ?", user.getName(), user.getPassword(), user.getLevel().intValue(), user.getLogin(), user.getRecommend(), user.getId()); }
수정 테스트 보완
- 가장 많은 실수가 일어나는 곳이 SQL 문장이기 때문에 위의 수정 기능 테스트만으로는 Query문이 잘못 되었는지 파악하기 어려운 에러 존재
- update 문장에서 where 절을 빠뜨렸어도, 이 테스트는 통과
- 보완하는 두 방법
- update()가 돌려주는 리턴 값 확인
- 영향 받은 row의 수를 리턴
- 사용자를 두 명 등록하고 하나만 수정한 뒤에, 두 사용자 모두 정보를 확인
- update()가 돌려주는 리턴 값 확인
- 두 번째 방법을 통해 보완하는 방법이 한눈에 파악하기 쉽다.
- 가장 많은 실수가 일어나는 곳이 SQL 문장이기 때문에 위의 수정 기능 테스트만으로는 Query문이 잘못 되었는지 파악하기 어려운 에러 존재
UserService.upgradeLevels()
사용자 관리 로직을 두기 위해 UserService 생성
- userDao 빈을 DI 받도록 설정
- DI 받기 위해서는 UserService도 빈으로 등록되어야함
- userDao 빈을 DI 받도록 설정
upgradeLevels() 메소드
public void upgradeLevels() { userDao.getAll().forEach(user -> { Boolean changed = false; if (user.getLevel() == Level.BASIC && user.getLogin() >= 50) { user.setLevel(Level.SILVER); changed = true; } else if (user.getLevel() == Level.SILVER && user.getRecommand >= 30) { user.setLevel(Level.GOLD); changed = true; } if (changed) { userDao.update(user); } }); }
upgradeLevels() 테스트
- 사용자 레벨 세 가지, GOLD를 제외한 두 레벨은 업그레이드가 되는 경우와 아닌 경우를 살펴보면, 최소 다섯 경우를 테스트하는 코드를 작성
UserService.add()
사용자 관리 비즈니스 로직에서 처음 가입하는 사용자가 BASIC으로 설정돼야 함
생성 시, 레벨이 정해진 경우와 비어있는 경우를 고려
public void add(User user) { if (user.getLevel() == null) { user.setLevel(Level.BASIC); } userDao.add(user); }
레벨이 비어있는 경우와 정해진 경우에 대한 케이스를 만들어 테스트를 진행
코드 개선
간단한 비즈니스 로직을 테스트하는데 DAO와 DB까지 모두 동원되는 것이 부적절
코드 검토 사항
- 중복 부분이 있는가?
- 이해하기 불편하지 않은가?
- 자신의 역할에 맞게 짜여있는가?
- 변경이 일어나면 어떤 사항들이 있고, 쉽게 대응할 수 있는가?
upgradeLevels() 메소드의 문제점
- for 루프 속에 if/elseif 블록이 가독성이 좋지 않음
- 성격이 다른 로직이 한데 섞여있음
- 레벨 파악 로직, 업그레이드 조건 로직, 다음 단계 파악 로직, 업그레이드 작업 로직, 임시 플래그 등
- 레벨을 확인하고 각 레벨별로 다시 조건을 판단하게 수정
upgradeLevels() 리팩토링
public void upgradeLevels() { userDao.getAll().filter(user -> canUpgradeLevel(user)) .forEach(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 메소드가 복잡함
- 다음 단계가 무엇인지 로직과 level 필드를 변경해주는 로직이 함께 있고 노골적으로 드러남
- 예외 상황에 대한 처리가 없음
- 레벨이 늘어나면 if 블록이 늘어나고, Level 이외의 필드를 update하면 조건이 길어짐
upgradeLevel() 분리
레벨의 순서와 다음 단계 레벨이 무엇인지 결정하는 일은 Level에게 위임
public enum Level { GOLD(3, null), SILVER(2, GOLD), BASIC(1, SILVER); private final int value; private final int next; Level(int value, Level next) { this.value = value; this.next = next; } // get set 메소드 }
User 내부 정보가 변경되는 것은 UserService보다 User 스스로 다루는게 적절
// User Class public void upgradeLevel() { Level nextLevel = this.level.nextLevel(); if (nextLevel == null) { throw new IllegalStateException(this.level + " 업그레이드 불가"); } this.level = nextLevel; }
업그레이드 및 업그레이드가 불가한 상황에 대한 예외처리
// User Service private void upgradeLevel(User user) { user.upgradeLevel(); userDao.update(user); }
- if 문장이 들어있던 코드보다 간결하고 작업 내용이 명확해짐
- 책임 분리가 깔끔하게 변경됨
- 변경이 필요할 때 어디를 수정할지 명확해짐
User 테스트
- 간단한 로직 메소드 테스트
- 새로운 기능과 로직 추가 가능성을 고려해서 테스트를 작성하는 것이 좋음
- upgradeLevel() 메소드 테스트 작성
- Level 이늄에 정의된 모든 레벨을 가져와서 User에 설정해두고, upgradeLevel()을 실행해서 다음 레벨로 바뀌는지 테스트
- 다음 단계가 null인 경우는 제외(GOLD인 경우)
- 간단한 로직 메소드 테스트
UserServiceTest 개선
기존 테스트에서는 checkLevel() 메소드를 호출할 때 일일이 다음 단계를 넣어줬지만, 이는 중복 코드
boolean타입의 upgraded flag를 파라메터로 전달받고, true인 경우 업그레이드 일어났는지 확인하고, false인 경우 업그레이드가 안일어났는지 확인하는 메소드를 생성해서 테스트
중복되는 숫자(상수)의 중복 값을 상수로 변경
public static final int MIN_LOGCOUNT_FOR_SILVER = 50; public static final int MIN_RECCOMEND_FOR_GOLD = 30;
테스트코드 뿐만 아니라 애플리케이션 코드 모두 위의 상수로 변경
기준이 되는 값 등 애플리케이션에서 중요한 특정 값은 상수로 빼서 관리하는 것이 더욱 직관적이고, 수정에 용이
정리
- 테스트 코드의 중요성
- 빠르게 실행 가능한 포괄적인 테스트를 만들어두면 기능의 추가 & 수정이 일어날 때, 빠르고 편리하고 비교적 안정적으로 작업할 수 있음
- 유연한 정책 변경(DI 활용)
- 위의 예시에서 이벤트 기간에 레벨 업그레이드 정책 변경을 유연하게 바꿔보기
- 사용자 업그레이드 정책을 UserService에서 분리
- 분리된 업그레이드 정책을 담은 오브젝트는 DI를 통해 UserService에 주입
- 평상시 정책을 UserService에서 사용하다가, 이벤트 기간동안 이벤트 업그레이드 정책을 주입하면서 사용하면 편리
- 위의 예시에서 이벤트 기간에 레벨 업그레이드 정책 변경을 유연하게 바꿔보기
반응형'Java & Spring > 토비의 스프링 3.1' 카테고리의 다른 글
5장) 5.3 서비스 추상화와 단일 책임 원칙 ~ 5.5 정리 (0) 2021.07.02 5장) 5.2 트랜잭션 서비스 추상화 (0) 2021.06.26 4장) 4.2 예외 전환 ~ 4.3 정리 (0) 2021.06.21 4장) 4.1 예외 (0) 2021.06.17 3장) 3.6 스프링의 JdbcTemplate~ 3.7 정리 (0) 2021.06.17