Spring @Transactional 3: 프록시 방식의 주의사항
TOC
- Overview
- 케이스 1: 내부 호출할 때
- 케이스 2: 접근 제한자를 사용할 때
- 케이스 3: 기존 트랜잭션에 참여할 때
- 커밋은 메소드 반환과 다르다
- Conclusion
- 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은 어노테이션보다 프록시를 통과하는 호출 구조가 중요하다.