"HTTP 상태 코드는 실패의 큰 범주를 알려 줘. 오류 본문은 무엇이 잘못됐고 클라이언트가 어떻게 대응할 수 있는지 설명해. 구조가 없으면 서로 다른 실패가 모두 같은 회색 덩어리로 보이고, 디버깅과 지원 비용이 쌓여."
오류 본문이 해야 하는 일
잘 설계한 오류 응답은 서로 다른 네 부류의 요구를 함께 다뤄야 해.
- 최종 사용자는 어떤 입력이나 행동을 고쳐야 하는지 이해할 수 있어야 해.
- 통합 개발자는 클라이언트 코드에서 안정적으로 분기할 기계 판독용 코드가 필요해.
- 고객 지원과 온콜 엔지니어는 서버 로그의 같은 요청을 찾을 추적 ID가 필요해.
- 자동 관측 시스템은 오류 유형별 발생량과 이상 징후를 집계할 수 있어야 해.
상태 코드만으로는 이 요구를 모두 담을 수 없어. 상태 코드는 HTTP 수준의 의미를 맡고, 구조화된 본문과 헤더가 세부 계약을 맡아.
최소 오류 envelope
안정된 코드, 설명, 추적 ID만으로도 기본적인 처리 흐름을 만들 수 있어.
{
"error": {
"code": "invalid_email_format",
"message": "이메일 주소 'pippa@@example.com' 가 유효하지 않습니다.",
"request_id": "req_xyz789"
}
}
code는 기계가 읽는 안정된 식별자야. 클라이언트는 메시지 문자열이 아니라 여기에서 분기해. message는 사람이 이해할 설명이지만, 그대로 최종 사용자에게 노출해도 안전한지와 현지화 책임은 별도로 정해야 해. request_id는 클라이언트가 본 실패를 서버 로그와 연결해 줘.
검증 오류를 여러 개 반환하기
요청 하나에 이메일 형식, 비밀번호 길이, 나이 범위처럼 여러 문제가 동시에 있을 수 있어. 하나씩 돌려주면 사용자가 수정과 재요청을 반복해야 하므로, 서로 독립적으로 확인할 수 있는 검증 오류는 한 번에 모두 반환하는 편이 좋아.
{
"error": {
"code": "validation_failed",
"message": "필드 3개에 대해 request validation 실패.",
"errors": [
{"field": "email", "code": "invalid_format", "message": "..."},
{"field": "password", "code": "too_short", "message": "...", "min": 8},
{"field": "age", "code": "out_of_range", "message": "...", "min": 0}
],
"request_id": "req_xyz789"
}
}
FastAPI와 Pydantic은 요청 검증 실패에 구조화된 422 응답을 기본 제공해. 다만 위의 사용자 정의 envelope과 모양이 같지는 않으므로, API 전체에서 하나의 계약을 원한다면 예외 처리기에서 변환해야 해. 필드 경로와 안정된 코드, 안전한 메시지를 함께 제공하면 클라이언트가 해당 입력란을 정확히 표시할 수 있어.
Problem Details — RFC 7807에서 RFC 9457로
IETF는 RFC 7807에서 HTTP API의 Problem Details를 정의했고, 2023년 RFC 9457이 이를 대체했어. 미디어 타입은 그대로 application/problem+json이며, 대표적인 표준 멤버는 다섯 가지야:
{
"type": "https://example.com/errors/invalid-email",
"title": "Invalid email format",
"status": 400,
"detail": "이메일 주소 'pippa@@example.com' 가 유효하지 않습니다.",
"instance": "/users/42"
}
type은 문제 유형을 식별하는 URI 참조이고, 문서를 제공하는 안정된 식별자로 쓸 수 있어. title은 문제 유형의 짧은 요약, status는 함께 반환된 HTTP 상태 코드, detail은 이번 발생에 대한 설명이야. instance는 문제의 특정 발생을 식별하는 URI 참조이지, 반드시 오류가 난 리소스 URI인 것은 아니야.
Problem Details를 채택할 수도 있고 일관된 자체 envelope을 유지할 수도 있어. 표준을 택한다면 현재 기준인 RFC 9457의 의미를 따라야 해. 어느 형태든 안정된 문제 식별자, 올바른 HTTP 상태, 확장 가능한 구조, 민감 정보를 흘리지 않는 메시지를 갖추는 게 핵심이야.
if error_message == "User not found": ...처럼 문구에 의존하면 번역이나 표현 개선만으로 클라이언트가 깨져. 프로그램의 제어 흐름은 안정된 코드나 type에 연결하고, 메시지는 설명과 현지화를 위해 독립적으로 발전시켜. 서버 메시지를 최종 사용자에게 그대로 보여 줄지는 보안과 제품 정책으로 따로 결정해야 해.피해야 할 안티패턴
HTTP API의 오류 계약에서는 다음 다섯 가지를 특히 조심해.
- {error:...}를 담은 200 OK. 실제 HTTP 작업이 실패했는데 200을 반환하면 캐시, 재시도, 관측 시스템이 성공으로 해석할 수 있어. 부분 성공이나 GraphQL처럼 별도 의미 체계를 명시한 프로토콜은 예외가 될 수 있지만, 일반 HTTP API에서는 결과에 맞는 4xx 또는 5xx를 써.
- 평문 문자열 본문. 응답 전체가
"User not found"뿐이면 안정된 분기 코드도, 확장 필드도, 추적 정보도 넣기 어려워. 사람이 읽는 문구가 곧 기계 계약이 되어 버리고. - 운영 환경의 HTML 스택 트레이스. 파일 경로, 프레임워크 버전, 설정 값 같은 내부 정보가 노출될 수 있어. 상세 추적은 보호된 로그에 남기고, 응답에는 안전한 설명과 추적 ID만 제공해.
- 엔드포인트마다 다른 형태.
/users는{error: ...},/orders는{message: ...},/payments는{detail: ...}을 반환하면 공통 클라이언트 처리가 어려워져. 하나의 오류 계약으로 통일해. - 추적 ID 부재. 오류 제보와 서버 로그를 연결하기 어려워져. 신뢰 경계에서 요청 ID를 검증하거나 새로 발급하고, 성공과 실패 응답 모두에 X-Request-ID 같은 헤더로 전달해. 오류 본문에도 넣을지는 계약에 따라 선택할 수 있어.
cwkPippa의 구조화된 오류 응답
{detail: "..."}이, Pydantic 요청 검증 실패에는 422 오류 배열이 반환돼. 단일 자체 프런트엔드가 detail을 읽어 알림을 표시하는 현재 구조에서는 동작하지만, detail 하나만으로는 안정된 기계 코드와 요청 추적 ID가 부족해 공통 오류 처리와 운영 추적에는 한계가 있어. 제3자 클라이언트나 지원 흐름이 늘어난다면 미들웨어에서 추적 ID를 발급하고, 예외 처리기에서 모든 실패를 하나의 envelope 또는 RFC 9457 Problem Details로 통일하는 편이 좋아.