예외마다 응답 형식이 달라지던 것을 ProblemDetail로 통일한 과정
문제 발생
예외 처리기를 필요할 때마다 추가하다 보니 오류 응답이 제각각이었습니다.
{ "message": "not found" }
{ "error": "FORBIDDEN", "code": 403 }
{ "timestamp": "...", "status": 500, "path": "/api/posts" }클라이언트는 세 가지 파서를 유지해야 했고, Spring 내장 예외(검증 실패, 잘못된 미디어 타입)는 또 다른 형식으로 나갔습니다.
원인 분석
표준 형식이 이미 프레임워크에 들어 있습니다. Spring 문서는 RFC 9457 "Problem Details for HTTP APIs"를 지원한다고 밝히며 네 가지 추상화를 소개합니다.
ProblemDetail— 명세가 정의한 표준 필드와 커스텀 비표준 필드를 담는 컨테이너ErrorResponse— HTTP 상태, 헤더, RFC 9457 형식의 본문을 노출하는 계약. 모든 Spring MVC 예외가 이 인터페이스를 구현합니다ErrorResponseException— 커스텀 예외의 기반 클래스로 쓰기 좋은 기본 구현ResponseEntityExceptionHandler—@ControllerAdvice의 기반 클래스로, 모든 Spring MVC 예외와ErrorResponseException을 처리해 RFC 9457 형식 본문으로 렌더합니다
처리 규칙도 정해져 있습니다 — ProblemDetail의 status가 HTTP 상태를 결정하고, instance가 비어 있으면 현재 URL 경로로 채워지며, Jackson 코덱이 application/problem+json을 producible 미디어 타입으로 사용합니다.
해결 방안
- 전역 핸들러를 기반 클래스에서 상속합니다. 내장 예외까지 한 번에 형식이 통일됩니다.
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(PostNotFoundException.class)
ProblemDetail handleNotFound(PostNotFoundException ex) {
ProblemDetail body = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
body.setType(URI.create("https://meokko.com/problems/post-not-found"));
body.setTitle("게시글을 찾을 수 없습니다");
return body;
}
}- Spring Boot에서는 설정 한 줄로 내장 예외를 켭니다.
spring.mvc.problemdetails.enabled=true문서 설명대로 이 설정은 내장 예외를 problem details로 처리하는 핸들러를 order 0으로 자동 구성하므로, 우리 @ControllerAdvice는 그보다 앞선 order를 갖게 둡니다.
-
기계가 읽을 값은 확장 필드에 넣습니다.
getProperties()맵에 넣으면 Jackson이 최상위 JSON 속성으로 펼쳐줍니다 — 사람이 읽는detail을 클라이언트가 파싱하게 만들지 않습니다. -
커스텀 예외는
ErrorResponseException을 상속합니다. 예외 자체가 상태와 본문을 알고 있으면 핸들러가 얇아집니다. -
메시지 국제화도 여기에 붙습니다. 문서가 설명하는 메시지 코드 전략(
problemDetail.title.<예외 클래스>)을MessageSource로 풀면 사용자 언어에 맞는 문구가 나갑니다. -
스택 트레이스를 담지 않습니다. RFC 9457의 보안 고려사항이자 이 저장소의 원칙이기도 합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.