API 오류 응답은 계약의 일부입니다. 클라이언트는 오류를 파싱하고, 재시도 로직은 오류에 따라 분기하며, 지원 엔지니어는 새벽 2시에 오류를 검색합니다. 하지만 많은 팀이 정상 흐름(happy path)만 상세히 설계하고 오류는 프레임워크 기본 동작에 맡깁니다. 그 결과 하나의 API에서 서로 다른 오류 형식이 나타나고, "success": false를 포함한 200 응답이나 데이터베이스 스키마를 노출하는 스택 트레이스를 반환하게 됩니다.
이 가이드에서는 REST 서비스의 API 오류 처리 모범 사례를 정리합니다. 올바른 상태 코드 선택, RFC 9457 문제 상세 정보(Problem Details)를 활용한 오류 본문 표준화, 기계가 읽는 코드와 사람이 읽는 메시지 분리, 재시도 가능 여부 표시, 민감한 정보 제거 방법을 다룹니다.
또한 REST API가 사용해야 할 HTTP 상태 코드에 대한 분석을 바탕으로 계약 수준의 결정 사항을 살펴보고, Apidog에서 모든 실패 경로를 테스트하는 방법도 알아봅니다. 테스트하지 않는 오류 계약은 존재하지 않는 계약과 같습니다.
본문보다 상태 코드부터 설계하세요
HTTP 상태 코드는 오류 의미론의 첫 번째 계층입니다. RFC 9110은 상태 코드 패밀리를 다음과 같이 정의합니다.
- 4xx: 클라이언트가 잘못된 요청을 보냈으며, 같은 요청을 반복해도 실패할 가능성이 높습니다.
- 5xx: 서버가 실패했으며, 클라이언트의 요청 자체는 올바를 수 있습니다.
클라이언트, 프록시, 캐시, 재시도 라이브러리는 JSON 본문을 읽지 않고도 상태 코드에 따라 동작합니다. 따라서 오류 본문을 설계하기 전에 상태 코드를 먼저 올바르게 선택해야 합니다.
설계할 때는 MDN의 HTTP 상태 코드 참조를 확인하고, 다음 결정 테이블을 팀의 기준으로 사용하세요.
| 상황 | 사용 | (사용) 지양 | 이유 |
|---|---|---|---|
| 잘못된 형식의 요청: 깨진 JSON, 잘못된 콘텐츠 타입, 필수 필드 누락 | 400 잘못된 요청 | 422 | 서버가 요청을 파싱하거나 이해할 수 없음 |
| 의미 규칙을 위반하는 올바른 형식의 요청: 금액이 음수이거나, 지원되지 않는 통화 | 422 처리할 수 없는 엔티티 | 400 | 구문은 유효하지만 값이 유효하지 않음 |
| 자격 증명 없음, 또는 만료/유효하지 않은 토큰 | 401 권한 없음 | 403 | 클라이언트가 본인임을 증명하지 못함. WWW-Authenticate 전송 |
| 유효한 자격 증명, 불충분한 권한 | 403 금지됨 | 401 | 신원은 알려져 있지만 접근이 거부됨. 재인증해도 소용없음 |
| 리소스가 존재하지 않거나, 존재 여부를 확인해 주지 않을 경우 | 404 찾을 수 없음 | 410 | 안전한 기본값이며 무단 탐침으로부터 리소스를 숨김 |
| 리소스가 존재했지만 의도적으로 영구 삭제됨 | 410 사라짐 | 404 | 클라이언트와 크롤러에게 참조를 삭제하도록 알림 |
| 상태 충돌: 중복 키, 오래된 버전, 편집 충돌 | 409 충돌 | 400 | 요청은 유효하지만 현재 리소스 상태와 충돌함 |
| 클라이언트가 요청 제한을 초과함 | 429 너무 많은 요청 | 503 | 클라이언트가 올바르게 후퇴하도록 항상 Retry-After 포함 |
| 코드에서 처리되지 않은 예외 | 500 내부 서버 오류 | 502 | 서버가 고장 났음 |
| 업스트림 서비스가 게이트웨이에 잘못된 데이터를 반환함 | 502 잘못된 게이트웨이 | 500 | 오류가 엣지 내부가 아니라 하위 시스템에 있음 |
| 서버 과부하 또는 유지보수 중 | 503 서비스를 사용할 수 없음 | 500 | 정의상 임시적이며, 가능하면 Retry-After 추가 |
| 업스트림 서비스 시간 초과 | 504 게이트웨이 시간 초과 | 500 | 느린 종속성과 손상된 코드를 구분함 |
특히 다음 두 가지를 기억하세요.
- 401과 403은 스타일이 아니라 보안 경계입니다. 인증되지 않은 호출자에게 403을 반환하면 리소스의 존재 여부가 노출될 수 있습니다.
-
429에는
Retry-After를 포함하세요. 이 헤더가 없으면 클라이언트가 계속 요청을 보내게 됩니다. 요청 제한을 사용한다면 상태 코드와 함께 구체적인 후퇴(backoff) 신호를 제공해야 합니다. 관련 헤더 계산과 알고리즘은 API 요청 제한(rate limiting) 구현 방법에서 확인할 수 있습니다.
모든 오류에 하나의 본문 형식을 사용하세요
상태 코드가 정해졌다면 모든 오류가 동일한 미디어 타입과 스키마를 공유하도록 설계하세요. 표준적인 선택은 application/problem+json으로 반환하는 RFC 9457 문제 상세 정보(Problem Details)입니다.
Problem Details는 다음 핵심 멤버를 정의합니다.
-
type: 오류 카테고리를 식별하는 URI -
title: 사람이 읽을 수 있는 간단한 요약 -
status: HTTP 상태 코드 -
detail: 이번 요청에서 발생한 구체적인 문제 -
instance: 특정 실패를 식별하는 URI
그 외 필드는 확장 멤버로 정의할 수 있습니다. 전체 멤버, 등록 규칙, RFC 7807과의 관계는 RFC 9457 설명서에서 확인하세요.
중요한 패턴은 표준 엔벨로프와 사용자 정의 확장을 결합하는 것입니다. 다음은 결제 엔드포인트의 유효성 검사 실패 예시입니다.
POST /v1/payments HTTP/1.1
Content-Type: application/json
{ "amount": -1400, "currency": "USD", "source": "card_8xKt2" }
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Request validation failed",
"status": 422,
"detail": "One or more fields failed validation.",
"instance": "/v1/payments/requests/req_9f3c1a7b",
"code": "PAYMENT_VALIDATION_FAILED",
"errors": [
{
"field": "amount",
"code": "AMOUNT_NOT_POSITIVE",
"message": "amount must be a positive integer in minor units"
}
],
"request_id": "req_9f3c1a7b"
}
errors[]는 확장 멤버지만 클라이언트가 가장 유용하게 활용하는 부분입니다. 하나의 모호한 배너 대신 프런트엔드가 각 오류를 정확한 폼 필드에 매핑할 수 있기 때문입니다.
클라이언트 코드가 필드 경로를 안정적으로 바인딩할 수 있도록 JSON 포인터 또는 점 표기 경로 중 하나를 선택해 일관되게 사용하세요.
또한 핸들러가 반환하는 오류뿐 아니라 프레임워크와 게이트웨이가 생성하는 오류까지 동일한 형식으로 반환해야 합니다. 애플리케이션에서는 Problem Details를 받지만 로드 밸런서의 502 응답에서는 HTML을 받는다면, 클라이언트는 결국 두 개의 파서를 작성해야 합니다.
기계가 읽는 코드와 사람이 읽는 메시지를 분리하세요
예시의 code와 message는 서로 다른 목적을 가집니다. 하나의 문자열로 합치지 마세요.
기계가 읽는 코드
AMOUNT_NOT_POSITIVE, CURRENCY_UNSUPPORTED, IDEMPOTENCY_KEY_REUSED 같은 코드는 계약의 일부입니다.
코드는 다음 조건을 만족해야 합니다.
- 안정적이어야 함
- 문서화되어야 함
- 열거 가능해야 함
- 클라이언트가 프로그래밍 방식으로 분기할 수 있어야 함
클라이언트가 자연어를 파싱하게 만들지 마세요. 누군가 다음과 같이 작성하는 순간 메시지 수정은 호환성을 깨는 변경이 됩니다.
if (message.includes("positive")) {
// ...
}
사람이 읽는 메시지
메시지는 언제든 개선할 수 있어야 하며, 로그를 읽는 개발자와 최종 사용자에게 필요한 정보를 제공해야 합니다. 무엇이 실패했고 어떻게 수정해야 하는지 명확하게 작성하세요.
- 나쁜 예:
"유효하지 않은 금액" - 좋은 예:
"금액은 소액 단위의 양의 정수여야 합니다"
현지화가 필요하다면 메시지만 번역하고 코드는 그대로 유지하세요.
API 소비자에 자율 에이전트가 포함되면서 이 구분은 더욱 중요해졌습니다. LLM 기반 클라이언트는 구조화되고 자체 설명적인 오류에서 더 안정적으로 복구합니다. 자세한 내용은 AI 에이전트를 위한 API 오류 설계에서 다룹니다.
오류 응답에 포함하지 말아야 할 정보
처리되지 않은 오류는 공격자에게 유용한 정찰 채널이 될 수 있습니다. 오류 미들웨어는 다음 정보가 클라이언트에 노출되지 않도록 해야 합니다.
- 스택 트레이스, 클래스 이름, 파일 경로
- 원시 SQL, 쿼리 조각, ORM 오류
- 내부 호스트 이름, IP, 포트, 서비스 이름
- 라이브러리 버전과 프레임워크 배너 문자열
- 예외 메시지에 포함된 비밀, 토큰, 연결 문자열
- 사용자 계정 존재 여부 로그인과 비밀번호 재설정 흐름에서는 성공·실패 응답을 대칭적으로 유지하세요.
권장 패턴은 간단합니다.
- 경계에서 모든 예외를 포착합니다.
- 요청 ID와 함께 전체 예외를 서버 로그에 기록합니다.
- 같은 요청 ID를 포함한 일반적인 Problem Details 본문을 반환합니다.
클라이언트에는 다음과 같이 반환합니다.
{
"detail": "내부 오류가 발생했습니다",
"request_id": "req_51ad0"
}
로그에는 실제 예외를 기록하고, 지원 팀은 request_id로 두 정보를 연결할 수 있습니다.
오류를 재시도 가능 또는 최종 오류로 표시하세요
모든 오류 응답은 클라이언트의 핵심 질문에 답해야 합니다.
다시 시도해야 할까요?
각 클라이언트 팀이 추측하게 하지 말고, 이 정보를 계약에 포함하세요.
일반적인 기본 의미론은 다음과 같습니다.
-
429,502,503,504: 지수 백오프(exponential backoff)와 지터(jitter)를 사용해 재시도 가능 -
500: 모호하지만 일반적으로 한 번의 신중한 재시도 가능 - 대부분의 다른
4xx: 최종(terminal) 오류
동일한 요청으로 401, 403, 404, 422를 반복하면 할당량을 낭비하고 로그만 오염시킵니다.
타임아웃은 특별히 주의해야 합니다. 클라이언트가 포기한 뒤에도 서버에서 요청이 성공했을 수 있기 때문입니다. 이것이 변경을 유발하는 엔드포인트가 멱등성 키(idempotency keys)를 지원해야 하는 이유입니다. 재시도된 결제가 이중 청구되는 것을 막을 수 있습니다.
확장 멤버로 재시도 가능 여부를 명시할 수도 있습니다.
{
"type": "https://api.example.com/problems/rate-limited",
"title": "Too many requests",
"status": 429,
"code": "RATE_LIMITED",
"retryable": true,
"retry_after_seconds": 30
}
명시적인 retryable 플래그를 사용하면 기본 의미론을 필요한 경우 재정의할 수 있습니다. 예를 들어 특정 500 하위 코드는 재시도할 경우 상태를 손상시킬 수 있으므로 최종 오류로 표시할 수 있습니다.
이 필드를 한 번 문서화하면 모든 클라이언트 SDK가 일관된 백오프 동작을 구현할 수 있습니다.
상관 관계 ID와 오류 계약 버전 관리
작은 결정 두 가지가 오류 계약을 완성합니다. 지금 도입하면 비용이 적지만, 나중에 추가하려면 비용이 큽니다.
모든 요청에 ID 부여하기
들어오는 X-Request-Id 헤더를 수락하거나 새로 생성하세요.
- 모든 로그 라인에 기록
- 모든 오류 본문에
request_id로 포함 - 지원 티켓의 오류와 서버 로그 연결
고객이 오류를 붙여넣었을 때 request_id 하나만으로 한 시간 동안의 로그 탐색을 단일 쿼리로 줄일 수 있습니다.
분산 환경에서는 W3C traceparent도 전파해 여러 서비스에 걸쳐 요청을 추적하세요.
오류 계약 버전 관리하기
오류 계약도 API 자체처럼 버전 관리해야 합니다.
다음 변경은 일반적으로 안전합니다.
- 새로운 확장 멤버 추가
- 새로운 오류 코드 추가
다음 변경은 호환성을 깨뜨릴 수 있습니다.
-
errors[].field이름 변경 - 기존 코드의 의미 변경
- 임시 오류 형식에서 Problem Details로 일괄 전환
type URI는 이를 관리하기 좋은 메커니즘입니다. 기존 type URI는 영구적으로 안정성을 유지하고, 새로운 의미에는 새로운 URI를 사용하세요.
문서에는 클라이언트가 알 수 없는 확장 멤버와 오류 코드를 실패로 처리하지 말고 무시해야 한다는 규칙도 명시해야 합니다. 이런 전방 호환성 규칙이 있으면 v2를 만들지 않고도 계약을 발전시킬 수 있습니다.
Apidog에서 모든 오류 경로 테스트하기
오류 계약이 부패하는 가장 큰 이유는 아무도 테스트하지 않기 때문입니다. 정상 흐름은 데모에서 항상 실행되지만, 422 분기는 고객이 실제 오류를 만났을 때만 실행됩니다.
해결책은 실패 사례를 테스트 스위트의 일등 시민으로 만드는 것입니다. 이때 Apidog이 유용합니다.
서버 측 테스트 시나리오
각 엔드포인트에 대해 실패 사례별 시나리오를 작성하세요.
- 인증 누락:
401 - 권한 부족:
403 - 음수 금액:
422,errors[0].code = AMOUNT_NOT_POSITIVE - 트래픽 폭주:
429,Retry-After헤더 포함
Apidog의 시각적 어설션(assertion)을 사용하면 스크립트 없이 상태 코드, 헤더, 본문 필드를 확인할 수 있습니다. 전체 페이로드를 Problem Details JSON 스키마에 대해 검증하면 오류 형식의 변경이 프로덕션이 아닌 CI에서 먼저 실패합니다.
자세한 어설션 패턴은 API 어설션 가이드에서 확인할 수 있습니다.
클라이언트 측 목 서버
프런트엔드와 SDK 팀은 백엔드가 준비되기 전에 4xx 및 5xx 응답을 대상으로 개발할 수 있어야 합니다.
Apidog 목 서버는 API 사양에 정의된 Problem Details 본문을 그대로 반환합니다. 이를 활용해 다음 응답을 시뮬레이션할 수 있습니다.
-
Retry-After: 120이 포함된503 - 이중 제출에 대한
409 - 전체
errors[]유효성 검사 페이로드
이후 클라이언트가 오류를 어떻게 렌더링하고 재시도하는지 확인하세요. 수작업으로 Express 스텁을 만들거나 실패를 강제하기 위해 백엔드 코드를 수정할 필요가 없습니다.
오류 계약을 설계하고, 시나리오와 목(mock)에 인코딩한 뒤, 둘 다 CI에 통합하세요. Apidog를 다운로드해 무료로 사용해 볼 수 있습니다. 기존 OpenAPI 사양을 가져오면 몇 분 안에 목 가능한 오류 응답을 만들 수 있습니다.
자주 묻는 질문(FAQ)
유효성 검사 오류에는 400과 422 중 무엇을 사용해야 하나요?
서버가 요청 형식을 이해할 수 없을 때 400을 사용하세요. 예를 들어 유효하지 않은 JSON, 잘못된 콘텐츠 타입, 필수 필드 누락 등이 해당합니다.
요청은 올바르게 파싱되었지만 값이 도메인 규칙을 위반할 때는 422를 사용하세요. 음수 결제 금액이나 지원되지 않는 통화가 예입니다.
실용적으로 422는 “데이터를 수정하세요”를, 400은 “요청 형식을 수정하세요”를 의미합니다. 어떤 기준을 선택하든 모든 엔드포인트에 일관되게 적용하세요.
application/problem+json은 무엇인가요?
HTTP API의 표준 JSON 오류 형식인 Problem Details를 위해 RFC 9457이 정의한 미디어 타입입니다.
이 콘텐츠 타입을 사용한 응답에는 다음 기본 멤버가 포함됩니다.
typetitlestatusdetailinstance
여기에 필드 수준 유효성 검사 실패를 위한 errors[] 같은 사용자 정의 확장을 추가할 수 있습니다. 전체 사양은 RFC 9457 설명서에서 확인할 수 있습니다.
클라이언트는 어떤 HTTP 오류를 자동으로 재시도해야 하나요?
429, 502, 503, 504는 지수 백오프와 지터를 사용해 재시도하세요. Retry-After 헤더가 있으면 반드시 준수해야 합니다.
500은 한 번의 신중한 재시도를 고려할 수 있습니다. 그 외의 4xx 응답은 일반적으로 재시도하지 마세요. 같은 요청은 같은 방식으로 다시 실패할 가능성이 높습니다.
변경을 유발하는 엔드포인트는 멱등성 키와 함께 사용해 재시도로 인한 이중 청구나 이중 생성을 방지하세요.
백엔드를 망가뜨리지 않고 API 오류 응답을 테스트하려면 어떻게 해야 하나요?
오류를 시뮬레이션하세요.
- API 사양에 정의된 정확한 4xx 및 5xx 본문을 반환하는 Apidog 목 서버를 사용합니다.
- 각 응답에 대한 클라이언트 렌더링과 재시도 동작을 확인합니다.
- 서버 측에서는 잘못된 페이로드, 인증 누락, 트래픽 폭주를 전송하는 테스트 시나리오를 작성합니다.
- 상태 코드, 헤더, 오류 본문 스키마에 대해 어설션합니다.
- 서버와 클라이언트 테스트를 모두 CI에서 실행합니다.
이렇게 하면 누구도 수동으로 실패를 강제하지 않아도 오류 계약을 지속적으로 검증할 수 있습니다.
Top comments (0)