Contents
see ListAPI 오류 응답을 먼저 표준화해야 하는 이유
Spring Boot API를 운영하다 보면 같은 실패라도 엔드포인트마다 응답 형태가 달라지는 문제가 자주 생깁니다. 어떤 API는 문자열 메시지만 반환하고, 어떤 API는 스택 트레이스 일부를 노출하며, 또 어떤 API는 200 상태 코드에 실패 정보를 담습니다. 이런 불일치는 프런트엔드와 외부 연동 개발자의 예외 처리 비용을 높이고, 모니터링에서도 같은 장애를 묶어 보기 어렵게 만듭니다.
Spring Framework 6부터는 RFC 9457 형식의 문제 상세 응답을 표현하는 ProblemDetail을 기본 API로 제공합니다. 핵심은 오류마다 임의의 JSON을 만들지 않고, HTTP 상태, 사람이 읽을 제목, 구체적인 설명, 애플리케이션 오류 코드를 분리하는 것입니다. 이 문서에서는 REST API에서 재사용 가능한 예외 처리 구조를 구성하는 방법을 다룹니다.
응답 계약부터 정한다
오류 응답에는 최소한 상태 코드, 오류 유형을 구분할 수 있는 코드, 사용자가 이해할 수 있는 메시지, 요청을 추적할 식별자가 필요합니다. 내부 SQL, 토큰, 클래스 이름, 스택 트레이스는 응답에 포함하지 않습니다. 클라이언트가 분기할 때는 문장 대신 안정적인 오류 코드인 errorCode를 사용하게 해야 합니다.
- 400: 형식이 맞지 않거나 필수 입력값이 빠진 요청
- 401·403: 인증되지 않았거나 권한이 없는 요청
- 404: 존재하지 않는 리소스
- 409: 중복 생성, 버전 충돌 등 현재 상태와 충돌하는 요청
- 500: 처리 중 예기치 않은 서버 오류. 상세 원인은 로그에서만 확인
도메인 예외와 ProblemDetail 연결하기
서비스 계층에서는 HTTP 응답을 직접 만들지 말고, 업무 의미를 가진 예외를 던집니다. 예를 들어 이미 사용 중인 이메일은 데이터베이스 예외를 그대로 노출하는 대신 DuplicateEmailException으로 변환합니다. 웹 계층의 전역 예외 처리기가 이 예외를 정해 둔 응답 계약으로 바꿉니다.
public class DuplicateEmailException extends RuntimeException {
public DuplicateEmailException() {
super("이미 사용 중인 이메일입니다.");
}
}
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(DuplicateEmailException.class)
ResponseEntity<ProblemDetail> handleDuplicate(DuplicateEmailException ex,
HttpServletRequest request) {
ProblemDetail detail = ProblemDetail.forStatusAndDetail(
HttpStatus.CONFLICT, ex.getMessage());
detail.setTitle("요청 충돌");
detail.setType(URI.create("https://api.example.com/problems/duplicate-email"));
detail.setProperty("errorCode", "USER_EMAIL_DUPLICATED");
detail.setProperty("path", request.getRequestURI());
return ResponseEntity.status(HttpStatus.CONFLICT).body(detail);
}
}type은 오류 문서 URL이나 고유 URI로 사용합니다. 지금 문서가 없다면 우선 고유한 URI를 유지하고, 이후 API 가이드 페이지를 연결할 수 있습니다. path는 문제 재현과 문의 대응에 유용하지만, 쿼리 문자열에는 개인정보가 들어갈 수 있으므로 요청 URI만 저장하는 편이 안전합니다.
검증 오류는 필드 단위로 반환한다
@Valid 검증이 실패했을 때 메시지 하나만 내려주면 화면에서 어떤 입력칸을 표시해야 하는지 알기 어렵습니다. MethodArgumentNotValidException을 처리하여 필드명, 거절된 값 대신 안전한 메시지, 검증 규칙을 배열로 제공합니다. 비밀번호나 주민번호처럼 민감할 수 있는 거절값은 응답과 로그 모두에서 제외합니다.
@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<ProblemDetail> handleValidation(MethodArgumentNotValidException ex) {
ProblemDetail detail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
detail.setTitle("입력값 검증 실패");
detail.setDetail("요청 값을 확인한 뒤 다시 시도하세요.");
detail.setProperty("errorCode", "VALIDATION_ERROR");
List<Map<String, String>> fields = ex.getBindingResult().getFieldErrors().stream()
.map(error -> Map.of(
"field", error.getField(),
"message", error.getDefaultMessage(),
"rule", error.getCode()))
.toList();
detail.setProperty("fields", fields);
return ResponseEntity.badRequest().body(detail);
}클라이언트는 fields 배열을 읽어 해당 폼 항목에 메시지를 표시하고, 공통 알림에는 detail만 보여 줄 수 있습니다. 이때 검증 메시지는 서버 내부 제약 조건의 이름이 아니라 사용자가 수정 가능한 행동을 설명해야 합니다. 예를 들어 “size must be between”보다 “닉네임은 2자 이상 20자 이하로 입력하세요”가 낫습니다.
예상하지 못한 오류의 처리 원칙
마지막에는 Exception을 받는 처리기를 둡니다. 하지만 이 처리기는 장애를 숨기기 위한 장치가 아니라, 응답 노출을 제어하면서 원인을 관측하기 위한 안전망입니다. 서버 로그에는 요청 ID, 사용자 식별자(필요 최소한), 예외 스택을 남기고 응답에는 일반적인 안내만 반환합니다. 요청 ID는 필터에서 생성하거나 전달받아 MDC와 응답 헤더에 넣으면 로그와 고객 문의를 연결하기 쉽습니다.
@ExceptionHandler(Exception.class)
ResponseEntity<ProblemDetail> handleUnexpected(Exception ex,
HttpServletRequest request) {
String requestId = (String) request.getAttribute("requestId");
log.error("unexpected error requestId={}", requestId, ex);
ProblemDetail detail = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
detail.setTitle("서버 처리 오류");
detail.setDetail("잠시 후 다시 시도하세요. 문제가 계속되면 요청 ID를 알려주세요.");
detail.setProperty("errorCode", "INTERNAL_ERROR");
detail.setProperty("requestId", requestId);
return ResponseEntity.internalServerError().body(detail);
}적용 체크리스트
- 모든 오류에서 HTTP 상태 코드와
errorCode의 의미를 문서화한다. - 도메인 예외는 서비스 계층에서 정의하고, HTTP 변환은
@RestControllerAdvice에 모은다. - 검증 오류는 필드별 배열로 반환하되 민감한 입력값은 포함하지 않는다.
- 500 응답에는 내부 구현 정보와 스택 트레이스를 절대 넣지 않는다.
- 예외 처리 테스트에서 상태 코드, 오류 코드, 필드 오류 구조를 함께 검증한다.
오류 응답은 실패를 꾸미는 형식이 아니라 API의 장기 호환성을 지키는 계약입니다. 먼저 몇 개의 핵심 오류 코드와 응답 구조를 고정하고, 신규 기능도 같은 처리기를 통과하도록 만들면 클라이언트 변경과 운영 분석이 훨씬 단순해집니다.
spring
| No | 작성일 | Title |
|---|---|---|
| 2122 | 2026. 02. 11. | Spring Boot 3.4 신기능과 마이그레이션 가이드 |
| 2013 | 2025. 11. 30. | Spring Cache 추상화 - Redis 캐시 적용 |
| 2012 | 2025. 11. 30. | Spring Validation - Bean Validation 활용 |
| 2011 | 2025. 11. 30. | Spring Profile로 환경별 설정 관리 |
| 2010 | 2025. 11. 30. | Spring AOP로 로깅과 트랜잭션 처리 |
| 2009 | 2025. 11. 30. | Spring Batch 대용량 데이터 처리 |
| 2008 | 2025. 11. 30. | Spring Cloud Gateway로 API Gateway 구축 |
| 2007 | 2025. 11. 30. | Spring WebFlux - 리액티브 프로그래밍 입문 |
| 2006 | 2025. 11. 30. | Spring Data JPA 쿼리 메서드 완벽 가이드 |
| 2005 | 2025. 11. 30. | Spring Security 6 JWT 인증 구현 |