Contents
see List왜 API 오류 형식을 표준화해야 하는가
API를 운영하다 보면 같은 실패라도 컨트롤러마다 응답 구조가 달라지는 문제가 빠르게 생긴다. 어떤 API는 문자열 메시지를 반환하고, 다른 API는 예외 클래스 이름을 노출하며, 또 다른 API는 프레임워크 기본 HTML 오류 페이지를 반환한다. 이런 상태에서는 프론트엔드와 외부 연동 시스템이 오류를 해석하기 어렵고, 장애 분석 때 요청마다 다른 로그를 추적해야 한다.
Spring Boot 3 계열에서는 RFC 9457 형식에 맞춘 ProblemDetail을 이용해 오류 응답의 기본 골격을 만들 수 있다. 핵심은 HTTP 상태 코드, 사람이 읽는 제목, 상세 설명, 요청 경로, 서비스가 정의한 오류 코드와 검증 필드 정보를 분리하는 것이다. 클라이언트는 오류 코드로 분기하고, 화면에는 안전한 상세 설명만 보여 주며, 운영자는 요청 식별자로 로그를 연결할 수 있다.
먼저 오류 응답 계약을 정한다
예외 처리 코드를 작성하기 전에 팀이 사용할 계약부터 정해야 한다. status는 HTTP 의미를 유지한다. 인증되지 않은 요청은 401, 권한 부족은 403, 존재하지 않는 자원은 404, 입력값 검증 실패는 400, 이미 처리된 요청처럼 현재 상태와 충돌하면 409를 선택한다. 모든 업무 실패를 200으로 반환하거나 500으로 뭉뚱그리면 재시도와 사용자 안내가 잘못 동작한다.
- type: 오류 문서 또는 오류 유형을 식별하는 URI
- title: 짧고 일반적인 오류 제목
- status: 실제 HTTP 상태 코드
- detail: 사용자에게 노출해도 안전한 설명
- code: 클라이언트가 처리하는 서비스 오류 코드
- fieldErrors: 입력 검증 오류의 필드별 목록
- traceId: 서버 로그를 찾기 위한 요청 식별자
비밀번호, 토큰, SQL 문장, 내부 서버 주소, 스택 트레이스는 detail에 넣지 않는다. 개발 환경에서 편리하더라도 운영 응답에 포함되면 시스템 구조와 개인정보가 노출될 수 있다. 내부 원인은 구조화 로그에 남기고, 응답에는 필요한 범위의 설명만 제공하는 원칙이 중요하다.
도메인 예외와 공통 처리기 구현
업무 규칙 위반을 IllegalArgumentException 같은 범용 예외로 던지면 의미와 상태 코드를 잃기 쉽다. 업무 오류 코드와 HTTP 상태를 가진 예외를 하나 만들고, 서비스 계층에서 이를 명시적으로 사용한다. 아래 예시는 주문이 이미 취소된 상태에서 다시 취소를 요청한 경우를 처리한다.
public class BusinessException extends RuntimeException { private final HttpStatus status; private final String code; public BusinessException(HttpStatus status, String code, String message) { super(message); this.status = status; this.code = code; } public HttpStatus getStatus() { return status; } public String getCode() { return code; } } @RestControllerAdvice public class ApiExceptionHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity<ProblemDetail> handleBusiness(BusinessException ex, HttpServletRequest request) { ProblemDetail body = ProblemDetail.forStatusAndDetail(ex.getStatus(), ex.getMessage()); body.setTitle("요청을 처리할 수 없습니다."); body.setType(URI.create("https://www.softmoa.com/problems/" + ex.getCode())); body.setProperty("code", ex.getCode()); body.setProperty("path", request.getRequestURI()); return ResponseEntity.status(ex.getStatus()).body(body); } }예외 객체에는 메시지를 만들기 위한 원본 데이터나 엔티티 전체를 보관하지 않는 편이 좋다. 오래 살아 있는 비동기 작업이나 로그 큐에서 불필요한 객체 참조가 유지될 수 있으며, 예외 직렬화 과정에서 민감 정보가 섞일 가능성도 줄일 수 있다. 오류 코드는 ORDER_ALREADY_CANCELLED처럼 안정적인 문자열로 정하고, 문구가 바뀌어도 코드는 유지한다.
입력값 검증 오류를 필드 단위로 반환하기
@Valid 검증 실패는 MethodArgumentNotValidException으로 들어온다. 기본 메시지만 돌려주면 사용자는 어느 입력을 고쳐야 하는지 알 수 없다. fieldErrors 배열에 필드명과 검증 코드만 내려 주면 화면은 각 입력칸 근처에 적절한 메시지를 표시할 수 있다. rejectedValue는 이메일 주소나 긴 텍스트처럼 민감하거나 큰 값일 수 있으므로 응답에 포함하지 않는 것이 안전하다.
@ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ProblemDetail> handleValidation(MethodArgumentNotValidException ex) { ProblemDetail body = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST); body.setTitle("입력값이 올바르지 않습니다."); body.setDetail("필수 항목과 입력 형식을 확인하세요."); body.setProperty("code", "VALIDATION_FAILED"); List<Map<String, String>> errors = ex.getBindingResult().getFieldErrors().stream().map(error -> Map.of("field", error.getField(), "reason", error.getCode())).toList(); body.setProperty("fieldErrors", errors); return ResponseEntity.badRequest().body(body); }필드명은 API 계약의 일부다. Java 필드명을 그대로 쓸지, JSON의 snake_case 이름을 쓸지 정한 뒤 전 API에서 일관되게 적용한다. 다국어 메시지는 서버가 모든 문구를 번역하기보다 reason과 오류 코드를 기준으로 클라이언트가 처리하는 방법도 있다. 다만 외부 공개 API라면 문서에 오류 코드와 발생 조건을 함께 제공해야 한다.
예상하지 못한 오류의 처리와 관측
마지막으로 Exception을 처리해 500 응답 형식을 통일할 수 있지만, 이를 모든 원인을 숨기는 장치로 사용하면 안 된다. 이 처리기는 예상하지 못한 버그의 최후 안전망이다. 원인은 error 레벨 로그에 예외 객체와 traceId를 포함해 기록하고, 응답에는 일반적인 안내와 INCIDENT_INTERNAL 같은 코드만 반환한다. Micrometer Tracing 또는 로그 MDC를 사용한다면 traceId를 응답 속성으로 넣어 고객 문의와 서버 로그를 빠르게 연결할 수 있다.
테스트에서는 상태 코드만 확인하지 말고 JSON 계약도 검증한다. MockMvc로 code, title, fieldErrors의 형태를 검사하면 리팩터링 중 오류 응답이 깨지는 일을 줄일 수 있다. 또한 프록시나 API 게이트웨이가 404와 502를 생성하는 경우에도 같은 형식이 필요한지 운영 경계를 확인한다. 애플리케이션 밖에서 생기는 오류는 별도 게이트웨이 정책으로 통일해야 한다.
배포 전 체크리스트
- 각 업무 예외에 적절한 HTTP 상태와 변경하지 않을 오류 코드를 정의했는가
- 검증 오류가 필드별로 반환되고 민감한 입력값은 제외되는가
- 500 응답에 내부 예외 메시지, SQL, 스택 트레이스가 노출되지 않는가
- 오류 응답의 traceId로 로그와 모니터링 정보를 찾을 수 있는가
- 주요 오류 사례를 통합 테스트로 고정해 API 계약 변경을 감지하는가
일관된 오류 응답은 화면 개발과 외부 연동의 예외 처리를 단순하게 만들고, 장애 원인 확인 시간을 줄인다. 상태 코드, 안정적인 오류 코드, 안전한 메시지, 추적 가능한 로그라는 네 가지 기준을 먼저 고정한 뒤 예외 유형을 점진적으로 추가하는 방식이 운영에 적합하다.
spring
| No | 작성일 | Title |
|---|---|---|
| 3456 | 2026. 09. 09. | Spring Boot API 오류 응답을 일관되게 만드는 방법: ProblemDetail과 예외 처리 실전 가이드 |
| 3424 | 2026. 08. 31. | Spring Data JPA N+1 조회 문제를 찾고 줄이는 실전 가이드 |
| 3392 | 2026. 08. 23. | Spring Boot 설정값을 안전하게 관리하는 방법: @ConfigurationProperties 검증과 환경별 구성 실전 가이드 |
| 3360 | 2026. 08. 15. | Spring Boot Actuator와 Micrometer로 장애 징후를 먼저 찾는 운영 모니터링 구성 |
| 3328 | 2026. 08. 06. | Spring Boot 예외 처리 표준화 가이드: ProblemDetail로 일관된 API 오류 응답 만들기 |
| 3296 | 2026. 07. 29. | Spring Boot 로그인 API 보안 설계: 세션 없이 JWT를 안전하게 검증하는 방법 |
| 3265 | 2026. 07. 21. | Spring Boot API 장애 원인 빨리 찾기: Actuator와 Micrometer 관측성 구성 가이드 |
| 3238 | 2026. 07. 13. | Spring Boot 안정적인 외부 API 연동: WebClient·Circuit Breaker·Retry를 함께 운영하는 실전 가이드 |
| 3182 | 2026. 07. 04. | Spring Boot 무중단 배포를 위한 Graceful Shutdown과 Readiness Probe 설정 |
| 3137 | 2026. 06. 26. | Spring Security API 권한 설계: 공개 엔드포인트와 관리자 기능을 분리하는 실전 기준 |