웹 개발자를 위한 HTTP 상태 코드 안내서

웹 개발자를 위한 HTTP 상태 코드 안내서

웹 개발자라면 200, 404, 500 같은 HTTP 상태 코드를 한 번쯤은 접해보셨을 텐데요. 자주 보는 코드의 뜻은 대강 알고 있어도, 막상 API를 설계하려면 어떤 코드를 골라야 할지 헷갈릴 때가 있습니다. 400422, 502504처럼 비슷해 보이는 코드도 있고요.

혹시 HTTP 메시지 구조나 버전 변천이 궁금하다면 HTTP 한눈에 보기에서 큰 그림을 먼저 살펴보셔도 좋습니다. 이번 글에서는 웹 개발에서 자주 만나는 HTTP 상태 코드를 범주별로 살펴보고, 서로 비슷한 코드를 어떤 기준으로 구분해야 하는지 알아보겠습니다.

HTTP 상태 코드가 중요한 이유

웹에서는 클라이언트가 요청을 보내면 서버가 이를 처리한 뒤 응답을 돌려줍니다. 요청이 성공할 때도 있지만, 클라이언트가 잘못된 값을 보내거나 서버가 일시적으로 과부하 상태에 빠질 수도 있죠. HTTP 상태 코드는 이 처리 결과를 클라이언트와 서버가 함께 이해할 수 있는 세 자리 숫자로 알려줍니다.

백엔드 개발자는 상황에 맞는 상태 코드를 응답해 서버에서 어떤 일이 일어났는지 알려줄 수 있습니다. 프런트엔드 개발자는 응답 본문(body)을 읽기 전에도 상태 코드를 보고 재시도, 로그인 이동, 오류 표시 같은 후속 처리를 결정할 수 있고요. 마이크로서비스 아키텍처(microservice architecture)에서는 이런 상호작용이 서버 사이에서도 빈번하게 일어납니다.

상태 코드는 디버깅(debugging)과 모니터링(monitoring)에도 중요한 기준이 됩니다. 많은 모니터링 도구가 상태 코드별로 요청을 집계하고, 5xx 응답 비율이 일정 수준을 넘으면 경고를 보냅니다. 서버가 처리 결과와 맞지 않는 코드를 돌려주면 클라이언트의 예외 처리가 복잡해지고 관측 지표도 왜곡될 수 있습니다.

상태 코드의 구조와 범주

HTTP 상태 코드는 세 자리 숫자이며, 첫 번째 숫자에 따라 다섯 범주로 나뉩니다. 모든 코드를 외우기는 어렵지만 범주를 알면 응답의 성격을 빠르게 짐작할 수 있습니다.

  • 1xx는 요청을 받았으며 처리가 계속되고 있음을 알리는 정보성 응답입니다.
  • 2xx는 요청을 성공적으로 받거나 처리했음을 나타냅니다.
  • 3xx는 다른 위치로 이동하거나 캐시를 재사용하는 등 추가 동작이 필요함을 나타냅니다.
  • 4xx는 요청에 문제가 있어 클라이언트 쪽에서 수정해야 함을 나타냅니다.
  • 5xx는 서버가 정상적인 요청을 처리하지 못했음을 나타냅니다.

다만 4xx라고 해서 항상 최종 사용자의 잘못인 것은 아닙니다. 클라이언트 애플리케이션이 잘못된 요청을 만들었거나 인증 정보가 만료된 상황도 여기에 포함됩니다. 마찬가지로 5xx는 재시도만으로 해결되지 않을 수 있으므로, 각 코드의 의미와 응답 헤더를 함께 살펴봐야 합니다.

그럼 웹 개발을 하면서 자주 접하는 HTTP 상태 코드를 하나씩 살펴볼까요?

101 Switching Protocols

101 Switching Protocols는 서버가 클라이언트의 요청에 따라 통신 프로토콜을 전환한다는 뜻입니다. 가장 익숙한 사례는 HTTP/1.1 연결을 웹소켓(WebSocket) 연결로 전환하는 과정인데요. 클라이언트가 Upgrade 헤더로 프로토콜 전환을 제안하고 서버가 이를 수락하면 101로 응답합니다.

실시간 양방향 통신에 사용하는 웹소켓은 웹소켓 한눈에 보기에서 자세히 다루고 있습니다.

103 Early Hints

103 Early Hints는 최종 응답을 준비하는 동안 클라이언트가 필요한 리소스를 미리 불러오도록 힌트를 주는 정보성 응답입니다. 예를 들어 서버는 HTML을 생성하기 전에 Link 헤더로 중요한 CSS나 글꼴을 알려줄 수 있습니다.

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

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

