ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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의 수를 리턴
          • 사용자를 두 명 등록하고 하나만 수정한 뒤에, 두 사용자 모두 정보를 확인
        • 두 번째 방법을 통해 보완하는 방법이 한눈에 파악하기 쉽다.
    • UserService.upgradeLevels()

      • 사용자 관리 로직을 두기 위해 UserService 생성

        • userDao 빈을 DI 받도록 설정
          • DI 받기 위해서는 UserService도 빈으로 등록되어야함
      • 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에서 사용하다가, 이벤트 기간동안 이벤트 업그레이드 정책을 주입하면서 사용하면 편리
    반응형

    댓글

Designed by Tistory.