Spring/토비의 스프링

[토비의 스프링] 6.1 트랜잭션 코드의 분리

한뜽규 2025. 6. 14. 00:05
728x90

스프링의 3대 기반기술 중 하나며, 굉장히 어렵다고 한다.

예전에 스프링을 처음 배울 때는 진짜 아무생각없이 그냥 썼던 거 같다.

스프링을 배울수록 아... 이게 참 어려운 내용이었구나 싶었던 부분이고, 로그 관련해서 앞으로 사용할 계획이기에 이번 기회에 깊게 공부하도록 하자.

 

아마 가장 많이 사용할 AOP는 트랜잭션이다.

@Transactional은 기계적으로 굉장히 많이 사용했던 것 같다.

트랜잭션 자체가 어떻게 적용되는지는 저번 글에서 알 수 있었고, 이번에는 AOP 자체가 어떻게 적용되는지 알아보자.

 

메소드 분리

기존에 만들었던 upgradeLevels()를 보면 비즈니스 로직을 트랜잭션 설정들이 감싸고 있는 것을 볼 수 있다.

또한 비즈니스 로직은 트랜잭션에게 어떠한 값도 넘겨주지 않는다.

서로 꼭 필요하지만 굉장히 독립적이다.

 

그렇기에 분리가 가능해보인다.

일단 바로 분리해보자.

 

public void upgradeLevels() {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try
        {
            this.upgradeLevelsLogic();
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }

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

이렇게 트랜잭션 코드와 비즈니스 코드를 분리했다.

 

DI를 이용한 클래스의 분리

이제 UserService에서 트랜잭션 코드를 분리해보자.

이건 참 클래스의 의존들이 어려우니, 이해가 힘들면 계속 다시 보도록 하자.

 

현재

이렇게 UserService를 인터페이스로 만들고, UserServiceImpl로 구현해 사용하고 있다.

 

그리고 UserServiceTest에서 이거를 받아 사용하고 있던 중이다.

 

이 문제를 해결하기 위해서, 처음보는 방법이지만 한 번에 두 개의 UserService 인터페이스를 사용한다고 한다.

 

UserServiceImpl에 비즈니스 로직을 담고, UserServiceTx는 트랜잭션을 담당하도록 만들고 싶다.

그리고 이거를 다시 UserService가 사용하는..? 어려운 구조이다.

솔직히 이해는 힘들지만 직접 코드를 만들며 이해해보자.

 

일단 UserService 인터페이스이다.

public interface UserService {

    void upgradeLevels();

    void add(User user);
}

이렇게 add(), upgradeLevels() 2개의 메서드를 가지고 있었다.

 

그리고 UserServiceImpl이다.

@RequiredArgsConstructor
public class UserServiceImpl implements UserService{

    private final UserDao userDao;

    @Getter
    private final MailSender mailSender;

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

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

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

기존에 존재하던 트랜잭션 코드를 모두 제거했다.

트랜잭션 설정은 UserServiceTx에서 하기 때문에, 이제 관련 코드를 제거해도 괜찮다.

 

이렇게 만드니 막상 과거의 코드로 돌아간 것 같다.

이제 트랜잭션 설정을 해보자.

@RequiredArgsConstructor
public class UserServiceTx implements UserService {

    private final UserService userService;

    private final PlatformTransactionManager platformTransactionManager;

    @Override
    public void upgradeLevels() {
        TransactionStatus transactionStatus = this.platformTransactionManager.getTransaction(new DefaultTransactionDefinition());

        try
        {
            userService.upgradeLevels();
            platformTransactionManager.commit(transactionStatus);

            this.platformTransactionManager.commit(transactionStatus);
        }
        catch(Exception e)
        {
            this.platformTransactionManager.rollback(transactionStatus);
            throw e;
        }
    }

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

이렇게 UserService를 가져와서 트랜잭션 설정을 하며 UserService의 메서드를 호출하는 방식이다.

 

이제부터 클라이언트는 UserServiceTx를 사용해야 하기에, 기존의 UserService를 주입하는 부분에는 UserServiceTx를 주입해주고 UserServiceTx의 UserService에는 내부의 메서드를 사용할 수 있도록 UserServiceImpl을 주입해준다.

 

작성한 xml은 다음과 같다.

    <bean id="userDao" class="seungkyu.UserDaoImpl">
        <constructor-arg ref="dataSource" />
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="userService" class="seungkyu.UserServiceTx">
        <constructor-arg ref="transactionManager"/>
        <constructor-arg ref="userServiceImpl"/>
    </bean>

    <bean id="userServiceImpl" class="seungkyu.UserServiceImpl">
        <constructor-arg ref="userDao"/>
        <constructor-arg ref="mailSender"/>
    </bean>

    <bean id="mailSender" class="seungkyu.MailSenderImpl"/>

UserService에 UserServiceTx를 넣어놨기에 앞으로 클라이언트는 트랜잭션이 설정된 UserServiceTx를 호출해서 사용하게 되는 것이다.

 

테스트 코드도 그에 맞도록 수정을 하고 테스트를 진행하면 성공이 나온다.

 

 

이렇게 트랜잭션 코드와 비즈니스 로직을 DI를 통해 분리해보았다.

이 방법의 장점은 역시 관심사가 비즈니스 로직과 트랜잭션 적용으로 완벽하게 분리된다는 것이다.

 

앞으로 이런 부분을 DI가 아닌 AOP를 사용해서 분리할 수 있도록 해보자.