Skip to content
블로그로 돌아가기
튜토리얼

HTTP 상태 코드 치트시트: 1xx-5xx 전체 코드 완전 정리

HTTP 상태 코드 1xx부터 5xx까지 완벽 정리: 실제 예제, 401 vs 403 같은 흔한 실수, SEO 영향까지 한눈에. 지금 온라인 치트시트를 확인하세요.

14 분 소요

HTTP 상태 코드 치트시트: 1xx-5xx 전체 코드 완전 정리

DevTools를 열었더니 Network 탭이 절반은 빨갛게 물들어 있습니다. 같은 엔드포인트가 프로덕션에서는 502를 뱉는데 로컬에서는 200이고, Slack에서 동료가 방금 “이거 401이어야 해, 403이어야 해?”라고 묻습니다. HTTP 상태 코드는 세 자리 숫자에 다섯 개의 분류로 단순해 보이지만, 잘못 고르면 정보가 새고 SEO가 망가지며 온콜 당직자의 새벽이 길어집니다.

이 치트시트는 실제 운영 환경에서 마주치는 모든 코드를 정리한 빠른 참조 표, 자주 헷갈리는 짝(301 vs 302, 401 vs 403, 404 vs 410, 502 vs 504)에 대한 의사결정 매트릭스, curl·fetch·Python requests로 상태 코드를 확인하는 방법을 담았습니다. 아래 모든 코드는 현행 HTTP 시맨틱스 표준인 RFC 9110IANA HTTP Status Code Registry에 근거합니다.

빠른 참조: HTTP 상태 코드 한눈에 보기

프로덕션에서 만나게 될 코드를 클래스별로 정리했습니다.

코드이름마주치는 상황
100ContinueExpect: 100-continue 헤더와 함께 큰 POST 본문을 보낼 때
101Switching ProtocolsWebSocket 핸드셰이크, HTTP/2 업그레이드
103Early Hints본 응답 전에 서버가 Link 헤더를 미리 푸시
200OKGET·PUT·PATCH의 기본 성공 응답
201Created리소스를 생성한 POST(Location 헤더 반환)
202Accepted비동기 작업이 큐에 들어갔지만 아직 완료 전
204No ContentDELETE 성공, 본문이 없는 PUT
206Partial Content범위 요청, 동영상 시크, 이어받기 다운로드
301Moved Permanently옛 URL 폐기, 검색 엔진이 링크 자산을 이전
302Found임시 리다이렉트, 원본 URL이 여전히 정규(canonical)
303See Other폼 POST 후의 Post/Redirect/Get 패턴
304Not ModifiedETag 또는 If-Modified-Since가 일치하는 조건부 GET
307Temporary Redirect302와 비슷하지만 메서드와 본문이 보존됨
308Permanent Redirect301과 비슷하지만 메서드와 본문이 보존됨
400Bad Request잘못된 JSON, 필수 필드 누락, 스키마 실패
401Unauthorized자격증명이 없거나 토큰이 만료됨
403Forbidden인증은 됐지만 권한 없음
404Not Found리소스가 존재하지 않음(또는 숨기고 있음)
405Method Not AllowedGET 전용 엔드포인트에 POST(Allow 헤더 필수)
408Request Timeout클라이언트가 요청 전송에 너무 오래 걸림
409Conflict낙관적 잠금 실패, 키 중복
410Gone영구 삭제된 리소스, 다시 돌아오지 않음
415Unsupported Media Type잘못된 Content-Type, 예: JSON API에 XML 전송
422Unprocessable Content문법은 맞지만 의미가 잘못됨(검증 오류)
425Too EarlyTLS 1.3 early-data 재생 위험
428Precondition Required서버가 잃어버린 갱신을 막기 위해 If-Match를 요구
429Too Many Requests속도 제한(Retry-After 헤더 필수)
451Unavailable for Legal ReasonsDMCA, GDPR 삭제 요청, 지역 차단
500Internal Server Error코드에서 처리되지 않은 예외
501Not Implemented메서드나 기능 미지원(REST에서는 드묾)
502Bad Gateway업스트림이 잘못된 응답을 반환
503Service Unavailable점검 모드 또는 과부하
504Gateway Timeout업스트림이 제때 응답하지 않음
507Insufficient StorageWebDAV에서 디스크가 가득 참
508Loop DetectedWebDAV에서 무한 리다이렉트 또는 재귀 발생
511Network Authentication Required호텔·공항 WiFi의 캡티브 포털

아래 본문은 각 클래스를 의사결정 매트릭스, 안티패턴, 잘못 골랐을 때의 SEO 영향과 함께 다룹니다.

HTTP 상태 코드의 동작 원리(세 자리 숫자의 해부학)

왜 세 자리인가?

