HTTP 상태 코드 올바르게 쓰기: 400 vs 422, 401 vs 403, 200 vs 201 vs 204 헷갈릴 때 보는 정리
에러인데 200에 success: false를 담고 있다면, 클라이언트와 모니터링 도구가 실패를 알 수 없다. 상황별로 어떤 상태 코드를 써야 하는지, 401과 403, 400과 422처럼 헷갈리는 코드의 차이, 301과 308처럼 POST가 GET으로 바뀌는 리다이렉트 함정까지 정리했다.
실패했는데 200 OK?
이런 API 응답을 본 적이 있을 것이다.
HTTP/1.1 200 OK
{ "success": false, "message": "존재하지 않는 사용자입니다" }
요청은 실패했는데 상태 코드는 "성공"이다. 이렇게 하면 문제가 줄줄이 생긴다.
- 프론트엔드에서 fetch의 res.ok가 true라서, 모든 응답의 본문을 열어 success를 따로 확인해야 한다.
- 에러 모니터링, 로드밸런서, CDN이 실패를 실패로 인식하지 못한다. 대시보드의 에러율은 0%인데 사용자는 계속 실패한다.
- 실패 응답이 "성공"으로 캐시될 수도 있다.
HTTP 상태 코드는 응답 본문을 열어보지 않고도 결과가 어떤 종류인지 알려주는 약속이다. 브라우저, 프록시, 모니터링 도구, 클라이언트 라이브러리가 모두 이 숫자를 보고 동작을 정한다.
첫 자리만 알아도 절반은 맞힌다
| 범위 | 의미 | 누구 책임? |
|---|---|---|
| 2xx | 성공 | - |
| 3xx | 다른 곳으로 가라 (리다이렉트, 캐시 사용) | - |
| 4xx | 클라이언트가 잘못 요청함 | 요청을 고쳐야 함 |
| 5xx | 서버가 처리하다 실패함 | 서버를 고쳐야 함 |
가장 중요한 구분은 4xx와 5xx다. 입력값이 틀린 건 4xx, 서버 코드에서 예외가 터진 건 5xx다. 사용자의 잘못된 입력에 500을 돌려주면, 서버 장애 알림이 울려서 엉뚱한 사람이 새벽에 깬다.
성공: 200, 201, 204
| 코드 | 언제 | 예시 |
|---|---|---|
| 200 OK | 일반적인 성공, 본문 있음 | 목록 조회, 수정 후 결과 반환 |
| 201 Created | 새 리소스를 만들었을 때 | 회원가입, 게시글 작성 |
| 204 No Content | 성공했지만 돌려줄 본문이 없을 때 | 삭제, 본문 없는 수정 |
201을 쓸 때는 새로 만든 리소스의 주소를 Location 헤더에 담아주는 것이 관례다.
HTTP/1.1 201 Created
Location: /users/42
{ "id": 42, "name": "kim" }
204 응답에는 본문이 없다. 프론트엔드에서 습관처럼 await res.json()을 부르면 SyntaxError가 난다. 삭제 API처럼 204를 돌려주는 요청은 res.status === 204를 먼저 확인하자.
가장 헷갈리는 4xx 구분
400 vs 422: 형식이 틀렸나, 내용이 틀렸나
| 코드 | 의미 | 예시 |
|---|---|---|
| 400 Bad Request | 요청 자체를 해석할 수 없음 | JSON 문법이 깨짐, 필수 파라미터 타입이 아예 다름 |
| 422 Unprocessable Content | 형식은 맞는데 내용이 규칙에 어긋남 | 이메일 형식이 틀림, 비밀번호가 8자 미만 |
실무에서는 검증 실패를 전부 400으로 처리하는 팀도 많다. 그것도 틀린 건 아니다. 중요한 건 한 API 안에서 기준을 일관되게 지키는 것이다.
401 vs 403: 누군지 모르나, 알지만 안 되나
| 코드 | 의미 | 클라이언트가 할 일 |
|---|---|---|
| 401 Unauthorized | 누구인지 모름 (로그인 안 함, 토큰 만료) | 로그인 화면으로 보내거나 토큰 갱신 |
| 403 Forbidden | 누군지 알지만 권한이 없음 | 권한 없음 안내 (다시 로그인해도 소용없음) |
이름이 Unauthorized라서 헷갈리지만, 401은 실제로는 "인증(Authentication)이 안 됨"이라는 뜻이다. 이 둘을 섞어 쓰면, 권한이 없는 사용자가 로그인 화면으로 계속 튕겨 나가는 무한 루프가 생긴다.
404로 존재 자체를 숨기기
다른 사람의 비공개 게시글처럼 존재한다는 사실 자체가 정보인 경우, 403 대신 404를 돌려주기도 한다. 403은 "그런 게 있긴 한데 넌 못 본다"고 알려주는 셈이기 때문이다. GitHub도 권한 없는 비공개 저장소에 404를 돌려준다.
그 밖에 자주 쓰는 4xx
| 코드 | 언제 | 예시 |
|---|---|---|
| 404 Not Found | 리소스가 없음 | 삭제된 게시글 |
| 405 Method Not Allowed | 그 주소에서 지원하지 않는 메서드 | 조회 전용 주소에 DELETE |
| 409 Conflict | 현재 상태와 충돌 | 이미 사용 중인 이메일로 가입, 동시 수정 충돌 |
| 429 Too Many Requests | 요청 횟수 제한 초과 | 로그인 시도 과다 (Retry-After 헤더로 대기 시간 안내) |
아래 미리보기에서 상황을 골라보자.
5xx: 서버 쪽 실패도 종류가 있다
| 코드 | 의미 | 예시 |
|---|---|---|
| 500 Internal Server Error | 서버 코드에서 예상 못 한 에러 | 처리 안 된 예외 |
| 502 Bad Gateway | 중간 서버(프록시, 게이트웨이)가 뒤쪽 서버로부터 잘못된 응답을 받음 | 뒤쪽 앱 서버가 죽어서 연결이 끊김 |
| 503 Service Unavailable | 서버가 일시적으로 요청을 처리할 수 없음 | 점검 중, 과부하 |
| 504 Gateway Timeout | 중간 서버가 뒤쪽 서버의 응답을 기다리다 시간 초과 | 느린 DB 쿼리로 앱 서버가 응답을 못 함 |
502, 504는 보통 직접 보내는 코드가 아니라, Nginx나 로드밸런서 같은 중간 서버가 대신 돌려주는 코드다. 이 코드가 보이면 "앱 서버 앞단과 앱 서버 사이"를 의심하면 된다.
500 응답 본문에 에러 스택이나 DB 쿼리를 그대로 담지 말자. 공격자에게 내부 구조를 알려주는 셈이다. 자세한 내용은 서버 로그에 남기고, 응답에는 추적용 ID 정도만 담는 게 좋다.
3xx 함정: POST가 GET으로 바뀌는 리다이렉트
리다이렉트 코드는 "어디로 가라"만 다른 게 아니라, 원래 요청의 메서드를 유지하는지가 다르다. 실제로 POST 요청(본문 name=kim)을 각 코드로 리다이렉트해보면 이렇게 된다.
| 코드 | 의미 | 리다이렉트 후 요청 |
|---|---|---|
| 301 Moved Permanently | 영구 이동 | GET, 본문 사라짐 |
| 302 Found | 임시 이동 | GET, 본문 사라짐 |
| 303 See Other | 다른 곳을 GET으로 조회하라 | GET (의도된 동작) |
| 307 Temporary Redirect | 임시 이동, 메서드 유지 | POST, 본문 유지 |
| 308 Permanent Redirect | 영구 이동, 메서드 유지 | POST, 본문 유지 |
301과 302는 역사적인 이유로 브라우저가 POST를 GET으로 바꿔버린다. 그래서 API 주소를 옮기면서 301을 걸면, POST 요청의 본문이 조용히 사라진다.
- 페이지 주소 이전(SEO): 301 또는 308
- API 주소 이전: 메서드와 본문을 유지하는 308 (임시라면 307)
- 폼 제출 후 결과 페이지로 보내기(새로고침 시 중복 제출 방지): 303
정리: 왜 알아두면 좋은가
| 상황 | 상태 코드 |
|---|---|
| 조회 성공 | 200 |
| 생성 성공 | 201 + Location |
| 성공, 본문 없음 | 204 |
| 요청 형식 오류 | 400 |
| 검증 실패 | 422 (또는 일관되게 400) |
| 로그인 필요 / 토큰 만료 | 401 |
| 권한 없음 | 403 (존재를 숨기려면 404) |
| 중복, 상태 충돌 | 409 |
| 요청 제한 초과 | 429 + Retry-After |
| 서버 예외 | 500 |
| 일시적 점검, 과부하 | 503 |
| API 주소 이전 | 308 (301은 POST를 GET으로 바꿈) |
상태 코드를 제대로 쓰면 클라이언트는 본문을 열기 전에 다음 행동을 정할 수 있고(다시 로그인, 재시도, 입력 수정), 모니터링 도구는 실패를 실패로 센다. 에러 응답의 본문을 어떤 모양으로 줄지는 다음 글인 API 에러 응답 형식 설계에서 이어서 다룬다.