본문 바로가기

Spring/토비의 스프링 3.1

6.1 트랜잭션 코드의 분리



트랜잭션 경계설정

  • 비즈니스 로직보다 코드가 더 길다.

6.1.1 메소드 분리

트랜잭션 적용 코드 구조

  • 트랜잭션 시작
  • 비즈니스 로직
  • 트랜잭션 종료

코드의 특징

  1. 두 가지 종류의 코드가 구분되어 있음
    • 트랜잭션 & 비즈니스 로직
  2. 두 코드 간 주고받는 정보가 없음
    • 완벽히 독립적인 코드

비즈니스 로직을 메소드로 추출하여 독립

public class UserService {
        public void upgradeLvls() throws Exception {
        TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            upgradeLvlsInternal();
            this.transactionManager.commit(status);
        } catch (Exception e) {
            this.transactionManager.rollback(status);
            throw e;
        }
    }

    // 분리된 비즈니스 로직 코드
    // 트랜잭션 적용 전과 동일
    private void upgradeLvlsInternal() {
        List<User> users = userDao.getAll();
        for (User user : users) {
            if (canUpgradeLvl(user)) {
                upgradeLvl(user);
            }
        }
    }
}

보기에 깔끔해지고, 비즈니스 로직이 독립된 메소드로 분리되어 이해하기도 편하며, 수정하기에도 부담이 없다.

6.1.2 DI를 이용한 클래스의 분리

여전한 문제점

  • 트랜잭션을 담당하는 기술적인 코드가 UserService내에 위치하고 있다.
  • 트랜잭션 코드를 클래스 밖으로 뽑아내자

DI 적용을 이용한 트랜잭션 분리

UserService는 클래스이고, 클라이언트는 UserService를 직접 참조하게 된다.

  • 직접 참조를 할 경우, 트랜잭션 코드가 UserService 밖으로 빠져버리면 해당 기능이 빠진 UserService를 사용할 수 밖에 없다.
  • 간접적으로 참조하도록 변경하자.

UserService를 인터페이스로 만들고, 기존 코드는 UserService를 구현한 클래스에 넣으면 된다.

  • 한 번에 두 개의 UserService 구현 클래스를 사용해야 한다.
  • UserServiceImpl(사용자관리) + UserServiceTx(트랜잭션)

UserService 인터페이스 도입

UserService를 인터페이스로 변환 후 UserServiceImpl과 UserServiceTx로 구현

public interface UserService {
    void add(User user);
    void upgradeLvls();
}

UserServiceImpl

  1. UserService의 내용을 대부분 유지
  2. 트랜잭션 관련 코드는 모두 제거
    • PlatformTransactionManager 인스턴스 변수 및 수정자 메소드 제거
    • upgradeLvls() 내 트랜잭션 코드 제거
  3. upgradeLvlsInternal() 코드를 다시 upgradeLvls()로 돌려놓음
public class UserServiceImpl implements UserService {
    private UserDao userDao;
    private MailSender mailSender;

    @Override
    public void upgradeLvls() {
        List<User> users = userDao.getAll();
        for (User user : users) {
            if (canUpgradeLvl(user)) {
                upgradeLvl(user);
            }
        }
    }
}

트랜잭션을 고려하지 않았던 처음 코드로 되돌아옴

  • 비즈니스 로직에만 충실한 코드

분리된 트랜잭션 기능

UserServiceTx

  1. UserService를 구현
  2. 같은 인터페이스를 구현한 다른 오브젝트에게 작업을 위임
public class UserServiceTx implements UserService {
    UserService userService;

    // UserService를 구현한 다른 오브젝트를 DI 받음
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    // DI 받은 UserService 오브젝트에 모든 기능을 위임

    @Override
    public void add(User user) {
        this.userService.add(user);
    }

    @Override
    public void upgradeLvls() {
        this.userService.upgradeLvls();
    }
}

UserService 인터페이스 구현

  • UserService 타입 오브젝트의 하나로 행세 가능
  • 사용자 관리 비즈니스 로직은 전혀 갖지 않고, 다른 오브젝트에 기능을 위임

트랜잭션 경계 설정 부가작업 부여

public class UserServiceTx implements UserService {
    UserService userService;
    PlatformTransactionManager transactionManager;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    @Override
    public void add(User user) {
        this.userService.add(user);
    }

    @Override
    public void upgradeLvls() {
        TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            this.userService.upgradeLvls();
            this.transactionManager.commit(status);
        } catch (RuntimeException e) {
            this.transactionManager.rollback(status);
            throw e;
        }
    }
}

