1. 개요
왜 이전에 스프링은 Runtime Exception을 상속받은 Unchecked Exception을 트랜잭션을 롤백하도록 구현하였을까요? 그건 바로 이전에 이야기했듯이 Unchecked Exception은 시스템적으로 처리를 할 수 없는 경우에 사용하기 때문입니다. 아래의 공식 문서 내용을 살펴봅시다.
롤백 기본 규칙 (Default Rollback Policy)
아래의 스프링의 공식문서를 살펴보면 Runtime Exception의 서브 클래스나 Error의 서브 클래스를 롤백을 지원하고 있다고 합니다. 이런 이유에 대해서는 자바의 Exception 철학 중 하나인 처리가 가능한 에러와 처리가 불가능한 에러를 분리하여 관리하기 때문입니다.
- 자바 예외 철학과의 연결: 자바의 "처리가 가능한 에러(Checked)"와 "처리가 불가능한 에러(Unchecked)"를 분리하는 철학을 프로그래밍 세계에 녹여낸 결과입니다.
- 예시:
- 롤백 대상 (Unchecked): 데드락 발생, 유니크 제약 조건 위반 등 애플리케이션 바깥(운영자/코드 수정)에서 해결해야 하는 시스템적 장애.
- 커밋 대상 (Checked): 네트워크 일시 오류나 락 점유 실패처럼 런타임 환경에서 재시도(Retry)하거나 폴백(Fallback)을 통해 충분히 핸들링할 수 있다고 판단되는 상황.
- 요약:
- Unchecked Exception (Runtime, Error): 롤백 수행.
- Checked Exception (Exception 하위): 커밋 수행.

2. RuntimeException을 활용한 에러 관리
Exception을 구조화하여 처리하는 방식은 다양한 방식이 있는데 Runtime Exception을 상속받아 BusinessException을 만들어서 커스텀 Exception을 만드는 형태로 구조를 분리하거나 Enum을 활용하여 관리하는 방법이 있습니다. Exception은 개발팀의 방향이나 프로젝트의 방향성에 따라 맞다고 생각하는 방식으로 구조화를 하시면 됩니다.
Custom Exception을 활용한 방법
public abstract class BusinessException extends RuntimeException {
private final String code;
private final HttpStatus status;
}
public class DuplicateEmailException extends BusinessException {
public DuplicateEmailException() {
super("AUTH_001", HttpStatus.BAD_REQUEST, "중복된 이메일입니다.");
}
}
Enum을 활용한 Exception 관리
public enum ErrorCode {
DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "AUTH_001", "중복된 이메일입니다."),
ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "ORDER_001", "주문을 찾을 수 없습니다."),
INVALID_ORDER_STATE(HttpStatus.CONFLICT, "ORDER_002", "해당 상태에서는 취소할 수 없습니다.");
private final HttpStatus status;
private final String code;
private final String message;
}
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public BusinessException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
}
}
주의해야 하는 Exception 핸들링과 롤백
중요한 건 발생한 Exception을 어떻게 핸들링하느냐 입니다. Exception의 구조화 또한 부채가 되어 문제를 일으킬 수 있지만 잘못된 Exception 핸들링으로 인해 발생하는 기술부채는 실질적으로 금전적 피해를 만들거나 데이터의 정합성이 깨트리는 크리티컬한 문제를 일으킬 수 있습니다.
@Service
public class OrderService {
@Transactional
public void createOrder(OrderRequest request) {
orderRepository.save(new Order(request)); // 1. 주문 저장
try {
paymentService.pay(request.getAmount()); // 2. 결제 실패!
} catch (PaymentException e) {
log.error("결제 실패: {}", e.getMessage()); // 로그만 찍고 넘김
}
// 3. 예외가 밖으로 안 나감 → 트랜잭션 매니저는 정상으로 인식 → 커밋!
// 결과: 결제 안 됐는데 주문은 저장됨
}
}
해결: 예외를 다시 던지거나 명시적 롤백
@Transactional
public void createOrder(OrderRequest request) {
orderRepository.save(new Order(request));
try {
paymentService.pay(request.getAmount());
} catch (PaymentException e) {
log.error("결제 실패: {}", e.getMessage());
throw e; // 예외를 다시 던져서 롤백 유도
}
}
case 2: Checked Exception은 롤백 안 됨
// Checked Exception 정의
public class InsufficientBalanceException extends Exception { // Exception 상속
public InsufficientBalanceException(String message) {
super(message);
}
}
@Service
public class PaymentService {
@Transactional
public void withdraw(Long userId, int amount) throws InsufficientBalanceException {
Account account = accountRepository.findByUserId(userId);
account.decreaseBalance(amount); // 1. 잔액 감소
if (account.getBalance() < 0) {
throw new InsufficientBalanceException("잔액 부족"); // 2. 예외 발생
}
// 3. Checked Exception → 롤백 안 됨 → 마이너스 잔액 커밋!
}
}
해결: rollbackFor 명시
@Transactional(rollbackFor = InsufficientBalanceException.class)
public void withdraw(Long userId, int amount) throws InsufficientBalanceException {
Account account = accountRepository.findByUserId(userId);
account.decreaseBalance(amount);
if (account.getBalance() < 0) {
throw new InsufficientBalanceException("잔액 부족"); // 이제 롤백됨
}
}
3. 정리
비즈니스 예외도 가급적 RuntimeException을 상속받아 롤백을 유도하는 것이 좋습니다. 롤백이 필요하지 않은 예외는 rollbackFor / noRollbackFor / rollbackForClassName / noRollbackForClassName으로 제어할 수 있기 때문에 적극적으로 활용하여 설계하시면 좋습니다.
예외처리는 가급적으로 전역 핸들러(@ControllerAdvice)에 맡기고 서비스에서는 던지도록 하는 것이 좋습니다. 에러 응답 코드를 만들어서 내뱉는 건 전역 컨트롤러로 핸들링 하는 것만으로 충분합니다. 지금까지 스프링에서 Exception을 활용하는 방법에 대해 알아보았습니다.
'spring' 카테고리의 다른 글
| 토비의 스프링 정복하기 10편 - 비즈니스 로직을 기술로부터 독립시키기 (0) | 2026.02.07 |
|---|---|
| 토비의 스프링 정복하기 9편 - 서비스의 추상화 (0) | 2026.02.03 |
| 토비의 스프링 7편 - 스프링 예외 처리의 철학 (1) | 2026.01.28 |
| 토비의 스프링 정복하기 6편 - 제어의 역전 (0) | 2026.01.25 |
| 토비의 스프링 정복하기 5편 - 변하는 것과 변하지 않는 것 (0) | 2026.01.21 |