HTTP 상태 코드가 십진수 세 자리인 이유는, HTTP/0.9가 파서가 빠르게 분기할 수 있을 만큼 작으면서도 새로운 코드를 추가할 여유가 있는 고정 폭 신호를 필요로 했기 때문입니다. 세 자리면 100~999, 900가지 값을 표현할 수 있고, IANA 레지스트리가 오늘날 사용하는 코드는 약 60개입니다.

첫 번째 숫자는 클래스이고, 두 번째와 세 번째 숫자가 그 클래스 내의 구체적인 코드입니다. 418을 모르는 클라이언트는 일반적인 4xx로 처리해야 합니다. RFC 9110 §15는 이 규칙을 못 박습니다. 클라이언트는 인식하지 못한 코드를 해당 클래스의 x00처럼 다뤄야 합니다.

다섯 가지 분류 한눈에 보기

클래스의미본문 필요?기본 캐시 가능?
1xx정보(중간 응답, 뒤에 더 옴)아니요아니요
2xx성공(요청을 이해하고 수락)대개 있음메서드에 따라 다름
3xx리다이렉트(추가 동작 필요)선택301·308은 가능, 302·307은 불가
4xx클라이언트 오류(요청을 고쳐야 함)예(원인 설명)일반적으로 불가
5xx서버 오류(서버 측 문제, 재시도가 도움될 수 있음)예(원인 설명)아니요

“기본 캐시 가능” 칸을 주의 깊게 보세요. CDN과 브라우저는 301308을 영구히 캐시합니다. 프로덕션에서 잘못된 리다이렉트 코드를 골랐다면, 사용자가 로컬에 캐시를 쥐고 있는 한 되돌리기 어렵습니다. SEO 섹션에서 자세히 살펴봅니다.

리다이렉트가 작동하는 대상인 URL 구조는 URL 인코딩/디코딩 가이드에서 퍼센트 인코딩, 쿼리 문자열, URL 유효성을 결정하는 바이트 수준 파이프라인과 함께 다룹니다.

1xx — 정보(실제로 마주치는 순간)

대부분의 개발자는 1xx를 직접 볼 일이 거의 없습니다. 서버가 클라이언트에게 “아직 여기 있다, 계속 진행하라”고 알리는 중간 응답이기 때문입니다. 브라우저 DevTools는 보통 이를 숨기고, 대부분의 HTTP 라이브러리는 최종 응답에 합쳐 버립니다.

각 코드의 교차 참조는 MDN HTTP 응답 상태 레퍼런스에서 확인할 수 있습니다.

100 Continue

클라이언트가 헤더에 Expect: 100-continue를 보내고 큰 요청 본문을 전송하기 전에 기다립니다. 서버가 본문을 받아들일 의향이 있으면 100 Continue로 응답하고, 거절할 거라면 4xx를 보냅니다. 큰 업로드의 대역폭을 아낄 수 있습니다. 서버가 헤더 누락으로 어차피 거절할 텐데 200MB를 굳이 보낼 이유가 없습니다.

curl -v -H "Expect: 100-continue" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @big-file.bin \
  https://api.example.com/upload

verbose 출력에서 < HTTP/1.1 100 Continue가 보이지 않는다면, 클라이언트가 헤더를 제거했거나 서버가 지원하지 않는 것입니다.

101 Switching Protocols

HTTP 연결을 WebSocket이나 HTTP/2 연결로 바꾸는 핸드셰이크입니다. 클라이언트가 Upgrade: websocket을 보내면 서버는 101 Switching Protocols로 응답하고, 그 시점부터 연결은 다른 프로토콜로 통신합니다. 채팅 앱, 실시간 대시보드, 협업 도구의 Network 탭에서 볼 수 있습니다.

103 Early Hints

RFC 8297(2017)로 추가된 코드입니다. 본 응답이 준비되기 에 서버가 프리로드 힌트용 Link 헤더를 미리 보낼 수 있게 해줍니다. 서버가 아직 렌더링 중일 때 브라우저는 CSS와 JS를 받기 시작합니다. 2026년 현재 Cloudflare, Fastly, Vercel이 프로덕션에서 103을 지원합니다. Chrome에서 폐기된 HTTP/2 서버 푸시를 대체하는 자리입니다.

HTTP/1.1 103 Early Hints
Link: </styles.css>; rel=preload; as=style
Link: </app.js>; rel=preload; as=script

HTTP/1.1 200 OK
Content-Type: text/html
...

기대하던 1xx 코드가 클라이언트에 전혀 도달하지 않는다면, 원인은 대개 리버스 프록시입니다. 구버전 nginx는 Expect: 100-continue103 Early Hints를 지워 버립니다. 서버를 의심하기 전에 프록시 설정부터 확인하세요.

2xx — 성공(200을 넘어서)

REST API에서 모든 응답에 200 OK만 반환하는 것이 흔한 코드 스멜입니다. 2xx 계열은 클라이언트의 분기 로직과 캐시 효율을 높이는 의미 정보를 담습니다.

