트랜잭션 AOP 주의 사항

프록시 내부 호출 1

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

img.png
  • AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록한다. 따라서 스프링은 의존관계 주입 시에 항상 실제 객체 대신에 프록시 객체를 주입한다. 그렇기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지는 않는다. 하지만 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다. 이렇게 되면 @Transactional이 있어도 트랜잭션이 적용되지 않는다.

  • 테스트 코드

internalCall()

  • 트랜잭션이 있는 코드인 internal()을 호출한다.

  • callService의 트랜잭션 프록시가 호출된다.

  • internal()@Transactional이 있으므로 트랜잭션 프록시는 트랜잭션을 적용한다.

  • 트랜잭션 적용 후 실제 객체 인스턴스(callService)의 메서드(internal())를 호출한다.

  • 실제 객체가 처리를 완료하면 응답이 트랜잭션 프록시로 돌아오고 트랜잭션 프록시는 트랜잭션을 완료한다.

img_1.png

실행 로그

externalCall()

  • 트랜잭션이 없는 코드인 external()을 호출한다.

  • @Transactional이 없기 때문에 트랜잭션 없이 시작한다. 그런데 내부에서 @Transactional이 있는 internal()을 호출한다.

  • 당연히 internal()에서는 트랜잭션이 적용되는 것 처럼 보인다.

실행 로그

로그를 보면 트랜잭션 관련 코드는 전혀 보이지 않고 프록시가 아닌 실제 객체(CallService)에서 남긴 로그만 확인된다. tx activefalse다.

img_2.png

external()내부에서 internal()을 호출할 때 문제가 발생한다.

자바 언어에서 메서드 앞에 별도의 참조가 없으면 this라는 뜻으로 자기 자신의 인스턴스를 가리킨다. 그래서 this.internal()이 호출 되는데 여기서 this는 자기 자신을 가리키므로 실제 대상 객체(target)의 인스턴스를 뜻한다. 이러한 내부 호출은 프록시를 거치지 않기 때문에 트랜잭션을 적용할 수 없다.

결과적으로 target안에 있는 메서드를 직접 호출한 것이다.

프록시를 사용하면 메서드 내부 호출에 프록시를 적용할 수 없다.

프록시 내부 호출 2

internal()메서드를 별도의 클래스로 분리하여 해결한다.

img_3.png

실행 로그

이렇게 별도의 클래스로 분리하는 방법을 주로 사용한다.

초기화 시점

스프링 초기화 시점에는 트랜잭션 AOP가 적용되지 않을 수 있다.

실행 로그

초기화 코드(@PostConstruct)와 @Transactional을 함께 사용하면 트랜잭션이 적용되지 않는다.

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

가장 확실한 대안은 ApplicationReadyEvent 이벤트를 사용하는 것으로 이 이벤트는 트랜잭션 AOP를 포함한 스프링이 컨테이너가 완전히 생성되고 난 다음에 이벤트가 붙은 메서드를 호출해주기 때문에 트랜잭션이 적용된다.

Last updated