Java & Spring/토비의 스프링 3.1

5장) 5.1 사용자 레벨 관리 기능 추가

Zin0_0 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에서 사용하다가, 이벤트 기간동안 이벤트 업그레이드 정책을 주입하면서 사용하면 편리
반응형