본문 바로가기

토비의 스프링 정리

토비의 스프링 - 6.1 트랜젝션 코드의 분리

6.1.1 AOP(Aspect Oriented Programming)

  • AOP는 IoC/DI, 서비스 추상화와 더불어 스프링의 3대 기반기술
  • AOP는 스프링의 기술 중에서 가장 난해한 용어와 개념을 가진 기술임
  • AOP는 주로 선언적 트랜잭션 기능에 많이 사용됨
  • 서비스 추상화를 통해 근본적 문제를 해결 했지만, AOP로 더욱 세련되고 깔끔한 방식으로 바꿀 수 있음
  • AOP 등장 배경, 스프링이 AOP를 도입한 이유, AOP 적용의 장점 등을 공부할 것임

6.1.2 메소드 분리

  • 서비스 추상화 기법을 통해 트랜잭션의 근본적 문제를 해결했지만 UserService에는 트랜잭션 로직비즈니스 로직이 같이 존재함
  • 스프링이 제공하는 트랜잭션 인터페이스 PlatformTransactionManager를 사용했지만, 메소드 내부에 트랜잭션 로직이 상당한 부분을 차지함
  • 비즈니스 로직 전후에 트랜잭션 경계설정이 되어야 하므로 어쩔수 없다고 느껴짐
  • 트랜잭션 로직과 비즈니스로직 사이에 주고받는 정보가 없기 때문에 메소드로 분리할 수 있음

public class UserService {
    ...
    public void upgradeLevels() {
        TransactionStatus status =
                this.transactionManager.getTransaction(
                        new DefaultTransactionDefinition());
        try{
            upgradeLevelsInternal(); //메소드 추출
            this.transactionManager.commit(status); //커밋
        }catch (Exception e){
            this.transactionManager.rollback(status); //롤백
            throw e;
        }
    }

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

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

  • 메소드로 분리는 했지만, 여전히 트랜잭션 로직이 UserService 내부에 있음
  • 트랜잭션 코드를 클래스 밖으로 추출하면 됨
  • UserService는 클래스 이므로, 다른 코드에서 UserService의 오브젝트를 직접적으로 사용할 수 밖에 없음
  • 직접 사용하는게 문제가 되므로 인터페이스를 통해 간접적으로 접근할 수 있게 함
  • 보통 인터페이스를 통해 DI하는 이유는, 운영과 테스트 상에서 손쉽게 구현 클래스를 바꿔서 사용하기 위함임
  • 하지만 꼭 그래야 하는 제약은 없음
  • 두 개의 UserService 인터페이스 구현 클래스를 동시에 이용하면. 하나의 구현에는 순수한 비즈니스 로직만을 남기고, 다른 하나는 트랜잭션 로직의 책임만 가질 수 있음
  • UserServiceTx는 비즈니스 로직이 없기 때문에 UserServiceImpl에 해당 부분을 위임

6.1.4 UserService 인터페이스 도입

  • UserService를 인터페이스로 변경하고, UserServiceImpl에 비즈니스 로직만 구현함
  • 트랜잭션 코드를 UserServiceTx에서 구현하고 비즈니스는 위임함
  • 먼저 UserServiceTx를 사용하여 트랜잭션 작업을 하고, 비즈니스 로직은 호출(위임)되어서 작업됨

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

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

public class UserServiceTx implements UserService {

    UserService userService;
    PlatformTransactionManager transactionManager;

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

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

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

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

            //위임
            this.userService.upgradeLevels();

            this.transactionManager.commit(status); //커밋
        } catch (Exception e) {
            this.transactionManager.rollback(status); //롤백
            throw e;
        }
    }
}
<beans
    .../>
    ...
    <bean id="userService" class="com.ksb.spring.UserServiceTx">
        <property name="transactionManager" ref="transactionManager"/>
        <property name="userService" ref="userServiceImpl"/>
    </bean>

    <bean id="userServiceImpl" class="com.ksb.spring.UserServiceImpl">
        <property name="userDao" ref="userDao"/>
        <property name="transactionManager" ref="transactionManager" />
        <property name="mailSender" ref="mailSender"/>
    </bean>
    ...
</beans>

6.1.5 테스트 코드 수정

  • 단순히 UserService 기능을 테스트 할 때 구체적 클래스 정보를 노출하는것이 좋지는 않지만, 목 오브젝트를 통해 수동 DI를 적용하는 테스트라면 어떤 클래스인지 알아야 함
  • 💡 테스트의 upgradeLevels() 메소드에서 수동 DI를 하기 때문에 테스트에 userServiceImpl 빈을 불어와야 함
  • upgradeAllOrNothing()는 트랜잭션 동작 테스트이기 때문에, UserServiceTx를 수동 DI시켜 동작하게 함
public static class TestUserService extends UserServiceImpl {
    ...
}

public class UserServiceTest {
    ...
    //수동 DI 때문에 필요함
    @Autowired
    UserServiceImpl userServiceImpl;
    
    @Test
    public void upgradeLevels() {
        ...
//        MockMailSender mockMailSender = new MockMailSender();
        userServiceImpl.setMailSender(mockMailSender);
    }

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

        UserServiceTx txUserService = new UserServiceTx();
        txUserService.setTransactionManager(transactionManager);
        txUserService.setUserService(testUserService);

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

        try {
            testUserService.upgradeLevels();
            fail("TestUserServiceException expected");
        } catch (UserServiceImpl.TestUserServiceException e) {
        }

        checkLevelUpgraded(users.get(1), false);
    }
}

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

  • 코드를 분리 함으로써 UserServiceImpl는 트랜잭션과 같은 기술적 내용에 전혀 신경을 쓰지 않아도 됨
  • 즉, UserServiceImpl는 비즈니스 로직만 구현
  • 트랜잭션은 DI를 통해 UserServiceTx를 먼저 실행되도록 하고, 호출(위임)을 통해 비즈니스 로직이 동작하게 함