200 OK

기본값. GET은 리소스를, PUT은 갱신된 리소스를(또는 204), PATCH는 패치된 리소스를 반환합니다. 더 구체적인 코드를 쓸 이유가 없다면 200을 사용하세요.

201 Created

새 리소스를 생성하는 POST201과 함께 새 리소스를 가리키는 Location 헤더를 반환해야 합니다. RESTful 클라이언트는 이 방식으로 방금 만든 리소스의 정규 URL을 알아냅니다.

HTTP/1.1 201 Created
Location: /api/users/42
Content-Type: application/json

{"id": 42, "name": "Ada Lovelace"}

202 Accepted

서버가 요청을 받았지만 처리는 아직 끝나지 않은 상태입니다. 비동기 작업에 사용하세요. 클라이언트는 폴링하거나 웹훅을 구독하거나 상태 엔드포인트를 확인합니다. 본문에 작업 ID를 함께 담는 것이 일반적입니다.

204 No Content

성공이지만 본문이 없습니다. DELETE(리소스가 사라졌으므로 반환할 것이 없음)와 클라이언트가 이미 새 상태를 알고 있는 PUT에서 흔히 씁니다. 폼 제출이 204를 반환하면 브라우저는 현재 페이지를 유지합니다. SPA의 fire-and-forget 액션에 유용합니다.

206 Partial Content

범위 요청에 대한 응답입니다. 클라이언트가 Range: bytes=1000-2000 헤더로 1000~2000바이트를 요청하면 서버는 그 부분만 반환합니다. 동영상 스트리밍, 이어받기 다운로드, HTTP 기반 파일 동기화가 206에 의존합니다.

결정: POST에는 200·201·204 중 무엇을?

시나리오코드본문
POST가 새 리소스를 생성201 Created새 리소스(또는 ID만) + Location
POST가 비동기 작업을 시작, 결과 미준비202 Accepted작업 ID, 폴링 URL
POST가 리소스 없는 액션(예: /login)200 OK액션 결과(토큰, 상태)
POST 성공이지만 응답 본문이 빔204 No Content(없음)

200201 중 망설인다면 기준은 하나입니다. 서버가 자체 URL을 가진 리소스를 새로 만들었으면 201, 아니면 200입니다.

3xx — 리다이렉트(301 vs 302 vs 307 vs 308)

리다이렉트는 가장 잘못 쓰이는 클래스입니다. 301, 302, 307, 308의 차이는 세 가지 직교 질문으로 정리됩니다. 이동이 영구적인가, 메서드가 보존되는가, 응답이 캐시 가능한가.

301 Moved Permanently

리소스가 이동했고 돌아오지 않습니다. 검색 엔진은 새 URL로 링크 자산을 이전합니다. 브라우저와 CDN은 301을 사실상 무한히 캐시합니다. /old/new301로 리다이렉트한 뒤 결정을 뒤집으면, 캐시를 가진 사용자는 캐시를 비울 때까지 계속 /new로 이동합니다.

브라우저는 과거에 301에서 요청 메서드를 다시 쓰는 일이 있었고(POST → GET), HTTP/1.1은 이를 고치기 위해 308을 도입했습니다.

302 Found

임시 리다이렉트입니다. 원본 URL이 여전히 정규(canonical)이고, 검색 엔진은 원본을 계속 색인해야 합니다. A/B 테스트 라우팅, 점검 페이지, “계속하려면 로그인” 흐름에 사용하세요.

301과 마찬가지로 브라우저는 과거 302에서 POST를 GET으로 다시 썼습니다. POST를 리다이렉트하면서 POST를 유지해야 한다면 307을 쓰세요.

303 See Other

항상 메서드를 GET으로 다시 씁니다. Post/Redirect/Get 패턴에서 폼이 /submit에 POST하고 서버가 Location: /thank-you와 함께 303을 반환하면, 브라우저는 GET /thank-you를 수행합니다. 감사 페이지를 새로고침해도 폼이 다시 제출되지 않습니다. 303이 설계된 용도가 바로 이것입니다.

304 Not Modified

조건부 응답. 클라이언트가 If-None-Match: "abc123"(또는 If-Modified-Since)를 보내면 서버는 리소스 변경 여부를 확인하고, 변경되지 않았다면 본문 없이 304를 반환합니다. 브라우저는 캐시된 사본을 씁니다. CDN과 캐시 계층이 이 메커니즘으로 사이트의 응답 속도를 유지합니다.

307 Temporary Redirect

302와 비슷하지만 메서드가 절대 바뀌어서는 안 됩니다. POST는 POST로, 본문도 보존됩니다. GET이 아닌 요청에 임시 리다이렉트가 필요할 때 쓰세요.

308 Permanent Redirect

