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();
즉, 트랜잭션이 안 먹은 게 아니라, 애초에 트랜잭션 로직이 실행되지 않은 것이다.
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(); // 프록시 경유
}
}
이렇게 하면 프록시를 거치기 때문에 트랜잭션이 정상 동작한다.
@Transactional
private void save() {}
동작하지 않는다.
프록시가 가로챌 수 없기 때문이다.
@Transactional
public void save() {
try {
throw new RuntimeException();
} catch (Exception e) {
// 아무것도 안 함
}
}
예외가 밖으로 던져지지 않으면 롤백되지 않는다.
@Transactional(rollbackFor = Exception.class)
Checked Exception은 기본적으로 롤백되지 않는다.
명시적으로 설정해야 한다.
Spring에서 @Transactional이 안 먹는 대표적인 이유는 다음과 같다.
그중 가장 많이 터지는 원인은 Self Invocation이다.
@Transactional이 안 먹는 게 아니라 프록시를 타지 못하고 있었던 것이다.
트랜잭션 문제는 코드 한 줄의 문제가 아니라 호출 구조의 문제인 경우가 많다.
실무에서 한 번쯤은 반드시 겪게 되는 이슈다.
같은 삽질을 반복하지 않기를.