@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를 변환해 errors에 field=null로 담아주세요.
👉 1편: Bean Validation(@Valid)로 입력 검증 표준 만들기
👉 2편: @ControllerAdvice 글로벌 예외 응답(에러코드 규격)
👉 3편: application-{profile}.yml 전략 + 비밀키 분리
👉 4편: Spring Boot Actuator: /health 커스터마이징
👉 5편: @Scheduled 크론 12패턴 + 중복실행 방지
'Java & Spring' 카테고리의 다른 글
| Spring Boot Actuator: /health 커스터마이징 (0) | 2025.09.30 |
|---|---|
| application-{profile}.yml 전략 + 비밀키 분리 (0) | 2025.09.30 |
| Bean Validation(@Valid)로 입력 검증 표준 만들기 (0) | 2025.09.29 |
| 예외 처리 베스트 프랙티스(체크/언체크, 경계 설계) (0) | 2025.09.29 |
| CompletableFuture allOf/anyOf로 외부 API 병렬화 (0) | 2025.09.29 |