protected 메서드에서의 @Transactional

그래서 왜 protected 메서드에 @Transactional이 안 먹힐까요?

이전 포스팅에서 프록시 팩토리를 통해 인터페이스를 구현한 클래스의 경우엔 JDK Dynamic Proxy를 사용하고, 아니라면 CGLIB을 사용한다고 했습니다. JDK 동적 프록시의 경우 인터페이스를 오버라이딩 해야 하기 때문에 final이나 private 키워드가 붙은 타겟에 대해선 사용할 수 없겠죠.

그런데 CGLIB는 클래스를 상속하여 오버라이딩 하기 때문에, 당연히 protected 메서드에도 AOP가 적용됩니다. 그러므로 CGLIB으로 생성된 프록시 객체에는 protected 메서드에도 @Transactional이 정상적으로 적용돼야 합니다. 그러나, 이 생각은 틀렸습니다.

💡@Transactional 어노테이션

먼저 @Transactional 어노테이션에 대해 조금 설명하겠습니다. Spring은 @Transactional이 달린 모든 클래스나 메서드에 대해서 프록시를 만듭니다. 클래스 레벨에 어노테이션을 붙이면 해당 클래스 내의 모든 메서드에 붙인 효과와 동일합니다. 이렇게 생성된 프록시를 통해 트랜잭션을 시작하고 커밋하기 위해 실행 전후로 트랜잭션 로직을 주입할 수 있습니다.

@Service
public class MyService {
    ...    
    @Transactional
    public void doSomething() {
        log.debug("Tx: {}", TransactionSynchronizationManager.getCurrentTransactionName());
    }
}
@Test
void transactionAppliedToPublicMethod() {
    log.debug("Proxy: {}", myService.getClass().getName());
    myService.doSomething();
}
Proxy: com.example.demo.MyService$$EnhancerBySpringCGLIB$$36fe23a0
Tx: com.example.demo.MyService.doSomething

따라서 위 테스트 코드를 실행 시 MyService는 인터페이스를 구현하지 않은 타겟이므로 Enhancer를 이용한 CGLIB 프록시가 생성되었고, 트랜잭션도 해당 메서드의 이름으로 잘 작동하는 것을 확인할 수 있습니다.

💡문제의 시작

상기 내용까지의 지식으로 저는 '타겟이 인터페이스를 구현하지 않았으니 CGLIB으로 프록시가 생성되고, CGLIB으로 생성된 프록시의 Tx는 protected에도 적용이 된다'라고 생각했었습니다. 왜냐하면 protected 레벨은 당연히 상속이 가능하니까요.

하지만 웬일인지 IDE는 protected에 빨간 줄을 그어버립니다. 물론 컴파일 타임엔 이상이 없습니다. 그러나 런타임에 doSomething을 호출해서 로그를 찍어보면 트랜잭션이 적용되지 않는다는 것을 확인할 수 있습니다.

@Transactional
protected void doSomething() {
    log.debug("Tx: {}", TransactionSynchronizationManager.getCurrentTransactionName());
}
Tx: null

💡공식 문서를 잘 읽어봅시다

AOP 문서에서는 Spring AOP의 프록시 기반 특성 때문에 CGLIB은 protected에 '기술적으로는' 가능하지만 추천하지 않는다고 하네요. 그러나 이건 일반적인 AOP 얘기고, @Transactional 얘기는 아닙니다. 더군다나 @Transcational은 protected 메서드에 기술적으로 먹히지도 않았습니다.

@Transcational 어노테이션 문서를 보니 스프링의 기본 설정에선 @Transactional 어노테이션은 public 제어자에만 적용된다고 합니다. @Transactional 자체가 굉장히 블랙박스처럼 지원되는 어노테이션이라 자세한 이유는 따지지 않고 'JDK 다이나믹 프록시 동작과의 일관성을 위해 public 메서드에만 지원한다.' 정도로만 이해하도록 하겠습니다.

💡@Transactional은 무조건 public 메서드에만 적용되나?

If you need to annotate non-public methods, consider the tip in the following paragraph for class-based proxies or consider using AspectJ compile-time or load-time weaving (described later).

공식 문서를 참고해보면 non-pulbic 메서드에 적용시키고 싶다면 AspectJ를 사용하여 CTW(Compile-Time Weaving)이나 LTW(Load-Time Weaving)을 할 것을 권장하고 있습니다. 또한 @Configuration 클래스에서 @EnableTransactionManagement를 사용하는 경우 다음과 같이 transactionAttributeSource 빈을 등록하여 @Transcational을 적용할 수 있다고 합니다. (프로퍼티에서 spring.main.allow-bean-definition-overriding 값을 true로 변경하신 후 등록하셔야 합니다.)

@Bean
TransactionAttributeSource transactionAttributeSource() {
    return new AnnotationTransactionAttributeSource(false);
}

💡이제는 신경 쓸 필요 없는 문제?

결론적으로 Spring 6, 그리고 그에 대응되는 Spring Boot 3부터는 protected 메서드에서도 @Transactional이 적용됩니다. 그러나 공식 문서에서는 여전히 RTW(Run-Time Weaving)은 public 메서드에서 할 것을 권장하는만큼 특별한 정책이 없다면 public 메서드에 적용시키는게 좋아보입니다.


🎯정리

  • @Transactional은 기본적으로 public 메서드에만 적용됩니다.

  • 정 쓰고싶다면 AspectJCTW 또는 LTW 하거나 TransactionAttributeSource 빈을 등록 해서 AnnotationTransactionAttributeSource(false)를 반환해주면 됩니다.

  • 다만 Spring 6, Spring Boot 3부터는 protected 메서드에도 기본적으로 적용됩니다.


🔖참고