본문 바로가기
Spring&Spring Boot

(Spring 서비스 추상화) 서비스 추상화란? 사용자 레벨 관리 기능 예시

by Developer RyanKim 2020. 7. 23.

Portable Service Abstraction

 

서비스 추상화

사용방법과 형식은 다르지만 기능과 목적이 유사한 기술이 존재한다.

환경과 상황에 따라 기술이 바뀌고, 그에 따른 API를 사용하고 다르게 접근해야 한다는건 매우 피곤한 일이다.

 


사용자 레벨 관리 기능 추가 예제

 

요구사항

  • 사용자의 레벨 (숫자가 클수록 높은 레벨)
    - BASIC(1) : 처음 가입 레벨. 50회 이상 로그인시 업그레이드
    - SILVER(2) : 30번이상 추천받을시 업그레이드
    - GOLD(3) : 최고레벨
  • 사용자 레벨의 변경 작업은 일정 주기로 일괄 진행. 작업 전에는 기준을 충족하였더라도 레벨이 바뀌지 않음.

 

Level Enum 추가

public static final int BASIC=1 과같이 상수 값으로 정의하여 쓰지 않은 이유

  • 사용하지 않는 값(ex. 777)을 검증하지 못함
  • 다른 종류의 정보를 넣는 실수를 컴파일러가 체크하지 못함 (ex. user.setLevel(user.getAge()) )

 

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) {
        switch (value) {
            case 1: return BASIC;
            case 2: return SILVER;
            case 3: return GOLD;
            default: throw new AssertionError("Unknown value" + value);
        }
    }
}

 

User 클래스에 레벨, 로그인 횟수, 추천수 추가

public class User {
    ...
    Level level;
    int login;
    int recommend;
    
}

 

UserService 추가

DAO는 데이터를 어떻게 가져오고 조작할 것인지를 다루는 곳이다.

이곳에 비즈니스 로직을 담는 것은 적절하지 않다. 따라서 비즈니스 로직을 담을 클래스를 추가한다.

UserDao를 DI 받아 사용하도록 만든다.

public class UserService {
    UserDao userDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

 

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;
        } else if(user.getLevel() == Level.SILVER && user.getRecommend() >= 30) {
            // SILVER 레벨 업그레이드
            user.setLevel(Level.GOLD);
            changed = true;
        } else if(user.getLevel() == Level.GOLD) {
            // GOLD 레벨 업그레이드는 일어나지 않는다.
            changed = false;
        } else {
            changed = false;
        }
        if(changed) { userDao.update(user); }
    }
}

 

처음 가입하는 사용자는 BASIC 레벨이다. 어느 부분에 이 로직을 담을까?

UserDao의 add에 넣는 것은 바람직하지 않다.

User 클래스에서 초기화하도록 한다? 처음가입할 때를 제외하면 무의미한 정보이다.

UserService의 add 메소드를 추가하여 사용자가 등록될 때 적용될 만한 비즈니스 로직을 담으면 된다.

 

코드 개선

  • 코드에 중복된 부분은 없는가?
  • 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가?
  • 코드가 자신이 있어야 할 자리에 있는가?
  • 앞으로 변경이 일어난다면 어떤 것이 있을수 있고, 그 변화에 쉽게 대응할 수 있게 작성되어 있는가?

upgradeLevels() 메소드 코드의 문제점

  • if-else 블록들이 읽기 불편하다.
  • 레벨의 변화단계, 업그레이드 조건, 조건 충족시 작업이 섞여있어 로직이해가 어렵다.
  • 플래그를 두고 값을 바꾸면서 마지막에 이를 확인하여 업데이트 하는부분이 깔끔하지 않다.
  • 레벨이 추가된다면 if 조건식과 블록이 또 추가된다. 점점 복잡해진다.

upgradeLevels() 리팩토링

가장 먼저 추상적인 레벨에서 로직을 작성해보자.

레벨을 업그레이드하는 작업의 기본 흐름만 만든다.

public void upgradeLevels() {
    List<User> users = userDao.getAll();
    for (User user : users) {
        if (canUpgradeLevel(user)) {
            upgradeUser(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()
    }
}

 

Level Enum 에서 다음 레벨을 지정하고, 반환하도록 수정한다.

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 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에게 맡겨야 한다.

 

User 클래스에 레벨 업그레이드 기능을 추가한다.

public void upgradeLevel() {
    if (this.level.nextLevel() == null) {
        throw new IllegalStateException();
    }
    this.level = level.nextLevel();
}

User 내부 정보가 변경되는 것은 UserService 보다는 User 클래스에서 다루는 것이 적절하다.

User에 업그레이드 작업을 담당하는 독립적인 메소드를 두고 사용할 경우

업그레이드시 기타 정보도 변경이 필요해지면 그 장점이 무엇인지 알 수 있을것이다.
가장 최근에 레벨을 변경할 일자를 기록해야 한다면? 위 메소드에 추가하면 될 것이다.

 

UserService 에서 User , UserDAO 클래스를 사용하여 사용자 레벨 업그레이드를 수행시킨다.

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

객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신, 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다. 오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리이기도 하다.

  • UserService -> User : 레벨 업그레이드 요청
  • User -> Level : 다음 레벨 정보 요청
  • UserService -> UserDAO : 유저 업데이트 정보 저장 요청

결과

  • if 문장이 많던 이전 코드보다 간결하고 작업 내용이 명확히 드러남
  • 각 오브젝트가 해야할 책임 깔끔히 분리
  • UserService, User, Level이 내부정보를 다루는 자신의 책임에 충실한 기능을 가지고, 필요하면 다른 오브젝트에 요청
  • 변경이 필요한 경우 쉽게 변경 포인트를 찾을 수 있음
  • 테스트 코드도 단순해짐

추가

public interface UserLevelUpgradePolicy {
	boolean canUpgradeLevel(User user);
    void upgradeLevel(User user);
}

업그레이드 정책을 담을 인터페이스를 만들고, UserService에서 DI로 제공받아 사용하게 한다면
업그레이드 정책 변경을 유연하게 할 수 있을 것이다.

 

※ 토비의 스프링  5장 참고

댓글