REST API 오류 처리 베스트 프랙티스: 상태 코드, RFC 9457 및 재시도 가능한 오류

REST API 오류 처리 마스터하기: 올바른 상태 코드 선택, RFC 9457 문제 세부 정보 반환, 재시도 가능한 오류 표시, 그리고 Apidog로 모든 실패 테스트하기.

INEZA Felin-Michel

INEZA Felin-Michel

31 August 2026

REST API 오류 처리 베스트 프랙티스: 상태 코드, RFC 9457 및 재시도 가능한 오류

Apidog 엔터프라이즈

온프레미스 배포

SSO & RBAC

SOC 2 준수

Apidog Enterprise 살펴보기

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을 반환하는 것은 리소스가 존재한다는 사실을 노출시킵니다. 둘째, Retry-After가 없는 429는 클라이언트가 끊임없이 요청을 보내도록 학습시킵니다. 요청 제한(rate limit)을 사용한다면(사용해야 합니다), 상태 코드와 함께 구체적인 후퇴(backoff) 신호를 제공해야 합니다. API 요청 제한(rate limiting) 구현 방법에 대한 저희 가이드는 관련 헤더 계산 및 알고리즘을 다룹니다.

하나의 오류 본문 형태: RFC 9457 문제 상세 정보 (Problem Details)

상태 코드가 올바르게 설정되면, API가 반환하는 모든 오류는 하나의 미디어 타입과 하나의 스키마를 공유해야 합니다. 표준적인 답변은 application/problem+json으로 제공되는 RFC 9457 문제 상세 정보(Problem Details)입니다. 이는 다섯 가지 핵심 멤버를 정의합니다: type (오류 카테고리를 식별하는 URI), title (간략한 사람이 읽을 수 있는 요약), status (편의를 위해 반복되는 HTTP 코드), detail (이 특정 발생에서 무엇이 잘못되었는지), 그리고 instance (이 특정 실패에 대한 URI). 그 외의 모든 것은 사용자가 직접 정의하는 확장 멤버에 포함됩니다.

여기에서 이 사양을 다시 설명하지는 않겠습니다. 저희의 RFC 9457 설명서는 모든 멤버, 등록 규칙, 그리고 RFC 7807을 어떻게 대체하는지 자세히 설명합니다. 계약에서 중요한 것은 패턴입니다: 표준 엔벨로프(envelope), 사용자 정의 확장. 다음은 결제 엔드포인트에서의 유효성 검사 실패 예시입니다.

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을 받는 클라이언트는 여전히 두 개의 파서를 작성해야 합니다.

기계가 읽을 수 있는 코드 vs 사람이 읽을 수 있는 메시지

예시에서 codemessage 필드가 모두 포함되어 있음을 주목하세요. 이는 의도적인 것입니다. 이들은 서로 다른 대상에게 서비스를 제공하며, 하나의 문자열로 결합되어서는 안 됩니다.

기계가 읽을 수 있는 코드(AMOUNT_NOT_POSITIVE, CURRENCY_UNSUPPORTED, IDEMPOTENCY_KEY_REUSED)는 계약의 일부입니다. 클라이언트는 이 코드들을 기반으로 분기하므로, 이 코드들은 안정적이고 문서화되어야 하며 열거 가능해야 합니다. 클라이언트가 산문(prose)을 파싱하게 만들지 마십시오. 누군가 if (message.includes("positive"))라고 작성하는 순간, 메시지 수정은 호환성을 깨는 변경(breaking change)이 됩니다.

사람이 읽을 수 있는 메시지는 그 반대입니다. 언제든지 개선할 수 있으며, 로그를 읽는 개발자를 위해 작성되고, 핵심 로직에 영향을 주지 않습니다. 무엇이 실패했는지, 그리고 어떻게 수정해야 하는지 명시하세요: "금액은 소액 단위의 양의 정수여야 합니다"가 "유효하지 않은 금액"보다 훨씬 좋습니다. 현지화한다면 메시지를 현지화하고 코드는 그대로 두십시오.

