이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 좋은 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 status | HTTP 수준 분류 | 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-Type이application/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가 언제 재시도해야 하는지 알 수 있어 장애 중 과도한 재시도를 줄일 수 있기 때문이다.