본문 바로가기

Spring

[Spring] @Transactional 호출 시 주의사항

Spring 프레임워크에서 제공하는 @Transactional 어노테이션을 사용하면 트랜잭션 AOP가 적용된다. 이 트랜잭션 AOP는 프록시 방식의 AOP를 사용하며 프록시 객체가 먼저 클라이언트의 요청을 받고 트랜잭션 처리를 한 후 실제 객체를 호출해준다. 만약 프록시 객체를 거치지 않고 실제 객체를 직접적으로 호출하게 되면 트랜잭션 AOP가 적용되지 않고 트랜잭션도 시작하지 않는다.

 

일반적으로 @Transactional 어노테이션을 선언하면 프록시 객체가 빈으로 등록되고 의존성 주입할 때도 프록시 객체를 주입하기 때문에 위와 같은 상황이 발생하지 않지만 실제 객체 내부에서 메서드 호출이 일어나면 프록시 객체를 거치지 않고 대상 객체를 직접 호출하기 때문에 트랜잭션이 적용되지 않는 경우가 발생한다.

예시 코드

@Slf4j
@SpringBootTest
public class InternalCallV1Test {

    @Autowired
    CallService callService;

    @Test
    void printProxy() {
        log.info("callService class = {}", callService.getClass());
    }

    @Test
    void internalCall() {
        callService.internal();
    }

    @Test
    void externalCall() {
        callService.external();
    }

    @TestConfiguration
    static class InternalCallV1TestConfig {

        @Bean
        CallService callService() {
            return new CallService();
        }
    }

    static class CallService {

        public void external() {
            log.info("call external");
            printTxInfo();
            internal();
        }

        @Transactional
        public void internal() {
            log.info("call internal");
            printTxInfo();
        }

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isSynchronizationActive();
            log.info("tx active = {}", txActive);
        }
    }
}

 

internalCall() 메서드를 실행하여 직접 @Transactional 어노테이션이 선언된 internal() 메서드를 호출해보자.

아래와 같이 정상적으로 트랜잭션이 시작하고 printTxInfo() 메서드를 호출하여 트랜잭션이 활성 상태인 것을 확인한 후 마지막으로 트랜잭션이 종료된 것 까지 확인할 수 있다.

 

 

이번에는 externalCall() 메서드를 실행하여 external() 메서드를 호출해보자.

external() 메서드는 @Transactional 어노테이션이 선언되어 있지 않기 때문에 트랜잭션이 비활성 상태인 것을 확인한 후 internal() 메서드를 호출한다. internal() 메서드에는 트랜잭션이 선언되어 있기 때문에 internalCall()을 통해 호출했을 때처럼 활성 상태로 출력될 줄 알았지만 결과는 아래와 같이 트랜잭션도 시작하지 않고 로그도 false로 출력됐다.

 

 

왜 이런 현상이 발생하는 것일까?

결론부터 말하면 실제 객체 내부에서 메서드를 호출하면 프록시 객체가 아닌 현재 인스턴스에서 internal() 메서드를 직접 호출하기 때문에 트랜잭션이 적용되지 않는다.

 

 

자바에서 메서드 앞에 별도의 참조가 없으면 this가 생략되어 있다. 예) internal() == this.internal()

현재 external() 메서드는 @Transactional 어노테이션이 선언되어 있지 않기 때문에 프록시 객체가 아닌 실제 객체를 통해 호출한다. 이때 internal() 메서드를 호출하면 this가 생략되어 있으므로 현재 인스턴스 즉, 실제 객체에서 internal() 메서드를 호출하고 결과적으로 프록시 객체를 거치지 않기 때문에 트랜잭션이 적용되지 않는다.

 

이를 해결하기 위한 방법으로 internal() 메서드를 별도의 클래스로 분리하는 방법이 있다.

수정 코드

@Slf4j
@SpringBootTest
public class InternalCallV2Test {

    @Autowired
    CallService callService;

    @Test
    void printProxy() {
        log.info("callService class = {}", callService.getClass());
    }

    @Test
    void externalCall() {
        callService.external();
    }

    @TestConfiguration
    static class InternalCallV1TestConfig {

        @Bean
        CallService callService() {
            return new CallService(internalService());
        }

        @Bean
        InternalService internalService() {
            return new InternalService();
        }
    }

    @RequiredArgsConstructor
    static class CallService {

        private final InternalService internalService;

        public void external() {
            log.info("call external");
            printTxInfo();
            internalService.internal();
        }

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isSynchronizationActive();
            log.info("tx active = {}", txActive);
        }
    }

    static class InternalService {

        @Transactional
        public void internal() {
            log.info("call internal");
            printTxInfo();
        }

        private void printTxInfo() {
            boolean txActive = TransactionSynchronizationManager.isSynchronizationActive();
            log.info("tx active = {}", txActive);
        }
    }
}

 

internal() 메서드를 별도의 클래스로 분리하고 스프링 빈으로 등록하여 external() 메서드가 있는 클래스에 의존성 주입 해주었다. 이때 internal() 메서드에 @Transactional 어노테이션이 선언되어 있으므로 프록시 객체가 주입된다. 이러면 프록시 객체를 통해 internal() 메서드를 호출하므로 externalCall() 메서드를 실행했을 때 아래와 같이 정상적으로 internal() 메서드에 트랜잭션이 적용된 것을 확인할 수 있다.