301과 비슷하지만 메서드가 바뀌어서는 안 됩니다. POST·PUT을 받는 API의 영구 리다이렉트에 더 안전한 현대적 선택입니다.

결정 매트릭스: 어떤 리다이렉트 코드를?

영구(영원히 캐시)임시(캐시하지 않음)
메서드가 GET으로 바뀌어도 됨301 Moved Permanently302 Found
메서드가 그대로 유지돼야 함308 Permanent Redirect307 Temporary Redirect

특수 케이스: POST → GET을 명시적으로 원한다면(Post/Redirect/Get 패턴) 303 See Other를 사용하세요.

브라우저 내비게이션이 있는 HTML 페이지에서는 GET이 GET이므로 301302가 보통 무난합니다. API와 폼에서는 메서드가 예기치 않게 바뀌는 일을 피하기 위해 308307을 선호하세요.

4xx — 클라이언트 오류(올바른 코드 고르기)

4xx는 클라이언트가 무언가 잘못했다는 뜻입니다. 4xx 어휘가 풍부하면 클라이언트가 오류 문자열을 파싱하지 않고 코드만으로 분기할 수 있어서 API가 쓰기 쉬워집니다.

400 Bad Request

일반적인 문법 오류. 잘못된 JSON, 구조적 차원에서 누락된 필수 필드, 서버가 아예 파싱조차 못 하는 요청이 해당됩니다. 요청은 파싱되지만 비즈니스 검증에서 실패한다면 422가 더 적합합니다.

401 Unauthorized vs 403 Forbidden

HTTP에서 가장 헷갈리는 짝이지만 구분은 단순합니다.

  • 401 Unauthorized: 요청에 유효한 인증이 빠져 있습니다. 서버는 사용자가 누구인지 모릅니다. 자격증명을 다시 보내거나 토큰을 갱신하면 해결될 수 있습니다. 응답에는 RFC 9110 §15.5.2에 따라 WWW-Authenticate 헤더가 반드시 포함돼야 합니다.
  • 403 Forbidden: 서버가 사용자를 식별했지만 거부합니다. 요청을 다시 보내도 소용없습니다. 다른 자격증명이나 다른 권한이 필요합니다.
보이는 응답의미
401WWW-Authenticate: Bearer토큰 없음, 만료, 또는 무효
로그인 성공 후 403로그인은 됐지만 이 사용자는 이 리소스에 접근 불가
로그인 성공 후 401버그, 아마 403이 맞을 것

안티패턴: 403을 404로 위장. 일부 사이트는 인증되지 않은 사용자가 /admin/dashboard를 요청하면 403을 반환합니다. 이 경우 /admin/dashboard의 존재가 노출됩니다. GitHub는 비공개 저장소에 대해 멤버가 아닌 사용자에게 404를 반환하는 방식으로 처리합니다. 해당 사용자 입장에서 리소스는 “존재하지 않는” 것입니다. 의도된 정보 은닉이며 버그가 아닙니다.

404 Not Found vs 410 Gone

둘 다 “이 리소스는 여기 없다”고 말합니다. 차이는 영구성과 SEO입니다.

  • 404 Not Found: 있을 수도, 없을 수도, 돌아올 수도 있습니다. 검색 엔진은 계속 확인합니다.
  • 410 Gone: 있었지만 의도적으로 제거됐고 돌아오지 않습니다. 검색 엔진이 색인에서 훨씬 빨리 제거합니다.

상품 페이지를 삭제하고 Google 색인에서 즉시 빼고 싶다면 410이 정답입니다. 일시적으로 깨진 URL이라면 404로 충분합니다.

405 Method Not Allowed

URL은 존재하지만 이 메서드를 받지 않습니다. 응답에는 지원하는 메서드를 나열한 Allow 헤더가 반드시 포함돼야 합니다.

HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD, OPTIONS
Content-Type: application/json

{"error": "POST is not allowed on this endpoint"}

Allow 헤더 누락은 직접 만든 REST API에서 가장 흔한 계약 위반입니다.

408 Request Timeout

클라이언트가 요청을 보내기 시작했다가 침묵에 빠진 경우입니다. 서버가 포기합니다. 업스트림 문제인 504 Gateway Timeout과 다릅니다. 408은 “클라이언트가 너무 오래 걸렸다”는 뜻입니다.

409 Conflict

요청이 현재 상태와 충돌합니다. 가장 흔한 용법은 낙관적 잠금입니다. 클라이언트가 If-Match: "etag-v3"을 보냈는데 서버의 현재 ETag가 "etag-v4"라면, 갱신은 409로 거부됩니다.

410 Gone

위 참조. 영구 삭제이며, 색인에서 소프트 삭제된 레코드를 빼는 데 유용합니다.

415 Unsupported Media Type

클라이언트가 서버가 이해하지 못하는 본문을 보냈습니다. JSON 전용 API에 XML을 POST하면 415가 돌아옵니다. 응답은 받아들일 수 있는 타입을 힌트로 알려주는 것이 좋습니다.

