@Transactional(rollbackFor = Exception.class)은 뭘까?
어느 날처럼 @Transactional 어노테이션을 쓰다가 @Transactional(rollbackFor = Exception.class)이 사용되고 있는 것을 발견했다.
트랜잭션은 메서드 실행 중 Exception 클래스나 그 하위 예외가 발생하면 트랜잭션을 롤백한다.
Spring의 기본 동작은 RuntimeException이나 Error만 롤백 대상으로 한다.
하지만, rollbackFor = Exception.class 를 명시하면 모든 예외에 대해 롤백이 수행된다.
예시를 들어보자
@Transactional(rollbackFor = Exception.class)
public void transferMoney(String from, String to, int amount) throws Exception {
// 1. A 계좌에서 돈 차감
accountService.withdraw(from, amount);
// 2. 만약 여기서 IOException 같은 checked exception 발생
fileService.logTransaction(); // IOException 발생 가능
// 3. B 계좌에 돈 추가
accountService.deposit(to, amount);
}
rollbackFor = Exception.class 이
없다면 → IOException이 발생해도 롤백되지 않아 A 계좌만 차감된 상태로 남는다.
있다면 → IOException 발생 시 전체 트랜잭션이 롤백되어 A 계좌 차감도 취소된다.
데이터 무결성을 보장하기 위해 모든 예외 상황에서 롤백을 원할 때 사용하는 안전한 설정이다.
그런데 드는 궁금점 !
그럼 그냥 디폴트 값이 (rollbackFor = Exception.class) 포함이면 안 되나? 왜 불편하게 RuntimeException과 Error만 잡지?
Spring은 Checked Exception(IOException, SQLException 등)은 복구 가능한 예외로 간주하고, RuntimeException은 프로그래밍 오류로 간주한다.
@Transactional
public void processOrder(Order order) throws IOException {
// 1. 주문 데이터 DB 저장 (성공)
orderRepository.save(order);
// 2. 이메일 발송 시도 (IOException 발생 - 메일 서버 일시 장애)
emailService.sendConfirmation(order.getEmail()); // IOException!
// 3. 재고 업데이트 (실행 안됨)
inventoryService.updateStock(order.getProductId());
}
기본 정책 (RuntimeException만 롤백한다면) :
- 이메일 발송 실패 시에도 주문 데이터는 DB에 남아있음
- 나중에 이메일을 다시 발송하면 됨
- 비즈니스 핵심 로직(주문)은 보호됨
만약 모든 Exception에서 롤백한다면 :
- 이메일 서버 장애로 전체 주문이 취소됨
- 고객 입장에서는 주문이 사라짐
- 사소한 부가 기능 때문에 핵심 비즈니스가 실패
그럼 언제 rollbackFor = Exception.class을 써야할까?
첫 번째로 봤던 예제 코드처럼 입출금이 실행되는 금융 시스템처럼 모든 단계가 필수이고, 부분 실패가 허용되지 않는 경우에만 사용한다.
결론적으로 핵심 비즈니스 로직은 보호하되, 부가 기능의 실패로 전체가 망가지지 않게 하자는 접근이라고 보면 될 것 같다.
이걸 공부하고 나니 예전에 내가 겪었던 스타벅스 어플 오류가 생각난다.
내가 사이렌 오더를 하기 위해 스타벅스 어플로 음료를 주문했는데 갑자기 렉이 걸리더니 로딩이 아주 오래 이어졌다.
그러더니 몇 분 후 주문 실패가 뜨는 것이다. 그렇지만 결제는 이미 되어 카드에서 돈이 나간 상태였다.
그렇지만 내 주문내역에는 방금 주문한 주문 건이 보이지 않았고, 점원분께 가서 내 주문이 들어갔는지 여쭤보았지만 들어오지 않았다고 하셨다.
결국 약 20분 정도를 헤매다, 스타벅스 어플 중 “히스토리”라는 페이지에 들어가보니 주문이 실패된 내 결제 내역이 존재했고,
그곳에서 다시 주문하기를 눌러 가까스로 주문을 할 수 있었다.
아무래도 rollbackFor = Exception.class을 쓰지 않고 @Transactional 만 적혀있어서 그게 가능했던 것 같다.
그렇지만 고객 입장에서는 꽤나 당황스러울 수 있는 상황이었다. 참고로 직원분도 어떻게 해결해야하는지를 모르셔서 고객센터에 문의를 해야하나 고민했었다.
내가 이런 말 하는 건 웃기지만…
스타벅스 어플 중 주문/결제가 가장 핵심 기능인 만큼, 개발자들이 더욱 신경 써서 핵심 비즈니스 로직과 부가 기능의 로직을 분리하고,
예외 처리를 더하여 어떤 이유 때문에 주문에 문제가 생겼음을 고지해주고,
결제까지 완료가 되었지만 예외가 발생된 상황에서는 히스토리로 가서 확인해달라는 안내 메시지가 보이게 해준다면 편리할 것 같다.
나말고 다른 사용자들도 충분히 겪을 수 있는 상황이기에 이를 반영해준다면 정말 좋을 것 같다.