[Spring]@Transactional 사용 시 주의사항

스프링에서의 @Transactional은 기본적으로 프록시방식의 AOP를 사용한다. 호출순서는 아래와 같다.

클라이언트 -> 프록시 호출 -> 프록시 @Transactional 메서드 호출 -> 트랜잭션 적용 -> target 호출 -> target 메서드 호출

그런데 target 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 메서드를 호출하기 때문에 트랜잭션이 적용되지 않는 문제가 있다. 아래 예제를 참조하자.

@Slf4j
@SpringBootTest
public class InternalCallV1Test {

    @Autowired
    CallService callService;

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

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

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

    @TestConfiguration
    static class TestConfig {
        @Bean
        CallService callService() {
            return new CallService();
        }
    }


    @Slf4j
    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 isTxActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("tx active = {}", isTxActive);
        }
    }
}

위 CallService에서 internal()메서드를 직접 호출하면 @Transactional이 적용되는 것을 확인할 수 있다. 하지만 external() 메서드를 호출하면, 내부에서 호출한 internal() 메서드에는 트랜잭션이 적용되지 않는 것을 확인할 수 있다.

이는 프록시 객체의 AOP를 통해 트랜잭션이 적용되는 과정이 빠졌기 때문이다. external() 메서드를 호출할 경우 실행 순서는 아래와 같다.

클라이언트 -> 프록시 호출 -> 프록시 external() 호출 -> 트랜잭션 미적용 -> target 호출 -> target external() 호출 -> target internal() 호출

이 문제를 해결하는 손쉬운 방법은 내부 호출을 피하는 것이다. internal() 를 별도의 클래스로 분리하면 해결된다. 아래 코드를 참조하자.

@Slf4j
@SpringBootTest
public class InternalCallV2Test {

    @Autowired
    CallService callService;

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

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

    @TestConfiguration
    static class TestConfig {
        @Bean
        CallService callService() {
            return new CallService(internalService());
        }

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


    @Slf4j
    static class CallService {

        private final InternalService internal;

        public CallService(InternalService internal) {
            this.internal = internal;
        }

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

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

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

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

internal()를 InternalService로 분리하고, CallService.external()에서는 외부의 InternalService.call()을 호출하게 한다. 실행순서는 아래와 같다.

클라이언트 -> CallService 프록시 호출 -> CallService 프록시 external() 호출 -> CallService target 호출 -> CallService target external() 호출 -> InternalService 프록시 호출 -> InternalService 프록시 call() 호출 -> 트랜잭션 적용 -> InternalService target 호출 -> InternalService target call() 호출

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다