422 Unprocessable Content

요청은 잘 파싱되지만 의미 검증에 실패합니다. RFC 9110은 2022년에 마침내 이 코드를 WebDAV에서 핵심 스펙으로 승격했습니다. 검증 오류에는 422를 쓰세요.

{
  "error": "validation_failed",
  "details": [
    {"field": "email", "message": "must be a valid email"},
    {"field": "age", "message": "must be at least 13"}
  ]
}

400422 사이에서 망설인다면 기준은 이렇습니다. 400은 “파싱조차 안 된다”, 422는 “파싱은 됐지만 의미가 맞지 않다”.

425 Too Early

서버가 TLS 1.3 early-data 재생일 가능성이 있는 요청을 처리할 위험을 감수하고 싶지 않을 때 보냅니다. 주로 CDN과 리버스 프록시에서 의미가 있습니다.

428 Precondition Required

서버가 잃어버린 갱신 문제를 막기 위해 If-MatchIf-Unmodified-Since를 반드시 보내라고 요구합니다. 협업 편집 API에서 사용됩니다.

429 Too Many Requests

속도 제한. 응답에는 행동 양식이 좋은 클라이언트가 물러설 수 있도록 Retry-After(초 단위 또는 HTTP 날짜)가 반드시 포함돼야 합니다.

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json

{"error": "rate_limited", "limit": 100, "window": "1m"}

숫자는 브래드버리(Ray Bradbury)에 대한 오마주입니다. DMCA 삭제 요청, GDPR 잊힐 권리 행사, 국가 단위 지역 차단이 451의 실제 용처입니다. RFC 7725에 따라 응답에는 차단을 요구하는 법적 권한을 가리키는 Link 헤더가 포함돼야 합니다.

418 I’m a Teapot (이스터에그)

RFC 2324는 1998년 만우절 농담이었지만, 농담으로 구현한 제품이 너무 많아 IETF가 공식 문서에 남겨두었습니다. 실제 API에 418을 내보내지는 마세요. 많은 리버스 프록시와 로드 밸런서가 이를 잘못 처리합니다.

결정 매트릭스: 어떤 4xx?

상황코드
본문이 잘못됐거나 파싱 불가400
인증 없음 / 토큰 만료401
인증은 됐지만 권한 없음403
URL이 존재하지 않음(또는 숨김)404
URL이 존재했고 의도적으로 삭제됨410
잘못된 HTTP 메서드405 (Allow 헤더 포함)
잘못된 Content-Type415
낙관적 잠금 충돌409
검증 오류(파싱은 되지만 검증 실패)422
속도 제한429 (Retry-After 포함)
법적 사유로 차단451

5xx — 서버 오류(진짜로 망가진 부분)

5xx는 “서버 잘못”입니다. 온콜 엔지니어가 새벽 3시에 자기를 깨운 5xx에 가장 민감한 까닭은, 코드가 어느 계층부터 들여다봐야 할지 알려주기 때문입니다.

500 Internal Server Error

만능 응답. 처리되지 않은 예외가 프레임워크의 기본 핸들러까지 거슬러 올라간 결과인 경우가 많습니다. 원인에 대해서는 아무것도 알려주지 않으므로, 이 경우엔 상태 코드보다 구조화된 로그가 더 중요합니다.

501 Not Implemented

서버가 해당 메서드를 아예 지원하지 않습니다. “이 URL에서 이 메서드를 허용하지 않는다”는 405와 다릅니다. 501은 “이 서버는 PROPFIND가 무엇인지조차 모른다”는 뜻입니다. REST API에서는 드뭅니다.

502 Bad Gateway

리버스 프록시나 로드 밸런서가 업스트림으로부터 잘못된 응답을 받았습니다. 업스트림이 응답은 했지만 잘못된 프로토콜, 망가진 헤더, 응답 도중 끊긴 연결 같은 결과였습니다. CDN에서 502가 보인다면 오리진이 크래시 중이거나 잘린 본문을 반환하고 있을 가능성이 높습니다.

503 Service Unavailable

서버가 지금은 요청을 처리하지 않기로 결정한 상태입니다. 점검 시간이나 과부하 시의 우아한 응답에 사용하세요. Retry-After를 함께 보내야 합니다.

504 Gateway Timeout

리버스 프록시가 업스트림을 기다렸지만 업스트림이 제때 응답하지 않았습니다. 업스트림이 느리거나 멈췄다는 신호이며, 업스트림이 잘못된 응답을 보낸 502와는 다릅니다.

502 vs 504: 온콜 진단법

보이는 응답가장 먼저 확인할 곳
502 Bad Gateway업스트림이 잘못된 데이터로 응답 중. 오리진 로그에서 크래시, 망가진 응답, 프로토콜 불일치 확인
504 Gateway Timeout업스트림이 멈춰 있음. 오리진 CPU, DB 쿼리, 다운스트림 API 호출, 프록시의 proxy_read_timeout 확인

