
Transactional self invocation은 Spring에서 정말 자주 만나는 함정입니다. 메서드에 @Transactional을 붙였는데도 트랜잭션이 걸리지 않는 것처럼 보이는 상황이 생깁니다.
결론부터 말하면 같은 클래스 내부에서 this로 호출하면 Spring AOP proxy를 지나지 않는다는 것이 핵심입니다. 트랜잭션은 어노테이션만으로 생기는 것이 아니라, 프록시가 메서드 호출을 가로챌 때 적용됩니다.
@Transactional self-invocation 판단 기준

Spring AOP proxy를 먼저 이해해야 한다
Spring Framework의 AOP proxy 문서는 Spring AOP가 proxy 기반이라고 설명합니다. 클라이언트가 proxy를 통해 메서드를 호출하면 advice가 실행될 수 있습니다.
하지만 target 객체 내부에서 자기 자신의 다른 메서드를 호출하면 이야기가 달라집니다. 그 호출은 proxy가 아니라 this 참조를 향한 직접 호출입니다.
@Service
public class OrderService {
public void placeOrder(OrderRequest request) {
// 같은 클래스 내부 호출
saveOrder(request);
}
@Transactional
public void saveOrder(OrderRequest request) {
// 트랜잭션이 필요하다고 기대한 로직
}
}왜 @Transactional이 있는데도 적용되지 않을까
Spring의 선언적 트랜잭션은 AOP advice로 적용됩니다. 외부 bean이 orderService.saveOrder()를 호출하면 proxy를 거칠 수 있지만, placeOrder() 내부의 saveOrder() 호출은 proxy 밖에서 일어납니다.
Spring 문서는 self invocation이 advice 실행 기회를 주지 않고 bypass한다고 설명합니다. 그래서 rollback, propagation, readOnly 같은 트랜잭션 설정이 기대와 다르게 보일 수 있습니다.
트랜잭션 경계는 JPA 동작과도 이어집니다. JPA 영속성 컨텍스트 글에서 다룬 1차 캐시와 변경 감지도 결국 트랜잭션 경계 안에서 더 의미가 분명해집니다.
해법 1: 책임을 다른 서비스로 분리한다
가장 무난한 해법은 트랜잭션 경계를 별도 bean의 public method로 분리하는 것입니다. 그러면 호출이 Spring bean proxy를 지나가고 트랜잭션 advice가 적용될 수 있습니다.
@Service
public class OrderFacade {
private final OrderWriter orderWriter;
public void placeOrder(OrderRequest request) {
orderWriter.saveOrder(request);
}
}
@Service
public class OrderWriter {
@Transactional
public void saveOrder(OrderRequest request) {
// DB 변경 로직
}
}해법 2: 트랜잭션 경계를 위로 올린다
내부 호출 전체가 하나의 업무 단위라면, 내부 메서드가 아니라 진입 메서드에 @Transactional을 붙이는 방법이 더 자연스럽습니다. 트랜잭션 경계는 보통 ‘DB 작업 한 줄’보다 ‘하나의 유스케이스’에 가까운 경우가 많습니다.
@Transactional
public void placeOrder(OrderRequest request) {
validate(request);
saveOrder(request);
publishEvent(request);
}피해야 할 해법: AopContext.currentProxy 남용
Spring 문서는 AopContext.currentProxy() 사용을 마지막 수단으로 소개하지만, 강하게 결합되는 방식이라 신중해야 합니다. 코드가 Spring AOP를 직접 알게 되고 테스트와 구조도 지저분해질 수 있습니다.
점검 체크리스트
- @Transactional 메서드가 외부 bean에서 호출되는가?
- 같은 클래스의 this 호출로 들어가는 구조는 아닌가?
- private 메서드나 final 메서드에 기대를 걸고 있지는 않은가?
- 트랜잭션 경계가 유스케이스 단위로 잡혀 있는가?
- self injection이나 AopContext보다 서비스 분리가 더 자연스럽지 않은가?
정리
@Transactional self invocation 문제는 Spring이 트랜잭션을 못 거는 문제가 아닙니다. 호출 경로가 proxy를 지나지 않아서 트랜잭션 advice가 실행되지 않는 문제입니다. 해결은 어노테이션을 더 붙이는 것이 아니라 트랜잭션 경계와 서비스 책임을 다시 나누는 데서 시작해야 합니다.