이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 좋은 API 응답이 status code, error code, message, details, request id, retry 정보를 어떻게 함께 담는가?
  • client와 운영자가 같은 오류를 같은 방식으로 분류하도록 응답 계약을 어떻게 설계하는가?
  • API 응답 변경이 backward compatibility, monitoring, retry, UX에 어떤 영향을 주는가?

개요

API 응답 계약은 client와 server, 운영자와 on-call이 같은 사건을 같은 언어로 이해하게 만드는 약속이다. 좋은 응답은 status code만 맞추는 것이 아니라 error code, request id, retry 가능성, 사용자 조치 여부, 문서 링크까지 고려한다.

  • 좋은 API는 상태 코드, error code, message, trace id가 일관된다.
  • 운영자는 이 계약으로 사용자 제보를 로그와 연결한다.
  • 클라이언트가 retry할 수 있는지, 사용자 수정이 필요한지 응답에서 드러나야 한다.

왜 백엔드 개발자가 알아야 하는가

응답 계약이 나쁘면 장애가 늦게 발견되고, client가 잘못 retry하고, 사용자는 해결할 수 없는 메시지를 받는다. 운영자는 status와 body가 모순되는 API에서 원인 분류에 시간을 낭비한다.

  • 모든 실패를 200 success=false로 보내면 monitoring이 장애를 놓친다.
  • 사용자 입력 오류를 500으로 보내면 서버 장애율이 부풀고 client가 재시도할 수 있다.
  • 429/503에 retry 정보를 주지 않으면 client가 장애를 증폭한다.
  • error code가 없으면 client가 localized message나 분기 처리를 안정적으로 하기 어렵다.
  • request id가 없으면 고객 문의를 로그와 연결하기 어렵다.

요청 흐름에서의 위치

응답 계약은 장애가 사용자에게 드러나는 마지막 표면이다.

domain error
→ HTTP status
→ error code
→ user/developer message
→ details
→ request id / trace id
→ retry hint

이 매핑이 일관되어야 client SDK, frontend, monitoring, support, on-call이 같은 판단을 한다.

원리

권장되는 오류 응답 요소는 다음과 같다.

필드/헤더의미예시
HTTP statusHTTP 수준 분류400, 409, 429, 503
code서비스 도메인 오류 코드ORDER_ALREADY_PAID
message/detail사람이 이해할 설명Order is already paid.
requestId로그 연결req-abc123
details필드별 오류 등 구조화 정보validation field list
Retry-After재시도 힌트60

RFC 9457 Problem Details는 HTTP API 오류 응답의 표준 형식으로 활용하기 좋다.

핵심 판단 기준

  • status code와 body code가 모순되면 안 된다.
  • client가 수정할 수 있는 오류와 재시도해야 하는 오류를 구분한다.
  • public message와 internal log message를 분리한다.
  • error schema는 versioning과 backward compatibility를 고려한다.
  • 응답에는 request id를 포함하고, 같은 id가 server log에 남아야 한다.

실무에서 자주 만나는 문제

  • 모든 실패를 200과 success=false로 보낸다.
  • 500에 사용자 입력 오류를 섞는다.
  • 429/503에 Retry-After를 주지 않는다.
  • field validation 오류가 문자열 하나라 frontend가 필드별 표시를 못 한다.
  • 같은 오류가 endpoint마다 다른 code와 message를 쓴다.
  • error response가 HTML로 내려와 mobile client JSON parser가 실패한다.
  • 새 필드를 필수로 추가해 오래된 client가 깨진다.
  • user message에 내부 table name이나 exception message가 노출된다.

응답 계약 문제는 장애가 아니라 “클라이언트 버그”처럼 보이기도 한다. 하지만 여러 client가 같은 응답을 다르게 해석한다면 API 계약이 불명확한 것이다.

디버깅 방법

  • status code와 body code가 맞는지 확인한다.
  • Content-Typeapplication/json 또는 Problem Details media type인지 본다.
  • request id가 response header/body와 server log에 모두 있는지 확인한다.
  • retry 가능한 오류에 Retry-After나 retryable hint가 있는지 본다.
  • validation error가 field 단위로 구조화되어 있는지 본다.