흔한 혼동: 60초 걸리는 데이터베이스 쿼리는 프록시 타임아웃이 30초라면 504로 표면화되고, 앱 서버 타임아웃이 90초라 예외를 던진다면 500으로 나타납니다. 같은 근본 원인이지만 코드와 로그 줄이 다릅니다. 대시보드가 두 경우 모두를 잡도록 설정해 두세요.

507 Insufficient Storage

WebDAV 전용입니다. 서버 디스크가 가득 찼습니다. WebDAV가 아닌 API에서 이 코드가 보인다면 누군가 의미를 임의로 확장해 쓰고 있는 것입니다.

508 Loop Detected

WebDAV PROPFIND 작업의 무한 재귀. 매우 드뭅니다.

511 Network Authentication Required

캡티브 포털 코드입니다. 호텔이나 공항의 WiFi가 브라우저에 “포털에서 먼저 로그인하라”고 알리려고 511을 보냅니다. 응답에는 포털 페이지를 가리키는 Location이 포함됩니다.

트러블슈팅 매트릭스: 어느 계층부터 확인할까

코드프록시DB네트워크
500가능(잡히지 않은 DB 오류)
502예(업스트림 응답 형식 오류)가능(TCP reset)
503예(점검 플래그)예(rate-limit 거부)
504예(느린 핸들러)예(타임아웃 설정)예(느린 쿼리)예(DNS, 패킷 손실)

흔한 HTTP 상태 코드 안티패턴

다음 다섯 가지 실수가 코드 리뷰에서 가장 자주 잡히는 잘못된 사용입니다.

1. 오류를 200 OK로 감싸기

HTTP/1.1 200 OK
{"success": false, "error": "user_not_found"}

이러면 모니터링 도구, CDN, 캐시가 요청이 성공했다고 판단합니다. 재시도 로직은 작동하지 않고, 상태 코드를 인식하는 로드 밸런서는 잘못된 트래픽을 “정상” 백엔드로 라우팅합니다. JSON-RPC에서 시작해 GraphQL이 물려받은 패턴이며, GraphQL은 부분 성공을 위해 필드별 오류 보고가 필요해서 정당화됩니다. REST에는 정당화 근거가 없습니다. 클라이언트 오류엔 4xx, 서버 오류엔 5xx를 쓰고 구조화된 상세는 본문에 담으세요.

2. 401과 403을 섞어 쓰기

401403이 일관되지 않으면 공격자는 API를 탐색해 어느 리소스가 존재하는지 알아낼 수 있습니다. 정책을 정해두세요. “볼 수 없는 리소스”에 대해 GitHub처럼 404를 쓰거나, 일관되게 403을 쓰거나 둘 중 하나입니다. 일관성 부재가 정보를 흘립니다.

3. 403을 404 뒤에 숨기기

옳을 때도 있고 버그일 때도 있습니다. GitHub가 비공개 저장소에 404를 반환하는 것은 의도적이며, 저장소의 존재 자체가 민감 정보이기 때문입니다. 반면 “이 사용자 계정은 정지됐다”에 대해 404를 반환한다면, 정상 사용자는 사용자명을 잘못 쳤는지 정지된 것인지 알 길이 없습니다. 정책을 문서화하고 일관되게 적용하세요.

4. 500을 기본 catch로 사용

프레임워크가 이 패턴을 너무 쉽게 만들어 둡니다. 잡히지 않은 모든 예외가 500이 되면 알림 체계는 “데이터베이스 다운”과 “사용자가 잘못된 UUID를 보냄”을 구분하지 못합니다. 검증 오류는 잡아서 400이나 422로 올리세요. ORM의 NotFound는 잡아서 404로 올리세요. 500은 정말 예상치 못한 실패에 남겨두고, 이를 던질 때는 요청 ID를 함께 로깅해 상관관계를 추적할 수 있게 하세요.

5. 긴 리다이렉트 체인

홉마다 왕복 비용이 듭니다. /old/intermediate/canonical이라면 최악의 경우 DNS 조회 두 번과 TCP 핸드셰이크 두 번이 추가됩니다. Google은 3홉이 넘는 체인에 대해 크롤 우선순위를 낮추고, 브라우저는 무한 루프를 막기 위해 리다이렉트 체인을 약 20개로 제한합니다. 체인은 발원지(CDN 설정이나 애플리케이션의 리다이렉트 맵)에서 한 번에 평탄화하세요.

HTTP 상태 코드와 SEO

검색 엔진은 상태 코드를 URL을 유지·제거·이전할지에 대한 권위 있는 신호로 다룹니다. 잘못 고르면 순위가 움직입니다.

301 vs 302 (링크 자산)