API 소비자에 자율 에이전트가 포함되면서 이러한 구분은 더욱 중요해졌습니다. LLM 기반 클라이언트는 구조화되고 자체 설명적인 오류로부터 훨씬 더 잘 복구됩니다. 이 관점에 대해서는 AI 에이전트를 위한 API 오류 설계에서 다룹니다.

오류 응답에 절대 포함되어서는 안 되는 것

오류 응답은 처리되지 않은 실패가 장황한 경향이 있기 때문에 공격자들에게 선호되는 정찰 채널입니다. 오류 미들웨어는 다음 중 어떤 것도 클라이언트에 도달하지 않도록 보장해야 합니다:

패턴은 간단합니다. 경계에서 모든 것을 포착하고, 요청 ID와 함께 서버 측에서 전체 예외를 기록하며, 동일한 ID를 가진 일반적인 문제 상세 정보(Problem Details) 본문을 반환합니다. 클라이언트는 "detail": "내부 오류가 발생했습니다", "request_id": "req_51ad0"를 받고, 로그에는 실제 정보가 기록되며, 지원 팀은 이 둘을 연결할 수 있습니다.

오류를 재시도 가능 또는 최종(terminal)으로 표시

API가 반환하는 모든 오류는 클라이언트가 곧 물어볼 질문에 답해야 합니다: 다시 시도해야 할까요? 각 클라이언트 팀이 추측하게 두는 대신, 이 답을 계약에 포함시키세요.