103은 최종 응답이 아닙니다. 클라이언트는 이 힌트를 받은 뒤에도 이어서 도착하는 200, 404 같은 최종 상태를 기다려야 합니다.

200 OK

200 OK는 요청이 성공적으로 처리되었음을 나타냅니다. GET 요청에서는 보통 요청한 표현(representation)을 응답 본문에 담아 반환하고, 다른 메서드에서는 처리 결과를 담을 수 있습니다.

성공한 모든 요청에 무조건 200을 사용하는 것은 피하는 편이 좋습니다. 리소스를 생성했다면 201, 비동기 처리를 접수했다면 202, 돌려줄 본문이 없다면 204가 처리 결과를 더 정확하게 표현할 수 있기 때문입니다.

201 Created

201 Created는 요청 처리 결과로 새로운 리소스가 생성되었음을 의미합니다. 예를 들어 POST /posts 요청으로 글이 생성되었다면 서버는 201로 응답할 수 있습니다.

새 리소스의 주소가 있다면 Location 응답 헤더에 담아주는 것이 좋습니다.

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

응답 본문에는 생성된 리소스나 이를 식별하는 정보를 함께 담을 수 있습니다.

202 Accepted

202 Accepted는 서버가 요청을 접수했지만 처리는 아직 끝나지 않았다는 뜻입니다. 동영상 변환, 대용량 보고서 생성, 메시지 큐 작업처럼 요청을 비동기로 처리할 때 유용합니다.

202는 작업이 결국 성공한다고 보장하지 않습니다. 따라서 클라이언트가 진행 상태를 확인할 수 있도록 작업 식별자와 상태 조회 URL을 응답하는 편이 좋습니다.

HTTP/1.1 202 Accepted
Location: /jobs/8f31
Content-Type: application/json

{"id":"8f31","status":"queued"}

204 No Content

204 No Content는 요청을 성공적으로 처리했지만 응답 본문으로 돌려줄 내용이 없음을 나타냅니다. 리소스를 삭제하는 DELETE 요청이나, 수정 결과를 다시 보낼 필요가 없는 PUTPATCH 요청에서 자주 사용합니다.

204 응답에는 메시지 본문을 넣을 수 없습니다. 또한 204 자체가 클라이언트 캐시의 리소스를 삭제하라는 뜻은 아닙니다. 캐시 무효화 여부는 요청 메서드와 캐시 규칙에 따라 판단해야 합니다.

206 Partial Content

206 Partial Content는 클라이언트가 요청한 리소스의 일부만 전송했음을 뜻합니다. 대용량 파일을 이어받거나 동영상의 특정 구간을 재생할 때 주로 볼 수 있습니다.

클라이언트는 Range 요청 헤더로 필요한 바이트 범위를 지정하고, 서버는 Content-Range 응답 헤더로 실제로 반환한 범위를 알려줍니다.

HTTP/1.1 206 Partial Content
Content-Range: bytes 1000-1999/5000
Content-Length: 1000

301 Moved Permanently

301 Moved Permanently는 요청한 리소스가 영구적으로 새로운 위치로 이동했음을 의미합니다. 새 URL은 Location 응답 헤더에 담습니다. 검색 엔진과 클라이언트는 이후 요청에서 새로운 URL을 사용할 수 있으므로 도메인 변경이나 URL 구조 개편에 적합합니다.

역사적인 호환성 때문에 일부 클라이언트는 POST 요청이 301 응답을 받으면 새 위치에 GET 요청을 보낼 수 있습니다. 요청 메서드와 본문을 그대로 유지해야 하는 영구 리디렉션이라면 308이 더 명확합니다.

302 Found

302 Found는 요청한 리소스가 일시적으로 다른 위치에 있음을 나타냅니다. 로그인하지 않은 사용자를 로그인 페이지로 보내거나 짧은 기간에만 다른 페이지를 보여줄 때 사용할 수 있습니다. 클라이언트는 다음 요청에서도 원래 URL을 사용해야 합니다.

301과 마찬가지로 일부 클라이언트는 POST 요청을 리디렉션하면서 메서드를 GET으로 바꿀 수 있습니다. 의도적으로 GET으로 전환하려면 303, 기존 메서드를 유지하려면 307을 사용하면 뜻이 더 분명해집니다.

303 See Other

303 See Other는 요청 처리 결과를 다른 URL에서 GET 또는 HEAD 요청으로 확인하라는 뜻입니다. 폼을 POST로 제출한 뒤 결과 페이지로 이동시키는 PRG(Post/Redirect/Get) 패턴에서 유용합니다.

