..

Spring @Transactional 3: 프록시 방식의 주의사항

TOC


  1. Overview
  2. 케이스 1: 내부 호출할 때
  3. 케이스 2: 접근 제한자를 사용할 때
  4. 케이스 3: 기존 트랜잭션에 참여할 때
  5. 커밋은 메소드 반환과 다르다
  6. Conclusion
  7. Reference

Overview


앞선 글에서는 @Transactional이 프록시와 TransactionManager를 통해 동작하는 과정을 살펴봤다.

이번 글에서는 프록시 기반 방식에서 자주 놓치는 세 가지 케이스를 정리한다.

케이스 1: 내부 호출할 때


같은 객체 내부에서 메소드를 호출하면 프록시를 거치지 않는다.

public void outer() {
    this.inner(); // 프록시 우회
}

@Transactional
public void inner() {
}

inner()가 호출되더라도 TransactionInterceptor가 실행되지 않는다. 트랜잭션 경계를 적용하려면 메소드를 별도 Spring 빈으로 분리하고, 그 빈을 통해 호출해야 한다.

케이스 2: 접근 제한자를 사용할 때


private 메소드는 외부에서 호출할 수 없고 프록시가 대신 호출할 수도 없다.

외부 호출 불가
→ 프록시 호출 불가
→ TransactionInterceptor 미동작

따라서 private 메소드에 @Transactional을 붙여도 트랜잭션 경계가 생기지 않는다. final 클래스나 메소드도 프록시 방식에 따라 제약이 있으므로, 트랜잭션은 외부에 공개된 서비스 메소드에 두는 것이 안전하다.

케이스 3: 기존 트랜잭션에 참여할 때


기본 전파 속성인 REQUIRED는 기존 트랜잭션이 있으면 새로 만들지 않고 참여한다.

outer() 프록시 진입
  → 트랜잭션 A 시작
  → 현재 실행 컨텍스트에 리소스 연결
  → inner() 프록시 진입
  → 리소스 확인 후 트랜잭션 A에 참여
outer() 종료
  → 트랜잭션 A 커밋 또는 롤백

inner()가 끝나는 시점에 물리적인 커밋이 일어나는 것이 아니라, 바깥 트랜잭션이 전체 커밋을 책임진다.

반대로 REQUIRES_NEW는 기존 트랜잭션을 보류하고 새 트랜잭션을 시작한다. 이 속성도 호출이 프록시를 통과해야 적용된다.

커밋은 메소드 반환과 다르다


대상 메소드가 정상적으로 끝나도 최종 커밋까지 성공했다는 뜻은 아니다.

메소드 정상 반환
→ flush
→ 데이터베이스 commit
→ 리소스 정리

flush나 실제 커밋 단계에서 오류가 발생할 수 있으므로, 메소드 반환과 트랜잭션 완료를 같은 시점으로 보면 안 된다.

Conclusion


프록시 기반 @Transactional은 메소드의 호출 경로가 중요하다.

  • 내부 호출과 private 메소드는 프록시를 거치지 않는다.
  • REQUIRED는 기존 트랜잭션의 리소스에 참여한다.
  • 실제 커밋은 바깥 트랜잭션의 종료 시점에 수행된다.

@Transactional은 어노테이션보다 프록시를 통과하는 호출 구조가 중요하다.

Reference