트랜잭션 AOP 주의 사항 (+ proxy 내부 호출, 초기화 시점)
◎ proxy 내부 호출
먼저 @Transactional 을 적용하면 proxy 객체가 먼저 요청을 받아서 트랜잭션을 처리하고, 실제 객체를 호출해준다.
만약 proxy 를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP 가 적용되지 않고, 트랜잭션도 적용되지 않는다.
공부한 내용을 자세히 정리하였다..
코드를 통해 알아보자
먼저 전체적인 코드 로직은 아래와 같다.
@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 internalCallV1TestConfig {
@Bean
CallService callService() {
return new CallService();
}
}
@Slf4j
static class CallService {
public void external() { // 외부에서 호출하는 메서드 (spring Bean 으로 등록이 안되있기 떄문에 프록시 생성이 안된다.) 트랜잭션은 수행 된다.
log.info("call external");
printTxInfo();
internal();
}
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("active={}", txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("tx readOnly={}", readOnly);
}
}
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive(); // 트랜잭션이 적용 되었는지 여부를 확인하는 메서드
log.info("active={}", txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); // 트랜잭션이 readOnly 인지 확인하는 메서드
log.info("tx readOnly={}", readOnly);
}
printTxInfo() 설정 정보 확인 : 트랜잭션의 적용 여부와 readOnly 의 타입을 확인하는 메서드
1. internal()
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
※ 그림 참조 : internal() 내부 트랜잭션 적용 로직
현재 internal() 메서드는 트랜잭션이 적용 되어 있는 메서드이다.
2. external()
public void external() { // 외부에서 호출하는 메서드 (spring Bean 으로 등록이 안되있기 떄문에 프록시 생성이 안된다.) 트랜잭션은 수행 된다.
log.info("call external");
printTxInfo();
internal();
}
※ 그림 참조 : external() 내부 로직
external() 메서드는 트랜잭션이 적용 되어 있지 않은 메서드이다.
당연히 internal() 메서드는 트랜잭션이 적용 되어 있기 때문에 Spring Bean 에 등록되고 프록시 객체가 생성된다.
반대로 external() 메서드는 트랜잭션이 적용 되어 있지 않고 Spring Bean 에 등록되지 않았기 때문에 일반적인 메서드로 취급된다.
그런데 여기 external() 메서드에서 proxy 가 적용 되었던 internal() 메서드를 다시 한번 호출하고 있는 것을 볼 수 있다. (실무에서는 이런 경우도 발생 할 수 있다)
이렇게 되면 internal() 은 프록시와 트랜잭션이 적용된 메서드일까?
위의 external() 메서드를 실행한 후 log 에서 확인 할 수 있듯이 internal 은 active = false 를 출력하는 것을 볼 수 있다.
즉, Transactional 이 적용되지 않은 일반 메서드에서 proxy, transcational 메서드를 호출 하게 되면 해당 proxy 메서드는 프록시와 Transactional 기능을 모두 잃어버린 일반 메서드로 취급 된다는 것이다.
※ 이유 :
자바 언어에서 메서드 앞에 별도의 참조가 없으면 this 인 자신의 인스턴스를 가리킨다.
결과적으로 위의 public void external(){} 인 @Transactional 이 적용되지 않은 메서드에서 외부 메서드를 호출하게 되면 위의 그림에 있는 target 에 있는 일반 메서드 internal() 을 호출 하게 되는 것이다.
이것이 프록시 방식의 AOP 한계이다.
해결 방안
- internal() 메서드와 같은 프록시 객체를 트랜잭션이 아닌 외부에서 호출할 때 프록시 객체가 생성되도록 하는 방법은 아래와 같이 트랜잭션이 적용된 internal() 을 별도의 클래스로 구분하는 방법이 있다
@Slf4j
@SpringBootTest
public class InternalCallV2Test {
@Autowired
CallService callService;
@Test
void printProxy() {
log.info("callService class={}", callService.getClass());
}
@Test
void externalCallV2() {
callService.external();
}
@TestConfiguration
static class internalCallV2TestConfig {
@Bean
CallService callService() {
return new CallService(internalService());
}
@Bean
InternalService internalService() {
return new InternalService();
}
}
@Slf4j
@RequiredArgsConstructor
static class CallService {
private final InternalService internalService;
public void external() { // 외부에서 호출하는 메서드 (spring Bean 으로 등록이 안되있기 떄문에 프록시 생성이 안된다.) 트랜잭션은 수행 된다.
log.info("call external");
printTxInfo();
internalService.internal(); // 별도로 분리된 internalService 를 통해 internal() 를 호출하면 트랜잭션과 프록시가 적용 된다.
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive(); // 트랜잭션이 적용 되었는지 여부를 확인하는 메서드
log.info("active={}", txActive);
}
}
// InternalService 를 '별도의 클래스로 분리'해서 해당 프록시를 외부에서 호출할 수 있도록 설정
static class InternalService {
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
private void printTxInfo() {
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive(); // 트랜잭션이 적용 되었는지 여부를 확인하는 메서드
log.info("active={}", txActive);
boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); // 트랜잭션이 readOnly 인지 확인하는 메서드
log.info("tx readOnly={}", readOnly);
}
}
}
internalService 클래스를 외부 클래스인 CallService 에 의존관계 주입을 통해 외부에서도 InternalService 의 클래스를 참조하여 internal() 메서드를 호출할 수 있도록 개선하면 external() 메서드에서도 트랜잭션이 적용된 internal() 메서드를 호출 할 수 있게 된다.
◎ 초기화 시점
초기화 코드에 @PostConstruct 를 선언하면 @Transcationl 이 걸려 있어도 트랜잭션 적용이 되지 않는다
code ex)
@Slf4j
static class Hello {
@PostConstruct // 종속성 주입이 완료된 후 초기화를 수행하기 위해 실행해야 하는 메서드에 사용 (?)
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}
public void initV2() {
}
}
@PostConstruct // 종속성 주입이 완료된 후 초기화를 수행하기 위해 실행해야 하는 메서드에 사용 (?)
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active={}", isActive);
}
initV1() 메서드에 @PostConstruct 와 @Transactional 어노테이션을 같이 사용하여 외부에서 아래와 같이 go() 라는 메서드를 통해 initV1() 메서드를 호출 하였다.
@SpringBootTest
public class InitTxTest {
@Autowired
Hello hello;
@Test
void go() {
// 초기화 코드는 스프링이 초기화 시점에 호출된다 (@PostConstruct 기능) 즉, initV1() 을 호출해준다. (신기..)
}
@TestConfiguration
static class InitTxTestConfig {
@Bean
Hello hello() {
return new Hello();
}
}
※ 이유 : 초기화 코드가 먼저 호출 되고, 그 다음에 트랜잭션 AOP 가 적용되기 때문이다. 따라서 초기화 시점에는 해당 메서드 트랜잭션 획득 불가
이러한 경우를 개선한 코드는 아래와 같다 - initV2()
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @ApplicationReadyEvent tx active={}", isActive);
}
- 앞서, @PostConstruct 와 달리 스프링에서 제공하는 EventListner(ApplicationReadyEvent.class) 를 적용하면 스프링은 컨테이너가 완전히 생성되고 난 다음 이벤트가 붙은 메서드를 호출해 준다. 따라서 initV2() 는 트랜잭션이 적용된다.
※ 정리 : 트랜잭션 내부에서 수행을 해야될때는 @EventListener 를 사용하고, 일반적인 초기화를 진행할 때 (트랜잭션 미적용)에는 @PostConstruct 를 사용하면 된다.