@Transactional을 붙였는데 롤백이 안 되는 이유
문제 발생
비즈니스 예외를 던졌는데 DB에 반영된 데이터가 롤백되지 않고 그대로 커밋된 경우가 있었습니다.
@Transactional
public void placeOrder(Order order) throws InsufficientStockException {
orderRepository.save(order);
if (stock < order.getQuantity()) {
throw new InsufficientStockException(); // 체크 예외
}
}원인 분석
Spring의 @Transactional은 기본적으로 RuntimeException(및 Error)이 발생했을 때만 롤백합니다. InsufficientStockException처럼 Exception을 직접 상속한 체크 예외(checked exception)는 기본 설정으로는 롤백을 유발하지 않고 트랜잭션이 그대로 커밋됩니다. 이건 실수가 아니라 Spring의 의도된 기본 동작입니다 — EJB의 관례를 따른 것으로, 체크 예외는 "비즈니스적으로 예상 가능한 상황"으로 간주해 커밋하는 것을 기본값으로 삼았습니다.
해결 방안
- 체크 예외에서도 롤백이 필요하면
rollbackFor를 명시합니다.
@Transactional(rollbackFor = InsufficientStockException.class)
public void placeOrder(Order order) throws InsufficientStockException {
// ...
}- 가능하면 비즈니스 예외를
RuntimeException기반으로 만들어 기본 롤백 규칙에 맞춥니다 — 대부분의 Spring 프로젝트가 이 방식을 씁니다. - **Propagation(전파 속성)**도 자주 헷갈리는 부분입니다. 기본값
REQUIRED는 이미 트랜잭션이 있으면 참여하고 없으면 새로 만듭니다. 다른 서비스의@Transactional메서드를 호출했을 때 "독립적으로 커밋/롤백되길" 기대한다면REQUIRES_NEW를 명시해야 합니다 —REQUIRED(기본값)는 호출자의 트랜잭션에 합쳐지므로, 호출자가 나중에 롤백하면 이미 호출한 메서드의 작업도 함께 롤백됩니다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAudit(String message) {
// 바깥 트랜잭션이 롤백되어도 이 로그는 별도로 커밋되길 원할 때
}- 자기 자신 호출(self-invocation) 문제도 함께 확인해야 합니다 — 같은 클래스 안에서
this.otherTransactionalMethod()로 호출하면 Spring AOP 프록시를 거치지 않아@Transactional이 아예 적용되지 않습니다. 이 경우 메서드를 별도 빈으로 분리해야 합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.