본문 바로가기

카테고리 없음

예외 처리 전략

 

현대의 소프트웨어 시스템은 동기·비동기, 단일·분산 아키텍처 등 다양한 형태로 구성됩니다. 이러한 환경에서 예외 처리 전략을 잘 수립하는 것은 시스템 안정성과 유지보수성을 결정짓는 핵심 요소입니다.

 

이 글에서는 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. 특수값(-1, null) 대신 예외 사용
  2. 예외 메시지에 입력값, 컨텍스트, 실패 원인 포함
  3. 레이어별 책임에 맞는 예외만 던짐 (Service에서 HTTP 금지)
  4. 외부 예외 감싸기: ExternalSystemException
  5. 최상위(ControllerAdvice)에서 처리해 표현 계층 분리

 

결론

  • Service Layer는 “왜 실패했는가”만 정의합니다. (비즈니스 의미)
  • 표현 계층(ControllerAdvice)은 “어떻게 응답할 것인가”를 담당합니다. (HTTP·메시지)
  • 비동기 환경에서는 보상·재시도·로그 전략을 반드시 설계하세요.

예외 처리 전략은 시스템 전반의 견고함을 좌우하는 가장 섬세한 도구입니다.