문제 발생

checkout() 안에서 savePayment()를 호출했는데 savePayment()@Transactional이 기대한 새 트랜잭션 경계를 만들지 않습니다. annotation이 붙었다는 사실만으로 모든 Java 메서드 호출이 가로채지는 것은 아닙니다.

proxy 기반 @Transactional은 같은 instance 내부 호출이 proxy를 통과하지 않으면 새 transaction advice가 적용되지 않을 수 있습니다.

원인 분석

Spring의 기본 proxy mode에서는 외부 호출이 proxy를 통과할 때 transaction interceptor가 실행됩니다. this.savePayment()처럼 target 객체 내부에서 자기 메서드를 호출하면 proxy를 거치지 않습니다. 먼저 실제 bean class, proxy mode, 호출 방향과 현재 transaction 활성 여부를 확인합니다. private 메서드에 annotation을 옮기거나 self-injection을 추가해 구조 문제를 가리는 방식은 우선 해법이 아닙니다.

해결 방안

트랜잭션 경계를 다른 Spring bean의 public 메서드로 분리해 호출이 proxy를 지나게 합니다.

@Service
class CheckoutService {
    private final PaymentTransaction paymentTransaction;

    CheckoutService(PaymentTransaction paymentTransaction) {
        this.paymentTransaction = paymentTransaction;
    }

    void checkout(Order order) {
        paymentTransaction.save(order);
    }
}

@Service
class PaymentTransaction {
    @Transactional
    public void save(Order order) {
        // DB 변경
    }
}

실패를 의도적으로 발생시켜 rollback 여부를 integration test로 확인합니다. propagation과 rollback rule은 별도 계약이므로 self-invocation 수정과 한꺼번에 바꾸지 않습니다.

공식 문서

현재 Spring Framework version, proxy mode와 실제 bean 호출 경계에서 확인합니다.