본문 바로가기
Java & Spring

@ControllerAdvice 글로벌 예외 응답(에러코드 규격)

by yamoojin83 2025. 9. 29.

@ControllerAdvice 글로벌 예외 응답(에러코드 규격)

같은 예외라도 엔드포인트마다 다른 형태로 응답하면 클라이언트가 고생합니다.

@ControllerAdvice로 예외를 중앙에서 처리해 하나의 에러 스키마로 통일하세요.

표준 에러 스키마


{
  "timestamp":"2025-09-28T12:00:00Z",
  "path":"/api/users",
  "status":400,
  "code":"VALIDATION_FAILED",
  "message":"입력 값이 올바르지 않습니다.",
  "errors":[{"field":"email","message":"유효한 이메일 형식이 아닙니다."}]
}

핸들러 구현


@RestControllerAdvice
public class GlobalExceptionHandler {

  @ExceptionHandler(MethodArgumentNotValidException.class)
  public ResponseEntity<ApiError> handleBind(MethodArgumentNotValidException ex,
                                               HttpServletRequest req) {
    List<FieldErrorVM> errors = ex.getBindingResult().getFieldErrors().stream()
        .map(fe -> new FieldErrorVM(fe.getField(), fe.getDefaultMessage()))
        .toList();
    ApiError body = ApiError.of(400, "VALIDATION_FAILED", "입력 값이 올바르지 않습니다.",
                                req.getRequestURI(), errors);
    return ResponseEntity.badRequest().body(body);
  }

  @ExceptionHandler(UserAccessException.class)
  public ResponseEntity<ApiError> handleUser(UserAccessException ex, HttpServletRequest req) {
    ApiError body = ApiError.of(500, "USER_ACCESS_FAILED", ex.getMessage(), req.getRequestURI());
    return ResponseEntity.status(500).body(body);
  }

  @ExceptionHandler(Exception.class)
  public ResponseEntity<ApiError> handleUnknown(Exception ex, HttpServletRequest req) {
    ApiError body = ApiError.of(500, "INTERNAL_ERROR", "예기치 않은 오류", req.getRequestURI());
    return ResponseEntity.status(500).body(body);
  }
}

응답 모델


public record ApiError(Instant timestamp, int status, String code, String message,
                       String path, List<FieldErrorVM> errors) {
  public static ApiError of(int status, String code, String message, String path) {
    return new ApiError(Instant.now(), status, code, message, path, List.of());
  }
  public static ApiError of(int status, String code, String message, String path,
                            List<FieldErrorVM> errors) {
    return new ApiError(Instant.now(), status, code, message, path, errors);
  }
}
public record FieldErrorVM(String field, String message) {}

운영 팁

  • 로깅은 한 번만(핸들러에서). SLF4J MDC로 requestId를 넣으면 추적이 쉬워집니다.
  • 비즈니스 예외는 RuntimeException 파생 클래스로 정의하고 code를 부여.

FAQ

Q1. 상태코드와 code의 차이?
A. status는 HTTP 레벨, code는 도메인 레벨 식별자입니다(클라이언트 분기용).

Q2. 필드 에러 외에 글로벌 에러는?
A. ObjectError를 변환해 errorsfield=null로 담아주세요.

 

 

👉 1편: Bean Validation(@Valid)로 입력 검증 표준 만들기

👉 2편: @ControllerAdvice 글로벌 예외 응답(에러코드 규격)

👉 3편: application-{profile}.yml 전략 + 비밀키 분리

👉 4편: Spring Boot Actuator: /health 커스터마이징

👉 5편: @Scheduled 크론 12패턴 + 중복실행 방지

👉 6편: 멀티파트 업로드: 제한/보안/임시저장 설계

👉 7편: Spring Cache로 API 3배 빠르게(@Cacheable/무효화)

👉 8편: Pageable 응답 DTO 규격(정렬·페이지 표준화)