[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() 호출
최신 댓글