스프링DB

트랜잭션 AOP 주의 사항

lby132 2022. 9. 14. 21:41

첫번째는 @Transactional을 쓸경우 public 이 붙은 메소드에서만 적용이된다.

만약 protected나 pakage visible일 경우에는 @Transactional이 작동하지 않는다.

//Transaction 작동함
@Transactional
public void internal() {
    log.info("call internal");
    printTxInfo();
}

//Transaction 작동안함
@Transactional
void internal() {
    log.info("call internal");
    printTxInfo();
}

private void printTxInfo() {
    final boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("txActive={}", txActive);
}

 

 

 

그리고 두번째는 @Transactional이 있는 메서드를 호출하면 그 메서드는 프록시가 적용이 되는데 @Transactional이 없는 메서드안에서 @Transactional이 있는 메서드를 호출하면 트랜잭션이 적용되지 않는다.

@SpringBootTest
@Slf4j
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
        public CallService callService() {
            return new CallService();
        }
    }

    @Slf4j
    static class CallService {

        public void external() {
            log.info("call external");
            printTxInfo();
            internal();	// 문제 발생. @Transactional이 있지만 여기서는 적용되지 않는다.
        }

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


        private void printTxInfo() {
            final boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("txActive={}", txActive);
        }

    }
}

external메서드를 보면 interanl메서드를 호출하는데 internal메서드에는 @Transactional이 있기때문에 트랜잭션이 적용되어야 맞다고 생각하겠지만 그렇지않다. 이유는 this이다. 보이지 않지만 결과적으로 자기 자신의 내부 메서드를 호출하는 this.internal()이 되는데 

여기서 this는 자기 자신을 가리키므로 실제 대상 객체(target)의 인스턴스이다. 즉 트랜잭션이 적용되어 있지 않은 external()를 호출 했기 때문에 external()는 프록시가 아닌 그냥 객체이기 때문에 this.internal()는 트랜잭션이 적용되지 않은 external()에 있는 internal()를 직접 호출하게 된 것이다. 그래서 해결 방안은 내부 호출을 피하기 위해 internal()를 별도의 클래스로 분리하는 것이다.

@SpringBootTest
@Slf4j
public class InternalCallV2Test {

    @Autowired
    CallService callService;

    @Autowired
    InternalService internalService;

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

    @Test
    void internalCallV2() {
        internalService.internal();
    }

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

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

        @Bean
        public InternalService internalService() {
            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() {
            final boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("txActive={}", txActive);
        }
    }

    static class InternalService {

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

        private void printTxInfo() {
            final boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
            log.info("txActive={}", txActive);
        }
    }
}

 

그리고 세번째는 @PostConstruct이다. 

@PostConstruct
@Transactional
public void initV1() {
    boolean isActive = TransactionSynchronizationManager.isSynchronizationActive();
    log.info("Hello init @PostConstruct tx active={}", isActive);
}

이렇게 되어 있다면 결과는 "Hello init @PostConstruct tx active=false" 가 출력된다.

(TransactionSynchronizationManager.isSynchronizationActive(); 이건 트랜잭션이 적용이 됬으면 true 아니면 false를 반환한다.)

트랜잭션이 적용되지 않는 이유는 초기화 코드가 먼저 호출되고 트랜잭션 AOP가 적용되기 때문. 따라서 초기화 시점에는 해당 메서드에서 트랜잭션을 획득할 수 없다. 

해결 방안은 

@PostConstruct
@Transactional
public void initV1() {
    boolean isActive = TransactionSynchronizationManager.isSynchronizationActive();
    log.info("Hello init @PostConstruct tx active={}", isActive);
}

@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
    boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
    log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}

ApplicationReadyEvent는 스프링 컨테이너가 완전히 다 뜨고 난후 호출되게 하는건데 스프링이 다 떳다는건 트랜잭션 aop등 적용돼서 스프링이 완성이 된 상태. 그리고 난 후 호출이 되기 때문에 트랜잭션이 적용이 된다. 결과를 보면

hello.springtx.apply.InitTxTest$Hello    : Hello init @PostConstruct tx active=false
hello.springtx.apply.InitTxTest          : Started InitTxTest in 3.567 seconds (JVM running for 5.048)
o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.apply.InitTxTest$Hello.initV2]
hello.springtx.apply.InitTxTest$Hello    : Hello init ApplicationReadyEvent tx active=true
o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.apply.InitTxTest$Hello.initV2]

첫번째 줄 : @PostConstruct가 적용되어서 트랜잭션 적용 안됨.

두번째 줄 : 스프링컨테이너가 완료되어서 스프링이 뜸.

세번째 줄 : 스프링이 뜨면 스프링은 @EventListener(ApplicationReadyEvent.class)가 붙어있는 메소드를 실행시켜주기 때문에 initV2메소드가 실행이 되면서 트랜잭션이 시작됐다. 프록시에서 호출한것.

네번째 줄 : 트랜잭션이 적용이 됨.

다섯번째 줄 : 트랜잭션이 종료가 되면서 프록시도 커넥션을 종료.

'스프링DB' 카테고리의 다른 글

트랜잭션 전파  (0) 2022.09.16
트랜잭션 이해  (0) 2022.09.15
트랜잭션 매니저 선택  (0) 2022.09.14
JPQL  (0) 2022.09.01
Jpa 적용  (0) 2022.09.01