HTTP/1.1 303 See Other
Location: /orders/123

클라이언트는 원래 요청이 POST였더라도 Location에 있는 주소를 GET으로 조회합니다. 덕분에 사용자가 결과 페이지를 새로고침했을 때 폼이 다시 제출되는 문제를 피할 수 있습니다.

304 Not Modified

304 Not Modified는 조건부 요청(conditional request)의 검증 결과, 클라이언트가 가진 캐시를 계속 사용해도 된다는 뜻입니다. 리소스가 생성된 뒤 한 번도 수정되지 않았다는 의미는 아닙니다.

클라이언트는 캐시에 저장한 ETag나 수정 시각을 각각 If-None-Match, If-Modified-Since 요청 헤더로 보냅니다. 서버는 조건에 맞으면 응답 본문 없이 304를 반환하고, 클라이언트는 저장해 둔 응답을 재사용합니다. 자세한 검증 흐름은 Cache-Control 헤더 정리에서 살펴볼 수 있습니다.

307 Temporary Redirect와 308 Permanent Redirect

307 Temporary Redirect308 Permanent Redirect는 리디렉션 과정에서 원래 요청 메서드와 본문을 유지합니다. POST 요청이었다면 새 위치에도 POST로 요청해야 합니다.

  • 307은 이동이 일시적이므로 이후 요청에서도 원래 URL을 사용합니다.
  • 308은 이동이 영구적이므로 이후 요청에서는 새 URL을 사용할 수 있습니다.

메서드가 바뀌면 안 되는 API 엔드포인트를 옮길 때는 301302보다 이 두 코드가 안전한 선택입니다.

400 Bad Request

400 Bad Request는 서버가 클라이언트 오류로 인해 요청을 처리할 수 없거나 처리하지 않겠다는 뜻입니다. 잘못된 요청 문법, 깨진 JSON, 올바르지 않은 메시지 프레이밍(framing)처럼 요청 형식 자체에 문제가 있을 때 사용할 수 있습니다.

{
  "error": "invalid_json",
  "message": "요청 본문의 JSON 형식을 확인해 주세요."
}

클라이언트가 문제를 고칠 수 있도록 오류 코드와 설명을 일관된 형식으로 제공하되, 서버 내부 정보나 민감한 값은 노출하지 않는 편이 좋습니다.

401 Unauthorized

401 Unauthorized는 이름과 달리 인가보다 인증(authentication)에 관한 문제를 나타냅니다. Authorization 헤더가 없거나 액세스 토큰(access token)이 만료된 요청이 대표적인 사례입니다.

서버는 요청한 리소스에 적용되는 인증 방식을 WWW-Authenticate 응답 헤더로 알려야 합니다.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api"

인증은 되었지만 그 작업을 수행할 권한이 없다면 403을 사용합니다.

402 Payment Required

402 Payment Required는 이름만 보면 결제가 필요하다는 뜻이 분명해 보입니다. 하지만 RFC 9110은 이 코드를 “향후 사용을 위해 예약”해 두었을 뿐, 어떤 결제 절차나 응답 헤더를 써야 하는지는 정하지 않았습니다. 따라서 카드 결제 실패, 구독 만료, 잔액 부족을 모두 402로 반환하는 것이 HTTP 표준으로 합의된 것은 아닙니다.

그렇다고 전혀 쓰이지 않는 코드는 아닙니다. Cloudflare의 Pay Per Crawl은 AI 크롤러에게 콘텐츠 가격을 알리는 용도로 402를 사용합니다.

HTTP/1.1 402 Payment Required
crawler-price: USD 0.01

이처럼 402를 도입하려면 클라이언트와 서버가 결제 흐름, 헤더, 오류 본문을 별도의 규약으로 합의해야 합니다. 이 빈자리를 아예 표준으로 채우려는 시도가 x402 프로토콜입니다. 결제 정보를 주고받는 헤더 세 개를 정해두고, AI 에이전트가 402 응답을 받으면 스스로 값을 치르고 다시 요청하도록 만듭니다. 실제 활용 사례와 요청 흐름은 Cloudflare Pay Per Crawl 살펴보기에서 자세히 다뤘습니다.

403 Forbidden

403 Forbidden은 서버가 요청을 이해했지만 처리를 거부한다는 뜻입니다. 인증된 사용자가 관리자 전용 리소스에 접근하는 등 인가(authorization)에 실패했을 때 흔히 사용합니다.

리소스의 존재 자체를 숨겨야 한다면 서버가 403 대신 404로 응답할 수도 있습니다. 반복해서 인증하더라도 권한이 달라지지 않는다면 같은 요청은 계속 거부됩니다.

