Spring

외부 메서드, 내부 메서드에 대한 @Transactional 트랜잭션 적용 결과 테스트

1. 알고자 하는 것

- AOP 개념을 다시 한번 공부하며 프록시와 내부메서드 호출에 따른 트랜잭션 적용 결과를 테스트 해보고자 한다.

- 다음과 같은 메서드(외부)와 해당 메서드 내에서 호출하는 메서드(내부)에 각각 @Transactional 어노테이션을 적용하였을 때 각 메서드에 대한 트랜잭션 적용 결과를 확인한다.

1. outerMethod (@Transactional) & innerMethod (@Transactional)

2. outerMethod (@Transactional) & innerMethod

3. outerMethod  & innerMethod (@Transactional)

@Slf4j
@Service
public class SimpleService {

    public void outerMethod() {
        log.info("==== outerMethod start ====");
        log.info("==== outerMethod transaction Active : {}", 
                TransactionSynchronizationManager.isActualTransactionActive());
        innerMethod();
        log.info("==== outerMethod end ====");
    }

    public void innerMethod() {
        log.info("==== innerMethod start ====");
        log.info("==== innerMethod transaction Active : {}", 
                TransactionSynchronizationManager.isActualTransactionActive());
        log.info("==== innerMethod end ====");
    }
}

 

- 테스트코드는 다음과 같다.

- 단순히 outerMethod를 호출하고, 로그를 통해 각 메서드의 트랜잭션 적용 여부를 확인한다.

@SpringBootTest
class SimpleServiceTest {

    @Autowired
    private SimpleService simpleService;

    @Test
    void transaction_test() {
        simpleService.outerMethod();
    }
}

 

2. 알게된 것

outerMethod (@Transactional) & innerMethod (@Transactional)

@Slf4j
@Service
public class SimpleService {

    @Transactional
    public void outerMethod() {
        log.info("==== outerMethod start ====");
        log.info("==== outerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        innerMethod();
        log.info("==== outerMethod end ====");
    }

    @Transactional
    public void innerMethod() {
        log.info("==== innerMethod start ====");
        log.info("==== innerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        log.info("==== innerMethod end ====");
    }
}
==== outerMethod start ====
==== outerMethod transaction Active : true
==== innerMethod start ====
==== innerMethod transaction Active : true
==== innerMethod end ====
==== outerMethod end ====

- outer, inner 메서드 모두 트랜잭션이 적용된다.

- 스프링에서 트랜잭션 전파(Transaction Propagation) 타입의 기본값은 REQUIRED 이다.

- REQUIRED 타입은 진행중인 트랜잭션 내부에 새로운 트랜잭션이 들어온다면 기존 트랜잭션에 참여하는 전파 방식이다.

- 이러한 이유로, outerMethod의 트랜잭션에 innerMethod의 트랜잭션이 참여하는 구조가 된다.

 

 

outerMethod (@Transactional) & innerMethod

@Slf4j
@Service
public class SimpleService {

    @Transactional
    public void outerMethod() {
        log.info("==== outerMethod start ====");
        log.info("==== outerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        innerMethod();
        log.info("==== outerMethod end ====");
    }

    public void innerMethod() {
        log.info("==== innerMethod start ====");
        log.info("==== innerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        log.info("==== innerMethod end ====");
    }
}
==== outerMethod start ====
==== outerMethod transaction Active : true
==== innerMethod start ====
==== innerMethod transaction Active : true
==== innerMethod end ====
==== outerMethod end ====

- outer, inner 메서드 모두 트랜잭션이 적용된다.

- outerMethod는 @Transactional 어노테이션을 통해 트랜잭션이 시작되어 있는 상태이다.

- outerMethod가 종료되기 전에(트랜잭션이 닫히기 전에) innerMethod가 호출되었다.

- 따라서, innerMethod는 outerMethod의 트랜잭션을 사용한다.

 

 

outerMethod  & innerMethod (@Transactional)

@Slf4j
@Service
public class SimpleService {

    public void outerMethod() {
        log.info("==== outerMethod start ====");
        log.info("==== outerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        innerMethod();
        log.info("==== outerMethod end ====");
    }

    @Transactional
    public void innerMethod() {
        log.info("==== innerMethod start ====");
        log.info("==== innerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        log.info("==== innerMethod end ====");
    }
}
==== outerMethod start ====
==== outerMethod transaction Active : false
==== innerMethod start ====
==== innerMethod transaction Active : false
==== innerMethod end ====
==== outerMethod end ====

- outer, inner 메서드 모두 트랜잭션이 적용되지 않는다.

- 흔히 표현하는 '프록시 객체 내부 메서드 호출 시 트랜잭션 적용이 안되는' 문제이다.
- 스프링은 @Transactional이 붙은 메서드를 가지는 / @Transactional이 붙은 클래스에 대해 프록시 객체를 Bean으로 등록한다.

- CGLIB라는 바이트 조작 기술을 통해 해당 클래스를 상속 받은 프록시 객체로 @Transactional이 붙은 메서드를 오버라이딩 해 트랜잭션 관련 코드를 삽입한다.

- 테스트 코드에서 호출된 simpleService.outerMethod()는 사실 프록시 객체의 outerMethod를 호출한 것이다.

- 이 때, 프록시 객체 내부에서 실제 simpleService의 outerMethod()를 호출한다. (target.outerMethod())

- outerMethod 내에서 innerMethod를 호출하므로 다음과 같이 동작한다.

public void outerMethod() {
    log.info("==== outerMethod start ====");
    log.info("==== outerMethod transaction Active : {}",
            TransactionSynchronizationManager.isActualTransactionActive());
    this.innerMethod();
    log.info("==== outerMethod end ====");
}

- 즉, 트랜잭션 관련 코드가 존재하지 않는 실제 객체의 innerMethod가 호출되어 트랜잭션이 적용되지 않는다.

- 이를 해결하기 위해서 @Transactional을 외부 메서드에 붙이는 것이 권장된다.

- 내부메서드에 @Transactional을 붙여야 한다면 별도의 클래스를 생성하여 해당 메서드를 사용해야한다.

@Slf4j
@Service
public class SimpleInternalService {

    @Transactional
    public void innerMethod() {
        log.info("==== innerMethod start ====");
        log.info("==== innerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        log.info("==== innerMethod end ====");
    }
}
@Slf4j
@Service
public class SimpleService {

    private final SimpleInternalService simpleInternalService;

    public SimpleService(SimpleInternalService simpleInternalService) {
        this.simpleInternalService = simpleInternalService;
    }

    public void outerMethod() {
        log.info("==== outerMethod start ====");
        log.info("==== outerMethod transaction Active : {}",
                TransactionSynchronizationManager.isActualTransactionActive());
        simpleInternalService.innerMethod();
        log.info("==== outerMethod end ====");
    }
}
==== outerMethod start ====
==== outerMethod transaction Active : false
==== innerMethod start ====
==== innerMethod transaction Active : true
==== innerMethod end ====
==== outerMethod end ====

 

3. 정리

- 프록시의 동작원리에 따라, 외부메서드 - 내부메서드 관계에 대해서 @Transactional을 외부메서드에 붙여야 트랜잭션 적용이 올바르게 수행된다.

- 내부메서드에만 트랜잭션을 적용해야 하는 상황이라면, 별도의 클래스로 내부메서드를 빼내 해당 클래스의 프록시가 올바르게 트랜잭션 관련 코드를 적용할 수 있도록 처리한다.