본문 바로가기
도서기록/토비의 스프링

5장 서비스 추상화 - 1

by 코엘리 2025. 4. 4.
반응형

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

아래와 같은 비즈니스 로직을 추가한다고 가정한다.

  1. 사용자의 레벨은 basic, silver, gold 세가지 중 하나이다.
  2. 사용자가 처음 가입하면 basic, 이후 활동에 따라 한단계씩 업그레이드 된다.
  3. 가입 후 50회 이상 로그인하면 basic 에서 silver 레벨이 된다.
  4. silver 레벨이면서 30번 이상 추천을 받으면 gold 레벨이 된다.
  5. 사용자 레벨의 변경 작업은 일정한 주기를 가지고 일괄적으로 진행된다. 변경 작업 전에는 조건을 추엊ㄱ하더라도 레벨의 변경이 일어나지 않는다.

5.1.1. 필드 추가

사용자의 레벨을 저장할 필드가 필요하다. 만약 int 값으로 레벨을 관리한다고 예를 들었을 경우 코드 관리는 깔끔할 수 있으나, 다른 종류 예를 들어 basic(1), silver(2), gold(3) 이외의 int 값을 넣게 되었을 때 컴파일러가 체크해 주지 못하다. 그렇기 때문에 자바 5 이상에서 제공하는 enum 을 이용하는게 안전하고 편리하다.

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

위와 같은 User 클래스의 대한 테이블 필드와 타입은 아래와 같다.

field type nullable
Level tinyint x
Login int x
Recommend int x

UserDaoJdbc 수정 코드

등록을 위한 INSERT 문장이 들어 있는 add() 메소드는 아래와 같이 변한다.

public class UserDaoJdbc implements UserDao {
    public void add(User user) {
    this.jdbcTemplate.update(
        "insert into user(id, name, password, level, login, recommend) " +
        "values(?,?,?,?,?,?)", user.getId(), user.getName(),
        user.getPassword(), user.getLevel().intValue(),
        user.getLogin(), user.getRecommend());
    }
}
  • Level Enum 은 오브젝트이므로 DB 에 저장될 수 있는 값이 아니다.
  • 따라서 DB 에 저장하기 위해서는 intValue() 메서드를 따로 선언하여 int 값을 리턴하는 메소드를 사용한다.
  • 반대로 조회 시에는 valueOf()메소드를 활용하여 int 타입의 값을 Level 타입의 enum Object 로 리턴하는 메소드를 사용한다.

5.1.2 사용자 수정 기능 추가

수정할 정보가 담긴 User 오브젝트를 전달하면 id 를 참고해서 사용자를 찾아 필드 정보를 UPDATE 문을 이용해 모두 변경해주는 메소드를 하나 만들자.

UserDao와 UserDaoJdbc 수정

public interface UserDao {
    ...
    public void update(User user1);
}
  • UserDao 인터페이스에 update() 메소드를 추가하고 구현한다.
  • 테스트 할 때 수정하지 않아야 할 로우의 내용이 그대로 남아있는지 확인하기 위하여, 두 개의 User 인스턴스를 생성하여 하나의 인스턴스만 변경 후 테스트하는 방법이 있다.

5.1.3 UserService.upgradeLevels()

UserService 클래스와 빈 등록

public class UserService {
    UserDao userDao;

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

UserDao 오브젝트가 DI 가 가능하도록 수정자 메소드도 추가하였다.
그리고 스프링 설정파일에 userService 아이디로 빈을 추가한다.

<bean id="userService" class="springbook.user.service.UserService">
      <property name="userDao" ref="userDao" />
</bean>

<bean id="userDao" class="springbook.dao.UserDaoJdbc">
  <property name="dataSource" ref="dataSource" />
</bean>

upgradeLevels() 메소드

  1. 모든 사용자 정보를 DAO 에서 가져온 후에 한 명씩 레벨 변경 작업을 수행한다.
  2. 현재 사용자의 레벨이 변경되었는지 확인할 수 있는 플래그(changed)를 하나 선언한다.
  3. changed 플래그를 확인해서 레벨 변경이 있는 경우에만 UserDao의 update()를 이용해 수정 내용을 DB 에 반영한다.

5.1.4 UserService.add()

UserDao의 add() 메소드는 사용자 정보를 담은 User 오브젝트를 받아서 DB에 넣어주는 데 충실한 역할을 한다면, UserService에도 add()를 만들어두고 사용자가 등록될 때 적용할 비즈니스 로직을 담당하게 한다.

public void add(User user) {
    if (user.getLevel() == null) user.setLevel(Level.BASIC);
    userDao.add(user);
}

5.1.5 코드 개선

코드 개선에는 다음과 같은 질문을 해보자.

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

upgradeLevels() 리팩토링

public void upgradeLevels() {
    List<User> users = userDao.getAll();
    for (User user: users) {
        if (canUpgradeLevel(user)) {
            upgradeLevel(user);
        }
    }
}

업그레이드가 가능한지 확인하는 방법은 User 오브젝트에서 레벨을 가져와서, switch 문으로 구분한다.
업그레이드용 메소드를 따로 분리해두면, 업그레이드 안내 메일을 보낸다거나 로그를 남기는 등 업그레이드 작업에 필요한 작업이 추가되었을 경우 어느 곳을 수정해야 할지 명확해진다.

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 에 맡길 수 있다.

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;
    }
}

