첫번째는 @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메소드가 실행이 되면서 트랜잭션이 시작됐다. 프록시에서 호출한것.
네번째 줄 : 트랜잭션이 적용이 됨.
다섯번째 줄 : 트랜잭션이 종료가 되면서 프록시도 커넥션을 종료.