코드로 확인하기

문제 있는 응답과 좋은 응답의 차이를 비교한다.

{
  "success": false,
  "message": "error"
}

이 응답은 status와 error code, request id, 조치 가능성이 없다.

{
  "type": "https://docs.example.com/errors/validation-failed",
  "title": "Validation failed",
  "status": 400,
  "detail": "One or more fields are invalid.",
  "code": "VALIDATION_FAILED",
  "requestId": "req-20260630-001",
  "details": [
    {"field": "quantity", "reason": "must be greater than 0"}
  ]
}

Spring에서는 Problem Details를 기반으로 만들 수 있다.

ProblemDetail detail = ProblemDetail.forStatus(HttpStatus.TOO_MANY_REQUESTS);
detail.setTitle("Too many requests");
detail.setDetail("Please retry later.");
detail.setProperty("code", "RATE_LIMITED");
detail.setProperty("requestId", requestId);
 
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
    .header(HttpHeaders.RETRY_AFTER, "60")
    .body(detail);

주니어가 자주 하는 오해

  • HTTP status만 맞으면 응답 계약이 충분하다고 생각한다.
    • client는 domain error code와 구조화 details도 필요하다.
  • 사용자에게 exception message를 그대로 보여줘도 된다고 생각한다.
    • 내부 구조와 보안 정보가 노출될 수 있다.
  • error schema는 나중에 바꿔도 된다고 생각한다.
    • mobile client와 외부 연동은 오래된 schema에 의존할 수 있다.
  • 429/503은 client가 알아서 천천히 재시도한다고 생각한다.
    • retry hint가 없으면 client가 더 공격적으로 재시도할 수 있다.

시니어의 설계 판단 기준

  • error code catalog를 만들고 owner를 둔다.
  • 응답 schema는 backward compatible하게 확장한다. 필드 제거와 타입 변경은 신중하게 한다.
  • retryable 오류와 non-retryable 오류를 명확히 구분한다.
  • support/on-call이 request id로 로그를 찾을 수 있게 응답과 로그를 연결한다.
  • API gateway/WAF가 만든 오류도 가능한 한 같은 형식이나 식별 header를 제공하도록 맞춘다.

인프라 협업 포인트

  • gateway/WAF/LB에서 만든 오류 응답과 app 오류 응답을 구분할 header나 body 형식을 정한다.
  • 429/503의 Retry-After 정책을 gateway rate limit과 app overload 양쪽에서 맞춘다.
  • 사용자에게 보여 줄 오류 page/API error가 CDN fallback과 충돌하지 않는지 확인한다.
  • 인프라 담당자는 에러 형식을 맞추고 싶어도 제품별 기본 error page와 제한이 달라 조율이 필요하다.

실전 팁

  • API 문서에는 정상 예시보다 오류 예시를 더 자세히 둔다.
  • error code는 message보다 안정적인 계약으로 취급한다.
  • user-facing message와 developer-facing detail을 분리한다.
  • 외부 공개 API는 deprecation policy와 schema compatibility를 문서화한다.

위험 신호!

  • 모든 실패가 200으로 내려간다.
  • error code catalog가 없다.
  • request id 없이 고객 문의를 받는다.
  • 429/503에 retry hint가 없다.
  • 응답 schema 변경이 client compatibility 검토 없이 배포된다.

확인 질문

확인 질문

  • 모든 실패를 200 success=false로 보내면 왜 위험한가?
    • monitoring, retry, cache, client 분기에서 HTTP 실패를 감지하지 못해 장애가 숨겨질 수 있기 때문이다.
  • 좋은 오류 응답에 request id가 필요한 이유는 무엇인가?
    • 고객이 본 응답과 서버 로그를 연결해 원인 분석 시간을 줄일 수 있기 때문이다.
  • 429/503 응답에 Retry-After가 중요한 이유는 무엇인가?
    • client가 언제 재시도해야 하는지 알 수 있어 장애 중 과도한 재시도를 줄일 수 있기 때문이다.

참고 문서