
API를 만들 때 오류가 나면 무조건 500을 주거나, 반대로 무조건 200에 {"success": false}를 담아 보내는 경우가 있다. 둘 다 나중에 문제가 된다. 이 글은 HTTP 상태 코드를 실제 상황에 어떻게 대응시킬지와, 오류 응답 본문을 어떻게 설계할지를 정리한다.
200에 실패를 담으면 안 되는 이유
먼저 가장 흔한 안티패턴부터. 모든 응답을 200으로 주고 본문에 성공 여부를 담는 방식이다.
HTTP/1.1 200 OK
{"success": false, "message": "권한이 없습니다"}
동작은 한다. 문제는 HTTP 계층의 도구들이 전부 무력해진다는 것이다.
로드 밸런서와 모니터링은 상태 코드로 오류율을 센다. 전부 200이면 오류율이 0%로 보인다. 실제로는 절반이 실패하고 있어도 대시보드는 초록색이다. CDN과 브라우저 캐시도 200을 캐시 가능한 응답으로 취급한다. 클라이언트 라이브러리의 재시도 로직도 동작하지 않는다.
상태 코드는 기계가 읽는 계층이고 본문은 사람과 애플리케이션이 읽는 계층이다. 둘을 겹쳐 쓰면 기계가 쓰는 계층을 버리는 셈이다.
실제로 쓰게 되는 코드는 열 개 남짓
상태 코드가 수십 개지만 실무에서 손이 가는 것은 많지 않다.
| 코드 | 쓰는 상황 |
|---|---|
| 200 OK | 조회·수정 성공 |
| 201 Created | 생성 성공. Location 헤더에 새 리소스 위치 |
| 202 Accepted | 접수했고 처리는 비동기 |
| 204 No Content | 성공했고 돌려줄 본문이 없음(주로 삭제) |
| 400 Bad Request | 요청 형식·값이 잘못됨 |
| 401 Unauthorized | 인증이 없거나 실패 |
| 403 Forbidden | 인증은 됐지만 권한 없음 |
| 404 Not Found | 리소스가 없음 |
| 409 Conflict | 현재 상태와 충돌(중복 생성, 버전 불일치) |
| 422 Unprocessable Content | 형식은 맞지만 의미가 잘못됨 |
| 429 Too Many Requests | 속도 제한. Retry-After 헤더 필수 |
| 500 Internal Server Error | 서버 버그 |
| 503 Service Unavailable | 일시적 과부하·점검 |
401과 403의 구분이 자주 틀린다. 401은 "네가 누군지 모르겠다"이고 403은 "누군지 알지만 안 된다"다. 401을 받으면 클라이언트는 재로그인을 시도하고, 403을 받으면 권한 요청 안내를 보여준다. 이 구분이 흐려지면 클라이언트가 무한 재로그인 루프에 빠진다.
400과 422의 구분은 팀마다 다르다. 나는 JSON 파싱이 안 되거나 필수 필드가 없으면 400, 형식은 맞는데 값이 규칙에 어긋나면(종료일이 시작일보다 빠름) 422를 쓴다. 어느 쪽이든 팀 안에서 통일하는 것이 중요하다.
404를 보안 목적으로 쓰는 경우도 있다. 남의 리소스에 접근했을 때 403을 주면 "그 리소스는 존재한다"는 정보가 새어 나간다. 민감한 경우 404로 감춘다. 의도적인 선택이라는 점을 문서에 남겨두면 좋다.

오류 본문에는 무엇을 담을 것인가
상태 코드만으로는 부족하다. 클라이언트가 무엇을 해야 할지 알려면 본문이 필요하다.
표준으로는 RFC 9457이 정의한 Problem Details 형식이 있다.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://docs.example.com/errors/invalid-date-range",
"title": "날짜 범위가 올바르지 않습니다",
"status": 422,
"detail": "종료일(2026-09-01)이 시작일(2026-09-10)보다 앞섭니다",
"instance": "/reservations/8821",
"errors": [
{"field": "endDate", "code": "before_start_date"}
]
}
핵심은 두 가지다. 기계가 분기할 수 있는 코드(type이나 code)와 사람이 읽을 설명(detail)을 나누는 것이다. 클라이언트가 메시지 문자열을 비교해 분기하게 만들면, 문구를 바꾸는 순간 클라이언트가 깨진다.
필드 단위 오류를 배열로 주는 것도 중요하다. 폼 검증에서 오류가 셋이면 셋을 한 번에 줘야 사용자가 한 번에 고친다. 하나씩 돌려주면 사용자가 세 번 제출해야 한다.
그리고 내부 정보를 담지 않는다. 스택 트레이스, SQL 문, 파일 경로가 오류 응답에 나가면 공격자에게 정보를 주는 것이다. 로그에는 자세히 남기고 응답에는 추적 ID만 준다.
{"title": "일시적인 오류가 발생했습니다", "status": 500, "traceId": "a3f9c1"}
사용자가 문의하면 그 ID로 로그를 찾는다.
헤더도 계약의 일부다
몇 가지 헤더는 반드시 챙기는 것이 좋다.
429에는 Retry-After를 붙인다. 언제 다시 시도하면 되는지 알려주지 않으면 클라이언트가 임의로 재시도하고, 그게 부하를 더한다.
201에는 Location을 붙인다. 생성된 리소스의 URL을 알려준다.
Cache-Control을 명시한다. 안 주면 중간 프록시가 임의로 판단한다. 개인화된 응답이 캐시되어 남에게 보이는 사고가 실제로 일어난다.
교과서적 정답보다 일관성이 낫다
마지막으로 관점 하나. 상태 코드 논쟁은 끝이 없다. "부분 성공은 207인가", "삭제된 리소스는 404인가 410인가" 같은 질문에 정답을 찾으려 하면 시간만 쓴다.
내 기준은 이렇다. 흔한 경우는 관례를 따르고, 애매한 경우는 팀에서 정해 문서에 적는다. 그리고 그 결정을 전체 API에 일관되게 적용한다.
클라이언트 입장에서 최악은 "이 API는 404를 주는데 저 API는 200에 빈 객체를 준다"는 상황이다. 어느 쪽이 더 옳은지보다 예측 가능한 것이 훨씬 가치 있다. 오류 응답 형식을 공통 미들웨어로 만들어 두면 일관성이 저절로 지켜진다. 사람마다 다르게 쓰는 것을 막는 가장 확실한 방법이다.
참고 자료
- HTTP response status codes — MDN — 상태 코드별 정의와 사용 맥락
- RFC 9457: Problem Details for HTTP APIs — 오류 응답 본문 표준 형식
- RFC 6585: Additional HTTP Status Codes — 429 와
Retry-After정의
'IT' 카테고리의 다른 글
| 정규화와 비정규화, 어디까지 나눌 것인가 (0) | 2026.10.27 |
|---|---|
| 알림 피로를 줄이는 SLO 기반 모니터링 (0) | 2026.10.25 |
| 환경 변수와 시크릿 관리, 어디까지 해야 하나 (0) | 2026.10.24 |
| 메시지 큐를 언제 써야 하고 언제 쓰면 안 되나 (0) | 2026.10.23 |
| 트랜잭션 격리 수준과 데드락 줄이기 (0) | 2026.10.22 |