먼저 프록시를 이용한 트랜잭션 처리 과정을 보자.
실제 비즈니스 로직은 프록시 객체가 호출하고 있는 것을 확인할 수 있다. 비즈니스 로직에서 @Transactional을 사용하고 있기 때문에 프록시 객체가 생성된 것인데, 만약 서비스 로직 내부에서 @Transactional이 붙어있지 않은 메서드가 @Transactional 메서드를 호출하면?
코드로 확인해보자.
public class InternalCallV1Test {
// 의존성 관련 로직 생략
@Test
void printProxy() {
log.info("callService class={}", callService.getClass());
}
static class CallService {
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx Active={}", txActive);
}
}
public class InternalCallV1Test {
// 의존성 관련 로직 생략
@Test
void printProxy() {
log.info("callService class={}", callService.getClass());
}
@Test
void externalCall() {
callService.external();
}
static class CallService {
public void external() {
log.info("call external");
printTxInfo();
// @Transactional 메서드 내부 호출
internal();
}
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx Active={}", txActive);
}
}
프록시 객체가 아닌 실제 서비스 객체에서 internal메서드를 호출하므로 트랜잭션이 적용되지 않는다.
그렇다면 이런 문제는 어떻게 해결할 수 있을까?
여러 방법이 있지만 가장 단순한 방법은 @Transactional이 붙은 메서드를 별도의 클래스로 정의하는 것이다.
코드로 확인해보자.
@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);
}
}
// internal 메서드를 별도의 클래스로 분리
static class InternalService {
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("tx Active={}", txActive);
}
}
스프링 트랜잭션 기능은 public메서드에만 적용이 된다고 한다.
@Transactional을 클래스 단위에 적용하면 의도하지 않는 메서드에까지 트랜잭션이 적용될 수 있는데, 트랜잭션은 주로 비즈니스 로직의 시작지점에 걸기 때문에 이처럼 외부에 열어준 부분을 시작점으로 한다. 따라서 스프링은 public 메서드에만 트랜잭션을 적용하도록 설정하고 있다.
@Slf4j
static class Hello {
// 트랜잭션 적용 X
@PostConstruct
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}
// 트랜잭션 적용 O
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active={}", isActive);
}
}
@PostConstruct와 @Transactional을 함께 사용하는 경우
트랜잭션이 적용되지 않는다.
@PostConstruct에 의해 초기화 코드가 먼저 호출이 되고, 그 뒤에 트랜잭션 AOP가 적용되는데, 빈의 생성과 초기화 단계에서는 아직 트랜잭션 AOP 프록시가 생성되지 않았기 때문이다.
@EventListener(ApplicationReadyEvent.class)와 함께 사용
트랜잭션 AOP를 포함한 스프링 컨테이너가 완전히 생성되고 난 후 메서드가 실행되므로 트랜잭션이 적용된다.
출처 : 김영한 - 스프링 DB 2편
유익한 글이었습니다.