상태 코드는 기본 의미론을 가집니다. 429, 502, 503, 504는 지수 백오프(exponential backoff)와 지터(jitter)를 사용하여 재시도할 수 있습니다. 500은 모호하지만 일반적으로 한 번의 조심스러운 재시도를 할 가치가 있습니다. 거의 모든 다른 4xx 코드는 최종(terminal)입니다. 동일한 요청으로 401, 403, 404 또는 422를 재시도하는 것은 할당량을 낭비하고 로그를 오염시킵니다. 타임아웃은 클라이언트가 포기한 후에도 요청이 성공했을 수 있으므로 특별한 주의가 필요합니다. 이는 고전적인 408 요청 타임아웃 문제이며, 변경을 유발하는 엔드포인트가 멱등성 키(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 하위 코드를 재시도하면 상태가 손상되기 때문에 최종(terminal)으로 표시하는 것과 같이 필요한 경우 기본값을 재정의할 수 있습니다. 이 플래그를 한 번 문서화하면, 출시하는 모든 클라이언트 SDK는 일관된 백오프 동작을 갖게 됩니다.

상관 관계 ID 및 오류 계약 버전 관리

두 가지 작은 결정 사항이 계약을 완성하며, 이들은 지금은 비용이 적게 들지만 나중에는 비싸집니다.

모든 요청에 ID를 부여하십시오. 들어오는 X-Request-Id 헤더를 수락(또는 생성)하고, 모든 로그 라인에 찍고, 모든 오류 본문에 request_id로 반영하십시오. 고객이 지원 티켓에 오류를 붙여넣을 때, 그 한 필드만으로 한 시간 동안의 로그 탐색이 단일 쿼리로 바뀝니다. 분산 환경에서는 W3C traceparent를 함께 전파하여 ID가 여러 서비스에 걸쳐 요청을 따라갈 수 있도록 하십시오.

API 자체처럼 오류 계약에 버전을 적용하십시오. 새로운 확장 멤버나 새로운 오류 코드를 추가하는 것은 안전합니다. errors[].field의 이름을 바꾸거나, 코드의 의미를 변경하거나, 임시(ad-hoc) 형태에서 문제 상세 정보(Problem Details)로 전환하는 것은 호환성을 깨는 변경(breaking change)이며, 팀이 가장 적게 테스트하는 코드 경로를 망가뜨립니다. type URI는 깔끔한 메커니즘을 제공합니다. 기존 type URI는 영구적으로 안정적으로 유지하고, 새로운 의미를 위해 새로운 URI를 도입하며, 문서에는 알 수 없는 확장 멤버와 알 수 없는 코드는 실패로 처리하지 않고 무시해야 한다고 명시하십시오. 이러한 전방 호환성 조항 덕분에 v2 없이도 발전할 수 있습니다.

Apidog에서 모든 오류 경로 테스트

불편한 진실은 다음과 같습니다. 오류 계약은 아무도 이를 테스트하지 않기 때문에 부패합니다. 정상적인 흐름(happy path)은 모든 데모에서 실행되지만, 422 분기는 고객이 해당 오류를 만났을 때만 실행됩니다. 해결책은 테스트 스위트에서 실패 사례를 일등 시민으로 만드는 것이며, 바로 이 지점에서 Apidog가 워크플로에서 중요한 역할을 합니다.

두 가지 기능이 이 문제에 직접적으로 적용됩니다.

오류 계약을 설계하고, 이를 시나리오와 목(mock)으로 인코딩하며, 둘 다 CI에 통합하십시오. Apidog를 다운로드하여 무료로 사용해보십시오. 기존 OpenAPI 사양을 가져오면 몇 분 만에 목(mock) 가능한 오류 응답을 얻을 수 있습니다.

버튼

자주 묻는 질문 (FAQ)

유효성 검사 오류에 400 또는 422 중 무엇을 사용해야 하나요?

요청 형식이 잘못되어 서버가 이해할 수 없을 때 400을 사용하십시오. 예를 들어, 유효하지 않은 JSON, 잘못된 콘텐츠 타입, 필수 필드 누락 등입니다. 요청은 올바르게 파싱되었지만, 음수 결제 금액이나 지원되지 않는 통화처럼 값이 도메인 규칙을 위반할 때 422를 사용하십시오. 실용적인 이점은 진단입니다. 422는 클라이언트에게 "데이터를 수정하세요"라고 말하는 반면, 400은 "요청 형식을 수정하세요"라고 말합니다. 어떤 구분을 선택하든 모든 엔드포인트에 일관되게 적용하십시오.

application/problem+json은 무엇인가요?

이는 HTTP API의 표준 JSON 오류 형식인 문제 상세 정보(Problem Details)에 대해 RFC 9457이 정의한 미디어 타입입니다. 이 콘텐츠 타입을 가진 응답은 type, title, status, detail, instance 멤버와 함께 필드 수준 유효성 검사 실패를 위한 errors[] 배열과 같은 사용자가 정의하는 모든 확장을 포함합니다. 등록된 미디어 타입을 사용하면 일반 클라이언트와 미들웨어가 사용자 정의 구성 없이 오류를 인식할 수 있습니다. 저희의 RFC 9457 설명서는 전체 사양을 다룹니다.

클라이언트는 어떤 HTTP 오류를 자동으로 재시도해야 하나요?

429, 502, 503, 504 오류는 지수 백오프(exponential backoff)와 지터(jitter)를 사용하여 재시도하며, Retry-After 헤더가 있으면 이를 준수하십시오. 500은 한 번의 신중한 재시도를 할 가치가 있다고 간주하십시오. 다른 4xx 응답은 재시도하지 마십시오. 요청은 매번 동일한 방식으로 실패할 것입니다. 변경을 유발하는 엔드포인트의 경우, 재시도를 멱등성 키(idempotency keys)와 결합하여 재실행된 요청이 이중 청구되거나 이중 생성되지 않도록 하십시오.

백엔드를 망가뜨리지 않고 API 오류 응답을 테스트하려면 어떻게 해야 하나요?

시뮬레이션하십시오. 클라이언트를 API 사양에 있는 정확한 4xx 및 5xx 본문을 반환하는 Apidog 목 서버로 지정한 다음, 각각에 대해 렌더링 및 재시도 동작을 확인하십시오. 서버 측에서는 유효하지 않은 페이로드, 인증 누락, 트래픽 폭주를 전송하는 테스트 시나리오를 작성한 다음, 상태 코드, 헤더 및 오류 본문 스키마에 대해 어설션하십시오. 양쪽 모두 CI에서 실행되므로, 누구도 수동으로 실패를 강제하지 않고도 오류 계약이 제대로 유지됩니다.

Apidog에서 API 설계-첫 번째 연습

API를 더 쉽게 구축하고 사용하는 방법을 발견하세요