301 Moved Permanently는 PageRank를 이전합니다. Google은 새 URL을 옛 URL이 받던 모든 신호의 정규 도착지로 봅니다. 302 Found는 링크 자산을 이전하지 않거나, Google의 휴리스틱에 따라 천천히 이전합니다. URL을 영구히 바꿨다면 301, 게스트를 /login으로 보내는 거라면 302입니다.

404 vs 410 vs 소프트 404

Google은 “없음” 상태를 세 가지로 구분합니다.

  • 404 Not Found: Google이 주기적으로 다시 확인하며 한동안 색인에 URL을 유지합니다.
  • 410 Gone: Google이 URL을 더 빨리, 종종 한 번의 크롤 사이클 안에 제거합니다.
  • 소프트 404: 200 OK를 반환하면서 본문에 “찾을 수 없다”는 메시지를 표시하는 페이지를 가리키는 Google의 용어입니다. Google은 콘텐츠 패턴으로 이를 감지해 404로 처리하지만, 그 사이 크롤 요청이 낭비되고 진짜 콘텐츠가 희석될 수 있습니다.

낡은 색인을 정리한다면 영구 삭제된 URL에는 진짜 410을 반환하세요.

5xx와 크롤 예산

Google 크롤러는 사이트가 지속적으로 5xx를 반환할 때 크롤 속도를 줄입니다. Search Console의 Crawl Stats 보고서에 그 흐름이 나타납니다. 5xx 오류가 며칠 내내 치솟으면 크롤 예산이 며칠간 떨어지고, 새 페이지가 색인되기까지 더 오래 걸립니다. 5xx 비율은 안정성 지표인 동시에 SEO 지표입니다.

사실상 망가진 200 OK

오류 페이지에 200 OK를 반환하는 소프트 404 안티패턴은 SEO 관점에서 최악입니다. Google은 오류 메시지를 색인하고, 그 페이지가 어디에도 랭크되지 못한 채 시간이 흘러 망가졌음이 드러납니다. SPA가 친절한 오류 UI를 렌더링하더라도 서버에서는 항상 올바른 상태 코드를 반환하세요.

HTTP 상태 코드를 들여다보는 방법(도구)

상태 코드를 보지 못하면 고칠 수 없습니다. 다음 도구 중 최소 셋은 익숙해 두세요.

브라우저 DevTools Network 패널

Chrome, Firefox, Safari가 Network 탭에 Status 열을 표시합니다. 보이지 않는다면 열 헤더를 우클릭해 Status Text를 추가하세요. 알아두면 좋은 기능:

  • Preserve log: 페이지 이동 후에도 항목을 유지해 전체 리다이렉트 체인을 확인.
  • Filter by status: Chrome에서 status-code:5xx를 입력하면 서버 오류만 표시.
  • Replay XHR: 요청을 우클릭 → Replay XHR로 페이지를 새로고침하지 않고 재실행.

리다이렉트는 요청을 펼치면 모든 홉과 각 단계의 상태 코드를 볼 수 있습니다.

curl(만능 해결사)

curl은 회선의 실제 동작을 그대로 보여줍니다. 디버깅의 대부분을 처리하는 세 가지 패턴:

# Just the status code
curl -o /dev/null -s -w "%{http_code}\n" https://api.example.com/users/1

# Headers only (HEAD request, follow redirects)
curl -I -L https://example.com

# Full verbose with request and response headers
curl -v https://api.example.com/users/1

쿼리 문자열에 특수 문자가 들어간 테스트 URL을 만들 때는 curl이 인코딩을 처리하도록 --data-urlencode를 쓰거나, URL 인코더 디코더에 URL을 붙여 넣어 회선에 실리는 실제 바이트를 확인하세요.

# curl encodes the query value for you
curl -G "https://api.example.com/search" \
  --data-urlencode "q=hello world & friends"

# Sends: GET /search?q=hello%20world%20%26%20friends

JavaScript fetch

Response.status 속성이 정수 코드를 담고 있습니다. Response.ok는 모든 2xx에 대해 true입니다.

const res = await fetch('https://api.example.com/users/1');

console.log(res.status);      // 200
console.log(res.statusText);  // "OK"
console.log(res.ok);          // true

if (!res.ok) {
  if (res.status === 401) {
    // refresh token and retry
  } else if (res.status === 429) {
    const retryAfter = Number(res.headers.get('Retry-After')) || 1;
    await new Promise(r => setTimeout(r, retryAfter * 1000));
  } else if (res.status >= 500) {
    throw new Error(`Server error: ${res.status}`);
  }
}

axios에서는 같은 로직을 인터셉터에 둡니다.

import axios from 'axios';

axios.interceptors.response.use(
  response => response,
  error => {
    const status = error.response?.status;
    if (status === 401) {
      // redirect to login
    }
    return Promise.reject(error);
  }
);

Python requests

import requests