404 Not Found

404 Not Found는 서버가 요청한 리소스를 찾지 못했거나 공개하지 않으려 한다는 뜻입니다. 예를 들어 GET /posts/101을 요청했는데 ID가 101인 글이 없다면 404를 반환할 수 있습니다.

컬렉션 검색 결과가 비어 있는 상황과 개별 리소스가 없는 상황은 구분하는 편이 좋습니다. GET /posts?search=xyz의 검색 결과가 없다면 컬렉션 자체는 존재하므로 보통 200 OK와 빈 배열을 반환합니다.

[]

405 Method Not Allowed

405 Method Not Allowed는 요청한 경로는 존재하지만 그 HTTP 메서드를 지원하지 않는다는 뜻입니다. 예를 들어 조회만 가능한 /reports/weeklyDELETE 요청을 보냈을 때 사용할 수 있습니다.

서버는 이 리소스가 지원하는 메서드를 Allow 응답 헤더에 반드시 담아야 합니다.

HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD

경로 자체가 존재하지 않는다면 405가 아니라 404가 알맞습니다.

409 Conflict

409 Conflict는 요청이 서버에 있는 리소스의 현재 상태와 충돌해 처리할 수 없음을 뜻합니다. 이미 사용 중인 사용자 이름으로 계정을 만들거나, 현재 버전과 맞지 않는 데이터를 갱신하려는 상황이 대표적입니다.

가능하다면 응답 본문에 충돌 원인과 해결에 필요한 정보를 담아 클라이언트가 요청을 수정할 수 있게 해야 합니다. 조건부 갱신에 실패했다는 사실을 HTTP 헤더로 정확히 표현할 수 있다면 뒤에서 다룰 412가 더 적합할 수 있습니다.

410 Gone

410 Gone은 요청한 리소스가 과거에는 존재했지만 지금은 영구적으로 제거되었음을 나타냅니다. 삭제된 이벤트 페이지나 지원이 끝난 API 엔드포인트처럼 제거 사실과 영구성을 알고 있을 때 사용할 수 있습니다.

리소스가 있었는지 알 수 없거나 제거가 영구적인지 확실하지 않다면 404를 사용합니다.

412 Precondition Failed

412 Precondition Failed는 요청 헤더에 담긴 사전 조건(precondition)을 서버가 충족하지 못했음을 뜻합니다. 여러 사용자가 같은 리소스를 수정할 때 변경 사항을 덮어쓰지 않도록 하는 낙관적 동시성 제어(optimistic concurrency control)에 유용합니다.

클라이언트가 이전에 받은 ETagIf-Match 헤더에 넣어 수정 요청을 보냈는데 리소스가 그사이 바뀌었다면 서버는 412로 응답할 수 있습니다.

PUT /documents/42 HTTP/1.1
If-Match: "v7"

클라이언트는 최신 리소스와 ETag를 다시 조회한 뒤 변경 사항을 합치거나 사용자에게 충돌을 알려야 합니다.

413 Content Too Large

413 Content Too Large는 요청 본문이 서버가 허용하거나 처리할 수 있는 크기보다 크다는 뜻입니다. 파일 업로드 제한을 넘었거나 프록시가 설정한 본문 크기 제한을 초과했을 때 볼 수 있습니다.

서버는 제한이 일시적이라면 클라이언트가 언제 다시 시도할 수 있는지 Retry-After 헤더로 알려줄 수 있습니다. 클라이언트가 스스로 요청 크기를 줄일 수 있도록 허용 크기를 별도의 오류 정보로 제공하는 것도 도움이 됩니다.

415 Unsupported Media Type

415 Unsupported Media Type은 요청 본문의 미디어 유형이나 콘텐츠 인코딩을 서버가 지원하지 않아 처리를 거부했다는 뜻입니다. JSON만 받는 API에 Content-Type: text/plain으로 요청하거나 지원하지 않는 압축 방식을 사용한 경우가 이에 해당합니다.

본문 형식은 맞지만 입력값의 의미가 잘못되었다면 415가 아니라 422를 고려해야 합니다.

422 Unprocessable Content

422 Unprocessable Content는 서버가 요청의 콘텐츠 유형과 문법을 이해했지만, 담긴 지시를 의미상 처리할 수 없음을 뜻합니다. JSON 문법은 올바르지만 이메일 주소 형식이 잘못되었거나 필수 필드 조합이 비즈니스 규칙을 위반한 경우가 대표적입니다. 예전 자료나 일부 프레임워크에서는 422 Unprocessable Entity라고 부르기도 하지만, 현재 HTTP 표준의 명칭은 Unprocessable Content입니다.

