예외를 던졌는데 트랜잭션이 커밋된 이유
문제 발생
결제 처리에서 예외를 던졌는데 그 앞의 INSERT가 그대로 커밋돼 있었습니다.
@Transactional
public void pay(Order order) throws PaymentFailedException { // 체크 예외
paymentRepository.save(record);
if (!gateway.charge(order)) {
throw new PaymentFailedException("결제 실패"); // 롤백되지 않는다
}
}결제는 실패했는데 결제 기록만 남는, 가장 곤란한 종류의 불일치가 생겼습니다.
원인 분석
기본 롤백 규칙이 예외의 종류를 봅니다. Spring 문서가 그대로 적습니다 — 기본 설정에서 Spring Framework의 트랜잭션 인프라 코드는 런타임 언체크 예외의 경우에만 트랜잭션을 롤백 대상으로 표시한다. 즉 던져진 예외가 RuntimeException의 인스턴스이거나 그 하위 클래스일 때다. (Error 인스턴스 역시 기본적으로 롤백을 유발한다.)
체크 예외는 여기에 없습니다. Java의 체크 예외는 "호출자가 처리할 것으로 기대되는, 복구 가능한 상황"이라는 의미이므로, 프레임워크는 그것을 자동으로 실패로 간주하지 않습니다. 설계상의 선택이고, 알고 쓰면 유용하지만 모르고 쓰면 조용히 데이터를 깨뜨립니다.
해결 방안
- 롤백해야 하는 체크 예외를 명시합니다.
@Transactional(rollbackFor = PaymentFailedException.class)
public void pay(Order order) throws PaymentFailedException { ... }- 반대 방향도 있습니다. 롤백하면 안 되는 런타임 예외는
noRollbackFor로 제외합니다 — 문서가 함께 제시하는 옵션입니다.
@Transactional(noRollbackFor = InstrumentNotFoundException.class)-
도메인 실패를 언체크 예외로 통일하는 방법도 있습니다. 팀의 규약을 "비즈니스 실패는
RuntimeException계열"로 정하면 이 함정 자체가 사라집니다. 대신 그 규약을 문서와 리뷰에서 지켜야 합니다. -
프로그래밍 방식은 최후 수단으로 둡니다. 문서도 선언적 방식을 강하게 권합니다. 정말 필요하면 이렇게 표시합니다.
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();-
테스트로 고정합니다. "예외가 났을 때 행이 남아 있지 않은가"는 단위 테스트로 확인 가능하고, 이 종류의 회귀는 코드 리뷰로 잡기 어렵습니다.
-
catch로 삼키지 않았는지 함께 봅니다. 트랜잭션 메서드 안에서 예외를 잡아 로깅만 하면 롤백 규칙에 도달하지도 못합니다 — 같은 증상, 다른 원인입니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.