트랜잭션 AOP 주의 사항 - 프록시 내부 호출시 트랜잭션 적용안되는 이슈

김건우·2023년 2월 27일
0

spring

목록 보기
9/9
post-thumbnail

프록시 내부 호출

@Transactional 을 사용하면 스프링의 트랜잭션 AOP가 적용된다.
트랜잭션 AOP는 기본적으로 프록시 방식의 AOP를 사용한다.
📢@Transactional 을 적용하면 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고, 실제 객체를 호출해준다.
따라서 트랜잭션을 적용하려면 항상 프록시를 통해서 대상 객체(Target)을 호출해야 한다. 이렇게 해야 프록시에서 먼저 트랜잭션을 적용하고, 이후에 대상 객체를 호출하게 된다.만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고, 트랜잭션도 적용되지않는다.

@Slf4j
@SpringBootTest
public class TxBasictest {
    
    @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 InternalCallV1Config {
            @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 txActive =
                        TransactionSynchronizationManager.isActualTransactionActive();
                log.info("tx active={}", txActive);
            }
        }
    }

위의 메서드는 external() 메서드만 호출하면 트랜잭션이 걸리지 않은 상태로 로직을 수행하다 internal()메서드를 호출할 때 트랜잭션을 물고 수행한다 라고 예상되는 로직의 코드이다. 하지만 실제론 external()도 internal()도 트랜잭션이 걸리지 않는 문제가 발생한다.(당연히 internal()은 Transactional 어노테이션이 존재하기 때문에 트랜잭션에 걸린다.)

  @Transactional
   public void internal() {
                log.info("call internal");
                printTxInfo();
            }
            
   private void printTxInfo() {
                boolean txActive =
                        TransactionSynchronizationManager.isActualTransactionActive();
                log.info("tx active={}", txActive);
            }

internal() 메서드의 동작 순서와 방식
1. 클라이언트인 테스트 코드는 callService.internal() 을 호출한다. 여기서 callService는 트랜잭션 프록시이다.
2. callService 의 트랜잭션 프록시가 호출된다.
3. internal() 메서드에 @Transactional 이 붙어 있으므로 트랜잭션 프록시는 트랜잭션을 적용한다.
4. 트랜잭션 적용 후 실제 callService 객체 인스턴스의 internal() 을 호출한다. 실제 callService가 처리를 완료하면 응답이 트랜잭션 프록시로 돌아오고, 트랜잭션 프록시는 트랜잭션을 완료한다.

🔽실행로그
TransactionSynchronizationManager.isActualTransactionActive(); 의 반환값이 true이므로 트랜잭션의 적용된 것을 확인할 수 있다.

TransactionInterceptor : Getting transaction for 
[..CallService.internal]
..rnalCallV1Test$CallService : call internal
..rnalCallV1Test$CallService : tx active=true
TransactionInterceptor : Completing transaction for 
[..CallService.internal]

문제는 이제 여기서 발생한다 🔽.

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

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

external() 은 @Transactional 애노테이션이 없다. 따라서 트랜잭션 없이 시작한다. 그런데 내부에서 @Transactional 이 있는 internal() 을 호출하는 것을 확인할 수 있다. 이 경우 external() 은 트랜잭션이 없지만, internal() 에서는 트랜잭션이 적용되는 것 처럼 보인다.

문제가 되는 코드의 실행로그🔽

CallService : call external
CallService : tx active=false
CallService : call internal
CallService : tx active=false

위의 로그를 보면 internal() 내부에서 호출한 tx active=false 로그를 통해 확실히 트랜잭션이 수행되지 않은 것을 확인할 수 있다.

external() 메서드의 실행 순서

  1. 클라이언트인 테스트 코드는 callService.external() 을 호출한다. 여기서 callService는 트랜잭션 프록시이다.
  2. callService 의 트랜잭션 프록시가 호출된다.
  3. external() 메서드에는 @Transactional 이 없다. 따라서 트랜잭션 프록시는 트랜잭션을 적용하지않는다.
  4. 📢트랜잭션 적용하지 않고, 실제 callService 객체 인스턴스의 external() 을 호출한다.
  5. external() 은 📢내부에서 internal() 메서드를 호출한다. 그런데 여기서 문제가 발생한다.

위의 코드를 잘보아야하는데 external() 메서드에서는 internal() 메서드를 호출하는데

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

📢 자기 자신의 내부호출을 하고 있다. 내부 호출은 프록시를 거치지 않는다.

문제원인

자바 언어에서 메서드 앞에 별도의 참조가 없으면 this 라는 뜻으로 자기 자신의 인스턴스를 가리킨다. 결과적으로 자기 자신의 내부 메서드를 호출하는 this.internal() 이 되는데,결과적으로 이러한 내부 호출은 프록시를 거치지 않는다. 따라서 트랜잭션을 적용할 수 없다. 결과적으로 target 에 있는 internal() 을 직접 호출하게 된 것이다.

✅ 해결방법 (내부호출에서 외부호출로 변경)

메서드 내부 호출 때문에 트랜잭션 프록시가 적용되지 않는 문제를 해결하기 위해 internal() 메서드를 별도의 클래스로 분리하자.

@Slf4j
@SpringBootTest
public class TxBasictest {

    @Autowired
    CallService callService;
    
    @Test
    void externalCallV2() {
        callService.external();
    }
    
    @TestConfiguration
    static class InternalCallV2Config {
        
        @Bean
        CallService callService() {
            return new CallService(innerService());
        }
        
        @Bean
        InternalService innerService() {
            return new InternalService();
        }
    }
    
    @Slf4j
    @RequiredArgsConstructor
    static class CallService {
    
        private final InternalService internalService;
        
        public void external() {
            log.info("call external");
            printTxInfo();
            internalService.internal();
        }
        
        private void printTxInfo() {
            boolean txActive =
                    TransactionSynchronizationManager.isActualTransactionActive();
            log.info("tx active={}", txActive);
        }
    }
    
    @Slf4j
    static class InternalService {
        
        @Transactional
        public void internal() {
            log.info("call internal");
            printTxInfo();
        }
        
        private void printTxInfo() {
            boolean txActive =
                    TransactionSynchronizationManager.isActualTransactionActive();
            log.info("tx active={}", txActive);
        }
    }
}

실제 호출 순서

  1. callService.external() 을 호출한다.
  2. callService 는 실제 callService 객체 인스턴스이다.
  3. callService 는 주입 받은 internalService.internal() 을 호출한다.
  4. internalService 는 트랜잭션 프록시이다. internal() 메서드에 @Transactional 이 붙어있으므로 트랜잭션 프록시는 트랜잭션을 적용한다.
  5. 트랜잭션 적용 후 실제 internalService 객체 인스턴스의 internal() 을 호출한다.

실행 로그

아래의 로그를 보면 internal()을 호출하면 트랜잭션이 true인 것을 확인할 수 있다.🔽

#external()
..InternalCallV2Test$CallService : call external
..InternalCallV2Test$CallService : tx active=false

#internal()
TransactionInterceptor : Getting transaction for 
[..InternalService.internal]
..rnalCallV2Test$InternalService : call internal
..rnalCallV2Test$InternalService : tx active=true
TransactionInterceptor : Completing transaction for 
[..InternalService.internal]
profile
Live the moment for the moment.

0개의 댓글