r = requests.get('https://api.example.com/users/1')

print(r.status_code)  # 200
print(r.reason)       # 'OK'

# Raises requests.exceptions.HTTPError for 4xx/5xx
r.raise_for_status()

# Manual handling
if r.status_code == 429:
    retry_after = int(r.headers.get('Retry-After', '1'))
    time.sleep(retry_after)
elif 500 <= r.status_code < 600:
    raise RuntimeError(f'Server error: {r.status_code}')

raise_for_status()는 “4xx/5xx에서는 시끄럽게 실패하라”는 Python 관용구입니다. status_code로 분기하는 대신 오류에서 예외를 받고 싶은 스크립트에서 쓰세요.

Postman과 Bruno

테스트 스크립트 안에서 상태 코드를 단언할 수 있습니다.

// Postman/Bruno test script
pm.test("Status is 201", () => {
  pm.response.to.have.status(201);
});

pm.test("Has Location header", () => {
  pm.expect(pm.response.headers.get('Location')).to.match(/^\/users\/\d+$/);
});

CI에서 스테이징을 대상으로 이런 테스트를 돌리면 프로덕션 전에 계약 위반을 잡습니다.

FAQ

401과 403의 차이는 무엇인가요?

401 Unauthorized는 서버가 사용자를 식별하지 못한다는 뜻이고, 자격증명이 없거나 만료됐거나 무효입니다. 403 Forbidden은 서버가 사용자를 식별했지만 거부한다는 뜻입니다. 다른 자격증명을 보내면 해결될 가능성이 있으면 401, 아니면 403입니다.

301과 302는 언제 써야 하나요?

이동이 영구적이고 검색 엔진이 새 URL로 링크 자산을 이전하기를 바란다면 301을 씁니다. 원본 URL이 정규인 임시 리다이렉트(로그인 흐름, A/B 테스트, 점검 페이지)에는 302를 씁니다. API에서는 요청 메서드를 보존하는 308307이 더 안전합니다.

502 Bad Gateway 오류는 무엇을 의미하나요?

502는 리버스 프록시나 로드 밸런서가 업스트림 서버로부터 잘못된 응답을 받았다는 뜻입니다. 업스트림이 응답은 했지만 잘못된 프로토콜, 망가진 헤더, 끊긴 연결 같은 결과였습니다. 업스트림이 아예 응답하지 않은 504 Gateway Timeout과는 다릅니다. 먼저 확인할 곳은 오리진 서버 로그의 크래시나 잘린 응답입니다.

”소프트 404”란 무엇인가요?

“소프트 404”는 200 OK를 반환하면서 본문에 “찾을 수 없다”는 메시지를 보여주는 페이지입니다. Google이 휴리스틱으로 감지해 404로 취급하지만, 크롤 예산을 낭비하고 진짜 콘텐츠를 희석할 수 있습니다. SPA가 친절한 오류 UI를 렌더링하더라도 서버에서는 항상 진짜 404410 상태 코드를 반환하세요.

422는 400 대신 언제 써야 하나요?

서버가 요청을 파싱조차 못 할 때(잘못된 JSON, 구조 필드 누락, 문법 오류)는 400 Bad Request를 씁니다. 요청은 파싱되지만 비즈니스 검증에 실패할 때(잘못된 이메일 형식, 범위를 벗어난 값, 의미가 모순되는 필드)는 422 Unprocessable Content를 씁니다. 한 줄 요약: 400은 문법, 422는 의미.

429 Too Many Requests에는 어떻게 응답해야 하나요?

Retry-After 헤더(초 단위 또는 HTTP 날짜)를 읽고 그만큼은 기다린 뒤 재시도하세요. Retry-After가 없으면 1초 정도부터 시작하는 지터(jitter) 포함 지수 백오프를 사용합니다. 즉시 재시도하면 차단당합니다.

1xx 정보 응답은 2026년에도 여전히 사용되나요?

대체로 애플리케이션 코드에서는 보이지 않지만 사용됩니다. 100 Continue101 Switching Protocols는 HTTP/1.1의 기본 기능입니다. 103 Early Hints는 본 응답 전에 프리로드 힌트를 푸시하기 위해 Cloudflare, Fastly, Vercel에서 점점 더 많이 사용되며, Largest Contentful Paint를 개선합니다. HTTP 라이브러리가 1xx를 최종 응답에 합치기 때문에 보통 DevTools나 curl -v에서 마주치게 됩니다.

418 “I’m a teapot”는 진짜 상태 코드인가요?

RFC 2324는 1998년 만우절 농담이었지만 농담으로 구현한 제품이 너무 많아 IETF가 RFC 7168에 공식 등록을 유지했습니다. 프로덕션에 418을 내보내지는 마세요. 많은 리버스 프록시와 로드 밸런서가 이를 제대로 처리하지 못하고, 농담 외에는 실용적 용도가 없습니다.