Spring @Transactional이 안 먹는 이유 (self-invocation 때문에 2시간 날린 썰)

fever·2026년 2월 20일

개발고찰

목록 보기
3/6

Spring에서 @Transactional을 붙였는데
분명 예외가 발생했는데도 롤백이 되지 않는 경험을 한 적이 있다.

처음에는 MyBatis 문제인가 싶었고,
DB 설정 문제인가 싶었고,
격하게 삽질을 했다.

결론은 단순했다.

트랜잭션이 안 먹은 게 아니라, 프록시를 타지 않았다.

문제 상황

다음과 같은 코드였다. (예시 코드)

@Component
@RequiredArgsConstructor
public class OrderApplication {

    private final OrderService orderService;
    private final OrderResultPublisher publisher;

    public void orderGenerateWithPublish(OrderVo orderVo) {
        OrderMessage message = generateOrder(orderVo);
        publisher.send(message);
    }

    @Transactional
    public OrderMessage generateOrder(OrderVo orderVo) {

        if (orderService.existsByOrderNumber(orderVo.getOrderNumber())) {
            orderService.deleteByOrderNumber(orderVo.getOrderNumber());
        }

        Order order = orderService.save(orderVo.toEntity());

        // 강제 예외
        if (true) {
            throw new RuntimeException("예외 발생");
        }

        return OrderMessage.from(order);
    }
}

의도는 단순했다.

  • 주문 저장
  • 예외 발생 시 롤백
  • 트랜잭션 보장

하지만 실제로는
삭제도 되고 insert도 되고 예외도 터지는데 롤백이 되지 않았다.

원인

문제는 이 한 줄이었다.

OrderMessage message = generateOrder(orderVo);

같은 클래스 내부에서 this 로 메서드를 호출했다.
이게 왜 문제일까?

Spring 트랜잭션은 프록시 기반이다.

Spring의 @Transactional은 AOP 기반으로 동작한다.

우리가 주입받는 Bean은 실제 객체가 아니라
트랜잭션 처리를 감싸고 있는 프록시 객체다.

구조를 단순화하면 다음과 같다.

[Proxy 객체] → [실제 Bean]

메서드를 호출하면 프록시가 먼저 실행되고
여기서 트랜잭션을 시작한다.

하지만 같은 클래스 내부에서 다른 메서드를 호출하면,

generateOrder();
  • 프록시를 거치지 않는다
  • 그냥 자기 자신 메서드를 호출한다
  • 트랜잭션 AOP가 실행되지 않는다

즉, 트랜잭션이 안 먹은 게 아니라, 애초에 트랜잭션 로직이 실행되지 않은 것이다.

트랜잭션이 동작하지 않는 구조

public void outer() {
    this.inner(); // 프록시 안 거침
}

@Transactional
public void inner() {
}

이 구조에서는 @Transactional이 적용되지 않는다.

정상 동작 구조

다른 Bean을 통해 호출해야 한다.

@Service
public class OrderTxService {

    @Transactional
    public void generate() {
        // 트랜잭션 보장
    }
}
@Component
@RequiredArgsConstructor
public class OrderApplication {

    private final OrderTxService orderTxService;

    public void outer() {
        orderTxService.generate(); // 프록시 경유
    }
}

이렇게 하면 프록시를 거치기 때문에 트랜잭션이 정상 동작한다.

추가로 많이 헷갈리는 포인트

1. private과 @Transactional

@Transactional
private void save() {}

동작하지 않는다.
프록시가 가로챌 수 없기 때문이다.

2. 예외를 try-catch로 삼켜버린 경우

@Transactional
public void save() {
    try {
        throw new RuntimeException();
    } catch (Exception e) {
        // 아무것도 안 함
    }
}

예외가 밖으로 던져지지 않으면 롤백되지 않는다.

3. Checked Exception

@Transactional(rollbackFor = Exception.class)

Checked Exception은 기본적으로 롤백되지 않는다.
명시적으로 설정해야 한다.

정리 및 마무리

Spring에서 @Transactional이 안 먹는 대표적인 이유는 다음과 같다.

  1. 같은 클래스 내부 호출 (Self Invocation)
  2. private 메서드
  3. 예외를 삼켜버림
  4. Checked Exception

그중 가장 많이 터지는 원인은 Self Invocation이다.

@Transactional이 안 먹는 게 아니라 프록시를 타지 못하고 있었던 것이다.
트랜잭션 문제는 코드 한 줄의 문제가 아니라 호출 구조의 문제인 경우가 많다.
실무에서 한 번쯤은 반드시 겪게 되는 이슈다.

같은 삽질을 반복하지 않기를.

profile
선명한 삶을 살기 위하여

0개의 댓글