트랜잭션 적용을 위한 DI 설정

빈 오브젝트와 의존관계

  • Client(UserServiceTest) -> UserServiceTx -> UserServiceImpl
<bean id="userService" class="com.david.tobysspring.user.service.UserServiceTx">
    <property name="transactionManager" ref="transactionManager" />
    <property name="userService" ref="userServiceImpl" />
</bean>

<bean id="userServiceImpl" class="com.david.tobysspring.user.service.UserServiceImpl">
    <property name="userDao" ref="userDao" />
    <property name="mailSender" ref="mailSender" />
</bean>

클라이언트는 UserServiceTx 빈을 호출해서 사용해야 함

  • userService 빈 아이디는 UserServiceTx 클래스로 정의된 빈에게 부여
  • userService 빈은 UserServiceImpl 클래스로 정의되는 userServiceImpl 빈을 DI

트랜잭션 분리에 따른 테스트 수정

Autowired는 타입이 일치하는 빈을 찾아 줌

  • UserService 타입의 빈이 2개 존재
  • 타입으로 찾을 수 없는 경우 필드의 이름으로 빈을 찾아 줌
@Autowired UserService userService;

userService 라는 id를 가진 UserServiceTx 빈이 주입 됨

  • UserServiceTest는 UserServiceImpl 빈도 가져와야 함
  • 목 오브젝트를 이용한 테스트에서 직접 MailSender를 DI 해줘야하기 때문

목 오브젝트를 이용해 수동 DI를 적용하는 테스트에서는 어떤 클래스의 오브젝트인지 분명하게 알 필요가 있음

@Autowired UserServiceImpl userServiceImpl;

upgradeLvls() 메소드

  • MailSender의 목 오브젝트 설정은 UserService의 인터페이스를 통해서는 불가능
  • 별도로 가져온 userServieImpl 빈에 해주어야 함
public class UserServiceTest {
    public void upgradeLvls() throws Exception {
        // ...

        MockMailSender mockMailSender = new MockMailSender();
        userServiceImpl.setMailSender(mockMailSender);

        // ...
    }
}

upgradeAllOrNothing() 메소드

  • 트랜잭션이 적용됐는지 확인하는 일종의 학습 테스트
  • TestUserService는 트랜잭션 기능이 빠진 UserServiceImpl을 상속해야함
    1. 트랜잭션 롤백의 확인을 위한 예외 발생 코드가 UserServiceImpl에 있기 때문
    2. 그런데 트랜잭션 기능이 없어짐
  • TestUserService 오브젝트를 UserServiceTx 오브젝트에 수동 DI 시킨 후 UserServiceTx의 메소드를 호출해야함
public class UserServiceTest {
    // 클래스 선언 변경
    static class TestUserService extends UserServiceImpl {
        // ...
    }

    @Test
    public void upgradeAllOrNothing() throws Exception {
        TestUserService testUserService = new TestUserService(users.get(3).getId());
        testUserService.setUserDao(userDao);
        testUserService.setMailSender(mailSender);

        // 트랜잭션 기능을 분리한 UserServiceTx는 예외 발생용으로 수정할 필요가 없으니 그대로 사용한다.
        UserServiceTx txUserService = new UserServiceTx();
        txUserService.setTransactionManager(transactionManager);
        txUserService.setUserService(testUserService);

        userDao.deleteAll();
        for  (User user : users) {
            userDao.add(user);
        }

        try {
            // 트랜잭션 기능을 분리한 오브젝트를 통해 예외 발생용 TestUserService가 호출되게 해야한다.
            txUserService.upgradeLvls();
            fail("TestUserServiceException expected");
        } // ...
    }
}

트랜잭션 경계설정 코드 분리의 장점

큰 변화를 주었는데 코드 분리의 장점은 무엇인가

  1. 비즈니스 로직을 담당하는 UserServiceImpl은 기술적 내용에 전혀 신경을 쓰지 않아도 된다.
    • 따라서 언제든 트랜잭션을 도입할 수 있다.
  2. 비즈니스 로직에 대한 테스트를 손쉽게 만들어 낼 수 있다.(후술)

' Spring > 토비의 스프링 3.1' 카테고리의 다른 글

6.3 다이내믹 프록시와 팩토리 빈  (0) 2019.02.13
6.2 고립된 단위 테스트  (0) 2019.02.12
6장. AOP  (0) 2019.02.07
5.5 정리  (0) 2019.02.07
5.4 메일 서비스 추상화  (0) 2019.02.07