REST API를 설계할 때 상태 코드는 '문서를 읽지 않고도 응답을 이해하게 해 주는 계약'입니다. 잘 고른 코드는 클라이언트가 재시도할지, 사용자에게 무엇을 보여줄지, 캐시할지를 정확히 판단하게 해 줍니다. 반대로 무엇이든 200으로 내려보내고 본문에 성공 여부 플래그를 숨기는 API는 클라이언트가 매번 본문을 파싱해야 하고, HTTP 인프라(캐시·프록시·모니터링)의 이점을 잃습니다.
성공 응답: 200 / 201 / 204
- 200 OK: 일반적인 성공.
GET으로 리소스를 반환하거나,PUT/PATCH로 갱신 후 갱신된 표현을 본문에 담아 돌려줄 때. - 201 Created: 새 리소스가 생성됨.
POST로 컬렉션에 항목을 추가했을 때 쓰고, 생성된 리소스의 위치를Location헤더에 담습니다. - 204 No Content: 성공했지만 돌려줄 본문이 없음.
DELETE성공이나, 응답 본문이 필요 없는PUT갱신에 적합합니다.
POST /v1/users HTTP/1.1
Content-Type: application/json
{"email":"a@example.com"}
HTTP/1.1 201 Created
Location: /v1/users/u_92311
Content-Type: application/json
{"id":"u_92311","email":"a@example.com"}
클라이언트 오류: 400 / 401 / 403 / 404 / 409 / 422 / 429
- 400 Bad Request: 요청 자체가 문법적으로 잘못됨(깨진 JSON, 필수 파라미터 누락 등).
- 401 Unauthorized: 인증 정보가 없거나 유효하지 않음.
WWW-Authenticate헤더로 인증 방식을 알립니다. - 403 Forbidden: 인증은 됐지만 그 작업을 할 권한이 없음.
- 404 Not Found: 대상 리소스가 없음. 존재 자체를 숨기고 싶을 때 403 대신 404를 쓰기도 합니다.
- 409 Conflict: 현재 리소스 상태와 충돌(중복 생성, 낙관적 잠금 버전 불일치 등).
- 422 Unprocessable Entity: 문법은 맞지만 의미상 검증에 실패(이메일 형식 오류, 값 범위 초과 등).
- 429 Too Many Requests: 레이트 리밋 초과.
Retry-After헤더로 언제 다시 시도할지 알립니다.
400과 422의 구분은 실무에서 자주 헷갈립니다. 관례적으로 400은 '서버가 요청을 파싱조차 못 함'(깨진 본문)이고, 422는 '파싱은 됐지만 필드 값이 검증 규칙을 위반'하는 경우입니다. 팀 안에서 규칙을 한 번 정하고 API 전체에서 일관되게 지키는 것이 개별 코드 선택보다 중요합니다.
서버 오류와 재시도: 500 / 503
500 Internal Server Error는 예상치 못한 서버 예외를 가리키는 포괄 코드입니다. 503 Service Unavailable은 과부하나 점검 등으로 일시적으로 처리할 수 없음을 뜻하며, Retry-After로 복구 예상 시각을 알릴 수 있습니다. 5xx와 429는 클라이언트가 지수 백오프(exponential backoff)로 재시도해도 되는 대표적 코드입니다.
멱등성 키로 안전하게 재시도하기
네트워크가 불안정하면 POST로 결제나 주문을 만들 때 '요청은 도달했는데 응답을 못 받은' 상황이 생깁니다. 이때 그냥 재시도하면 중복 생성 위험이 있습니다. 해결책은 클라이언트가 Idempotency-Key 헤더에 고유 값을 실어 보내고, 서버가 같은 키의 요청을 한 번만 실행한 뒤 이후 동일 키 요청에는 저장해 둔 원래 응답을 그대로 돌려주는 것입니다.
POST /v1/charges HTTP/1.1
Idempotency-Key: 3f9c1b7a-2e5d-4a11-9c8e-77b0a1d2e4f6
Content-Type: application/json
{"amount":5000,"currency":"krw"}
일관성은 상태 코드 설계의 최종 목표입니다. 같은 종류의 상황에는 항상 같은 코드를 쓰고, 오류 본문 형식(예: error 객체 안에 code·message 필드를 두는 형태)을 표준화하며, 문서에 코드별 의미를 명시하세요. 그러면 클라이언트 개발자가 방어 코드를 예측 가능하게 작성할 수 있습니다.