본문 바로가기
Spring/Common

[Spring] 효율적인 예외 처리: GlobalExceptionHandler로 사용자 경험과 운영 효율성 잡기

by coding_whale 2026. 4. 20.
반응형

개발을 하다 보면 수많은 예외(Exception)를 마주하게 된다. 하지만 모든 컨트롤러에서 try-catch를 남발한다면 코드는 지저분해지고 유지보수는 어려워진다. 오늘은 예외를 한 곳에서 처리해야 하는 이유더 견고한 애플리케이션을 위한 실무적인 예외 처리 전략을 정리해 보았다.

1. 예외 처리 흐름도 (Visual Flow)

예외가 발생했을 때 애플리케이션 내부에서 어떤 경로로 처리되는지 시각화하면 다음과 같습니다.

[동작 순서]

  1. Client: API 요청 전송.
  2. Controller: 비즈니스 로직 수행 중 예외 발생 (throw).
  3. GlobalExceptionHandler: @RestControllerAdvice가 예외를 인터셉트.
  4. Server Log: 상세한 에러 정보(Trace ID, Stack Trace)를 로그에 기록.
  5. Client Response: 정제된 에러 메시지와 상태 코드를 JSON으로 반환.

 

2. 예외를 한 곳에서 처리하는 이유 (Why?)

가장 큰 이유는 일관성관심사의 분리입니다.

  • 중복 제거: 컨트롤러마다 반복되는 예외 처리 로직을 제거할 수 있습니다.
  • 일관된 응답 구조: 에러 발생 시 사용자에게 항상 동일한 포맷의 JSON 응답을 내려줄 수 있습니다.
  • 비즈니스 로직 집중: 컨트롤러는 본연의 기능에만 집중하고, 예외 처리는 전담 클래스(GlobalExceptionHandler)에게 맡깁니다.

 

3. 실무형 개선 포인트 반영: 구조화된 예외 처리

단순히 예외를 잡는 것을 넘어, 확장성 있게 설계하는 것이 중요합니다. 아래 코드는 에러 코드의 Enum화데이터 검증 예외 처리를 반영한 모습입니다.

① 에러 코드 정의 (ErrorCode Enum)

상태 코드와 메시지를 별도로 관리하여 코드의 가독성을 높입니다.

@Getter
@AllArgsConstructor
public enum ErrorCode {
    INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "C001", "입력 값이 올바르지 않습니다."),
    ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "C002", "찾을 수 없는 리소스입니다."),
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S001", "서버 내부에 오류가 발생했습니다.");

    private final HttpStatus status;
    private final String code;
    private final String message;
}

② GlobalExceptionHandler 구현

개선된 포인트를 적용하여 더 정밀하게 예외를 처리합니다.

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * @Valid 검증 실패 시 발생하는 예외 처리
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<Void>> handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpServletRequest request) {
        logClientError(ErrorCode.INVALID_INPUT_VALUE.getCode(), request, e);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.error(ErrorCode.INVALID_INPUT_VALUE.getCode(), "검증에 실패한 필드가 있습니다."));
    }

    /**
     * 비즈니스 로직 중 발생하는 잘못된 인자 예외 처리
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ApiResponse<Void>> handleIllegalArgument(IllegalArgumentException e, HttpServletRequest request) {
        logClientError(ErrorCode.INVALID_INPUT_VALUE.getCode(), request, e);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ApiResponse.error(ErrorCode.INVALID_INPUT_VALUE.getCode(), ErrorCode.INVALID_INPUT_VALUE.getMessage()));
    }

    // 서버 로그 기록 (Observability 전략)
    private void logClientError(String code, HttpServletRequest request, Exception e) {
        log.warn("[CLIENT ERROR] code={} endpoint={} method={} message={}",
                code, request.getRequestURI(), request.getMethod(), e.getMessage());
    }
}

 

4. 서버를 위한 전략: Observability (관측 가능성)

에러 발생 시 "어디서, 왜" 발생했는지 모르면 운영 단계에서 대응이 불가능합니다.

  • Trace ID 사용: 분산 환경에서 요청의 시작부터 끝까지를 식별할 수 있는 ID를 로그에 함께 남깁니다.
  • Sanitization (오염 제거): 내부 DB 에러나 라이브러리 메시지를 사용자에게 그대로 노출하지 마세요. 이는 보안 사고로 이어질 수 있습니다. 사용자에게는 "시스템 오류가 발생했습니다"라고 말하고, 서버 로그에는 정확한 원인과 스택 트레이스를 남겨야 합니다.

 

5. 요약 및 결론

  1. 중앙 제어: @RestControllerAdvice로 예외 처리 창구를 단일화하세요.
  2. 구조화: ErrorCode Enum을 사용하여 에러 메시지와 상태 코드를 관리하세요.
  3. 검증: MethodArgumentNotValidException 등을 처리하여 클라이언트에게 친절한 가이드를 제공하세요.
  4. 보안: 사용자에게 노출되는 메시지와 개발자가 보는 로그를 철저히 분리하세요.

예외 처리는 단순한 에러 막기가 아니라, 서비스의 안정성과 보안을 결정짓는 중요한 설계입니다. 여러분의 프로젝트에도 견고한 예외 처리 시스템을 구축해 보세요!

반응형