@Transaction 과 같은 어노테이션들은 모두 런타임에 동작하는 Spring AOP를 기반으로 동작하는데 Spring AOP에서는 어노테이션이 붙어있는 타겟 클래스의 프록시를 만들어 해당 타겟 클래스의 메서드 수행 전후에 일련의 처리를 한다. 이때 타겟 클래스의 타입에 따라 인터페이스 기반의 프록시 생성 시 JDK Dynamic Proxy, 그 외에는 CGLIB을 사용한다고 한다.
우선 Spring이 빈 생성시 해당 빈에 AOP 어노테이션이 있는지 확인하고 만약 있다면 프록시 객체를 생성하여 빈을 대체한다. 이때 위에서 말한 JDK Dynamic Proxy의 경우 인터페이스의 public 메서드, CGLIB의 경우 private를 제외한 public, protected, package-private 메서드에 한해 AOP가 적용이 가능하다.
이제 직접 눈으로 확인해보자.
테스트를 위한 서비스 코드는 다음과 같다.
이렇게 각각 인터페이스와 인터페이스가 아닌 itemRepository와 itemService를 검사해보면 무슨 프록시인지 확인해볼 수 있다.
그리고 이제 트랜잭션 관련 테스트를 진행해보면 ...
뭔가 이상하다.
private는 트랜잭션이 실행되지 않을 것이니 save 후 예외가 발생해도 모든 item 조회시 1이 나올것으로 예상할 수 있다. 하지만 트랜잭션이 실행될 것으로 예상했던 public은 조회시 결과가 0이 나오지 않았다. 트랜잭션이 실행되지 않았다는 것인데 이게 어떻게 된걸까?
사실 Spring AOP는 외부에서 프록시 객체를 통해 메서드가 호출될 때만 AOP 어드바이스를 적용 즉 트랜잭션을 적용하므로 같은 클래스 내에서 메서드를 호출(self invocation)하면 프록시를 거치지 않고 직접 호출되므로 트랜잭션 어드바이스가 적용되지 않는다. 위의 서비스 코드에서 publicSave와 privateSave를 호출하기 위해 callByPublic, callByPrivate 메서드를 public으로 설정하고 자기 자신의 메서드를 호출하게 했으므로 위와 같은 결과가 나타난 것이었다.
해결 방법은 크게 두가지이다.
첫번째 방법은 서비스 코드에서 자기 자신을 주입받게 하는 방법인데, 단순히
private final ItemService itemService;
로 주입받으면 순환참조 문제가 발생한다. 그래서 아래와 같이
ApplicationContext를 주입받아 빈을 찾아와서 실행하게 처리해주면
테스트가 성공하는걸 볼수 있다.
두번째 방법은 별도 클래스로 분리하는 방법이다.
이렇게 ItemService를 또 하나의 ExtraItemService로 묶어버리면 프록시가 생성되고 AOP가 제대로 적용되어 트랜잭션이 잘 작동한다.
사실 이렇게까지 할 필요는 없고 결국 트랜잭션을 제대로 적용하기 위해서는 private를 쓰지말고 AOP가 잘 작동할 수 있게 self-invocation을 잘 고려해서 코드를 작성하면 될 것 같다.