User의 레벨 업그레이드 작업용 메소드는 아래와 같이 추가된다.

private void upgradeLevel() {
    Level nextLevel = this.level.nextLevel();
    if (nextLevel == null) {
        throw new ..
    } else {
        this.level = nextLevel;
    }
}

그럼 아래와 같이 변경한다.

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

UserServiceTest 개선

  • 테스트 코드는 테스트 로직이 분명하게 들어나야 한다.
  • 중복 숫자 값들에 대해서는 정수형 상수로 관리하는 것이 좋다.

5.2 트랜잭션 서비스 추상화

트랜잭션 실행 중 장애 발생 시 변경된 사용자 레벨은 어떻게 해야할까?

5.2.1 모 아니면 도

  • 트랜잭션은 더 이상 나눌 수 없는 단위 작업을 말한다.
  • 작업을 쪼개서 작은 단위로 만들 수 없다는 것은 트랜잭션의 핵심 속성인 원자성을 으미ㅣ한다.
  • 모든 사용자에 대한 레벨 업그레이드 작업은 새로 추가된 기술 요구사항대로 전체가 다 성공하든지 아니면 전체가 다 실패하던지 해야 한다.

5.2.2 트랜잭션 경계설정

  • DB 는 그 자체로 완벽한 트랜잭션을 지원한다.
  • SQL을 이용해 다중 로우의 수정이나 삭제를 요청했을 때 일부만 삭제되고 나머지는 안되는 등과 같은 경우는 없다.
  • 하나의 SQL 명령을 처리하는 경우 DB 는 트랜잭션을 보장한다고 믿을 수 있다.

JDBC 트랜잭션의 트랜잭션 경계설정

  • JDBC의 트랜잭션은 하나의 Connection 을 가져와 사용하다가 닫는 사이에 일어난다. 트랜잭션의 시작과 종료는 Connection 오브젝트를 통해 이루어지기 때문이다.
  • 트랜잭션이 한 번 시작되면 commit() 또는 rollback() 메소드가 호출될 때까지 하나의 작업이 하나의 트랜잭션으로 묶인다.
  • 이렇게 setAutoCommit(false)로 트랜잭션의 시작을 선언하고 commit() 또는 rollback() 으로 트랜잭션을 종료하는 작업을 트랜잭션 경계설정이라고 한다.

UserService와 UserDao 의 트랜잭션 문제

  • 세 번에 걸쳐 update()를 호출하고, 두 번째 update() 에서 오류가 난다고 할 경우에 첫 번째 트랜잭션이 그대로 DB 에 남는다.

같은 트랜잭션에 묶이기 위해 Connection 을 공유하도록 UserService 를 만들게 되면 Connection을 UserDao에 파라미터로 넘겨줘야만한다.

UserService 트랜잭션 경계설정의 문제점

위처럼 파라미터로 Connection 을 넘기게 하면 트랜잭션 문제를 해결할 순 있지만 다른 문제가 발생한다.

  1. DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 더 이상활용할 수 없다. try/catch/fainlly 블록이 UserService에 존재하게 되고, JDBC 작업 코드의 전형적인 문제점을 그대로 갖게 된다.
  2. DAO의 메소드와 비즈니스 로직을 담고 있는 UserService의 메소드에 Connection 파라미터가 추가가 되어야 한다는 점이다.
    • upgradeLevels() 에서 사용하는 메소드의 어딘가에서 DAO를 필요로 한다면 Connection 오브젝트는 계속해서 전달되어야 한다.
  3. Connection 파라미터가 UserDao 인터페이스 메소드에 추가되면 UserDao 는 더 이상 데이터 액세스 기술에 독립적일 수가 없다.
반응형

'도서기록 > 토비의 스프링' 카테고리의 다른 글

3장 템플릿  (0) 2025.03.22
1장 오브젝트와 의존관계  (0) 2025.03.08