현대의 소프트웨어 시스템은 동기·비동기, 단일·분산 아키텍처 등 다양한 형태로 구성됩니다. 이러한 환경에서 예외 처리 전략을 잘 수립하는 것은 시스템 안정성과 유지보수성을 결정짓는 핵심 요소입니다.
이 글에서는 Service Layer(비즈니스 로직 계층)에서 예외를 어떻게 설계하고 던질지, 그리고 REST API, 비동기(이벤트 기반) 아키텍처에서 예외를 어떻게 표현/처리할지를 단계별로 정리합니다.
1. 예외 계층 구조 설계
1.1 예외 분류 예시
RuntimeException
├── BusinessException // 도메인 비즈니스 규칙 위반
├── ValidationException // 입력값 검증 실패
├── AuthenticationException // 인증 실패
├── AuthorizationException // 권한 없음
├── ResourceNotFoundException // 도메인 리소스 없음
├── ConflictException // 상태 충돌, 중복 요청
└── ExternalSystemException // 외부 API·DB·Kafka 통신 오류
각 예외는 책임과 의도를 반영하는 메시지를 명확히 갖도록 분리합니다.
1.2 예외 클래스 예시
public class ProductSizeValidationException extends ValidationException {
public ProductSizeValidationException(String message) {
super(message);
}
}
2. Service Layer에서의 예외 처리 원칙
2.1 비즈니스 규칙 위반만 던진다
- 검증 실패: ValidationException (ex. InvalidEmailException)
- 재고 부족: BusinessException (ex. OutOfStockException)
- 리소스 없음: ResourceNotFoundException
- HTTP 상태(400, 404 등)는 여기서 다루지 않습니다.
- → HTTP 개념은 표현 계층에서 전환합니다.
2.2 외부 의존(Infra) 예외는 포장해서 던진다
try {
paymentClient.charge(order);
} catch (HttpTimeoutException e) {
throw new ExternalSystemException("결제 시스템에 연결할 수 없습니다.", e);
}
외부 라이브러리나 API의 구체 예외를 도메인 의미의 예외로 감싸야, 추후 교체·테스트가 용이해집니다.
3. REST API 표현 계층 예외 처리
3.1 @ControllerAdvice 활용
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidation(ValidationException ex) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(ex.getMessage()));
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("내부 오류가 발생했습니다."));
}
}
3.2 공통 응답 포맷
public record ErrorResponse(String message) {}
- 응답 형식 통일
- 메시지와 HTTP 상태 코드 분리
4. 비동기·이벤트 기반 예외 처리 전략
비동기 환경에서는 즉시 HTTP 응답을 줄 수 없으므로, 실패 이벤트를 다른 방식으로 다룹니다.
전략설명
보상 트랜잭션 | 실패 정보를 별도 토픽/테이블에 기록하고, 후속 보상 로직 수행 |
재시도 큐 | 실패 메시지를 Retry 토픽 또는 Dead Letter Queue에 전송 |
경고만 로그 | 사용자 영향이 크지 않은 경우, 로그만 남기고 처리를 계속 진행 |
4.1 보상 처리 큐 예시
try {
orderService.create(orderEvent);
} catch (InsufficientStockException e) {
compensationProducer.send("order-failure", orderEvent);
}
- 실패 이벤트는 보상 프로세스에서 별도 처리
- 원본 메시지는 재처리를 방지하도록 Dead Letter에 배치
5. 예외 처리의 핵심 원칙 추가 요약
- 특수값(-1, null) 대신 예외 사용
- 예외 메시지에 입력값, 컨텍스트, 실패 원인 포함
- 레이어별 책임에 맞는 예외만 던짐 (Service에서 HTTP 금지)
- 외부 예외 감싸기: ExternalSystemException 등
- 최상위(ControllerAdvice)에서 처리해 표현 계층 분리
결론
- Service Layer는 “왜 실패했는가”만 정의합니다. (비즈니스 의미)
- 표현 계층(ControllerAdvice)은 “어떻게 응답할 것인가”를 담당합니다. (HTTP·메시지)
- 비동기 환경에서는 보상·재시도·로그 전략을 반드시 설계하세요.
예외 처리 전략은 시스템 전반의 견고함을 좌우하는 가장 섬세한 도구입니다.