{
  "error": "validation_failed",
  "fields": {
    "email": "올바른 이메일 주소를 입력해주세요."
  }
}

400, 409, 422는 다음처럼 구분하면 이해하기 쉽습니다.

상황상태 코드
요청의 문법이나 형식 자체가 잘못됨400
요청이 리소스의 현재 상태와 충돌함409
문법은 맞지만 내용의 의미를 처리 못 함422

429 Too Many Requests

429 Too Many Requests는 클라이언트가 일정 시간 안에 너무 많은 요청을 보냈음을 의미합니다. 서버는 과부하를 막거나 사용자별 API 사용량을 제한하기 위해 요청률 제한(rate limiting)을 적용할 수 있습니다.

클라이언트가 무작정 즉시 재시도하면 상황이 더 나빠질 수 있습니다. 서버는 가능하면 Retry-After 헤더로 다시 요청해도 되는 시점을 알려주고, 클라이언트는 이 값이나 지수 백오프(exponential backoff)에 따라 재시도해야 합니다.

HTTP/1.1 429 Too Many Requests
Retry-After: 60

500 Internal Server Error

500 Internal Server Error는 서버가 예상하지 못한 상황을 만나 요청을 처리하지 못했음을 뜻합니다. 처리하지 않은 예외, 잘못된 설정, 코드 결함처럼 더 구체적인 5xx 코드로 설명하기 어려운 서버 오류에 사용합니다.

서버는 오류를 로그와 모니터링 시스템에 기록하고, 필요하다면 클라이언트가 문의할 때 사용할 요청 식별자를 응답할 수 있습니다. 다만 스택 추적(stack trace), 데이터베이스 쿼리, 비밀값 같은 내부 정보는 응답에 노출하지 않아야 합니다.

502 Bad Gateway

502 Bad Gateway는 게이트웨이(gateway)나 프록시(proxy) 역할을 하는 서버가 업스트림(upstream) 서버로부터 유효하지 않은 응답을 받았음을 뜻합니다. 리버스 프록시가 애플리케이션 서버에 연결했지만 연결이 비정상적으로 종료되거나 응답 형식을 해석하지 못한 경우가 여기에 해당합니다.

장애를 조사할 때는 클라이언트와 맞닿은 게이트웨이뿐 아니라 그 뒤에 있는 업스트림 서버의 로그와 상태도 함께 확인해야 합니다.

503 Service Unavailable

503 Service Unavailable은 서버가 과부하나 예정된 유지보수 때문에 현재 요청을 처리할 수 없음을 의미합니다. 문제가 일시적이라는 점이 중요하며, 서버는 가능하다면 예상 복구 시간을 Retry-After 헤더로 알려줄 수 있습니다.

HTTP/1.1 503 Service Unavailable
Retry-After: 120

로드 밸런서(load balancer)와 오케스트레이션(orchestration) 플랫폼이 호출하는 준비 상태(readiness) 확인 엔드포인트도 서비스할 준비가 되지 않은 동안 503을 반환할 수 있습니다. 단, 모든 내부 오류를 503으로 뭉뚱그리기보다 실제로 서비스가 일시적으로 요청을 받을 수 없는 상황에 사용해야 합니다.

504 Gateway Timeout

504 Gateway Timeout은 게이트웨이나 프록시가 업스트림 서버의 응답을 정해진 시간 안에 받지 못했음을 뜻합니다. 업스트림이 잘못된 응답을 보낸 502와 달리, 504는 필요한 응답이 제때 오지 않은 상황에 초점을 둡니다.

헷갈리기 쉬운 세 가지 서버 오류는 다음처럼 구분할 수 있습니다.

상황상태 코드
업스트림 서버에서 유효하지 않은 응답을 받음502
서버가 과부하나 점검으로 일시적으로 요청을 못 받음503
업스트림 서버의 응답을 정해진 시간 안에 못 받음504

마치며

지금까지 웹 개발에서 자주 마주치는 HTTP 상태 코드와 서로 비슷한 코드를 구분하는 기준을 살펴봤습니다. 핵심은 가장 익숙한 코드를 고르는 것이 아니라, 서버가 실제로 요청을 어떻게 처리했는지 가장 정확하게 표현하는 코드를 고르는 것입니다.

이 글에서 다루지 않은 코드나 세부 조건이 궁금하다면 HTTP Semantics 표준인 RFC 9110IANA HTTP 상태 코드 목록을 확인해보세요. 상태 코드와 함께 필요한 응답 헤더까지 올바르게 사용하면 API는 클라이언트가 예측하고 복구하기 훨씬 쉬워집니다.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord