API 오류 응답 포맷을 팀마다 다르게 만들다가 표준으로 정리한 과정
문제 발생
서비스마다 오류 응답이 달랐습니다.
{ "error": "NOT_FOUND" }
{ "message": "게시글을 찾을 수 없습니다", "code": 404 }
{ "success": false, "errors": [{ "field": "title", "msg": "필수" }] }클라이언트는 세 가지 파서를 갖게 됐고, 새 오류가 생길 때마다 문자열 비교가 늘었습니다.
원인 분석
이미 표준이 있는데 매번 새로 만들고 있었습니다. RFC 9457은 목적을 첫 문장에 적습니다 — HTTP API를 위해 새 오류 응답 포맷을 정의할 필요가 없도록, 응답 본문에 기계가 읽을 수 있는 오류 상세를 담는 "problem detail"을 정의한다.
미디어 타입은 application/problem+json이고, 표준 멤버는 다섯입니다.
type— 문제 유형을 식별하는 URI 참조. 스펙은 **소비자가typeURI를 문제 유형의 기본 식별자로 사용해야 한다(MUST)**고 못박습니다.title— 문제 유형에 대한 짧고 사람이 읽을 수 있는 요약status— 원 서버가 생성한 HTTP 상태 코드detail— 이번 발생 건에 특정한, 사람이 읽는 설명instance— 이 발생 건을 식별하는 URI 참조
그리고 클라이언트 구현에 대한 지침도 있습니다 — 소비자는 정보를 얻기 위해 detail 멤버를 파싱해서는 안 되며(SHOULD NOT), 확장 멤버가 더 적절하고 오류가 적은 방법입니다.
해결 방안
- 응답 형태를 하나로 맞춥니다.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://meokko.com/problems/not-post-author",
"title": "작성자만 수정할 수 있습니다",
"status": 403,
"detail": "이 게시글의 작성자가 아닙니다.",
"instance": "/posts/react-server-component-머꼬-a1b2c3d4"
}- 기계가 분기할 값은 확장 멤버로 넣습니다. 필드 단위 검증 오류가 대표적입니다.
{ "type": "...", "status": 422, "errors": [{ "field": "title", "rule": "required" }] }-
typeURI는 안정적으로 유지합니다. 클라이언트의 분기 기준이 되므로, 문구를 다듬는다고 URI를 바꾸면 계약이 깨집니다. 문서 페이지를 실제로 열어둘 필요는 없지만 열어두면 좋습니다. -
내부 구현을 흘리지 않습니다. RFC의 보안 고려사항 그대로 — 스택 덤프 같은 구현 세부를 HTTP 인터페이스로 노출하지 않아야 합니다. 서버 구조와 데이터가 드러납니다.
-
상태 코드를 대체하지 않습니다.
status는 상태 코드의 사본일 뿐입니다. HTTP 상태를 200으로 두고 본문에만 오류를 담는 방식은 캐시·프록시·클라이언트 기본 동작을 전부 어긋나게 만듭니다. -
한 번에 전면 도입하지 않아도 됩니다. 새 엔드포인트부터 이 포맷으로 내고, 기존 것은 클라이언트가 옮겨갈 때 맞춰 바꿉니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.