Logo

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

웹 개발자라면 200, 404, 500과 같은 HTTP 상태 코드에 대해 한 번쯤은 들어보셨을텐데요. 경험을 통해서 이렇게 자주 보이는 코드에 대해서 막연히 감은 있으시지만, 실제로 내가 HTTP 상태 코드에 대해서 잘 알고 있는지에 대해서 스스로 의문이 들 때가 있으실 것입니다.

특히 백엔드 개발자라면 요즘에 웹이나 Rest API 개발을 간편하게 해주는 프레임워크가 워낙 잘 나와있다보니, 어떤 상태 코드를 응답하고 있는지에 대해서 오히려 소홀해 질 수 있는 것 같아요. 이번 글에서는 웹 개발에 있어서 HTTP 상태 코드가 얼마나 중요한지에 대해서 알아보고, HTTP 상태 코드를 올바르게 사용하는 방법에 대해서 살펴보겠습니다.

HTTP 상태 코드 중요성

웹은 기본적으로 HTTP 프로토콜 통해 서버와 클라이언트 간에 원격 통신으로 돌아가죠? 쉽게 말해서 클라이언트에서 요청을 보내면 서버는 이를 받아서 처리한 후 응답을 보냅니다. 그리고 이렇게 데이터를 주고 받는 과정이 클라이언트와 서버 사이에서 계속해서 반복적으로 일어나죠.

서버가 클라이언트 요청을 항상 성공적으로 처리하고 응답을 줄 수 있다면 이상적이겠지만 실제 세상에는 이를 방해할 수 있는 변수가 너무나도 많죠? 클라이언트가 실수로 잘못된 요청을 보낼 수도 있으며, 서버 측의 유지보수 작업으로 인해 서비스가 일시적으로 다운될 수도 있습니다. 또한 클라이언트가 동시에 보내는 요청이 폭증하여 서버에서는 여러가지 예상치 못한 상황이 장애 상황이 발생할 수도 있죠.

이렇게 다양한 상황이 발생할 수 있는 웹에서 HTTP 상태 코드는 클라이언트와 서버가 효과적으로 상호작용을 하기 위해서 핵심적인 역할을 담당하는데요. 바로 HTTP 상태 코드를 통해 서버는 미리 약속된 표준 코드를 클라이언트에게 응답하여, 요청 처리 결과에 대한 명확한 피드백을 줄 수 있다는 것입니다.

그래서 HTTP 상태 코드는 프로그래밍 언어를 불문하고 HTTP 기반으로 이뤄지는 웹 개발에 있어서 정말로 중요한 주제인데요. 백엔드 개발자는 서버의 상황에 따라 정확한 HTTP 상태 코드를 응답함으로써 클라이언트에게 서버에 어떤 일이 발생했는지 알려줄 수 있고, 프론트엔드 개발자는 굳이 응답 바디(body)를 처리하여 읽지 않고도 수신된 HTTP 상태 코드만을 보고 좀 더 효율적으로 적절한 후속 처리를 할 수 있습니다. 게다가, 마이크로서비스 아키텍처(microservice architecture)에서는 HTTP 기반의 이러한 상호작용이 REST API를 통해 서버 사이에서도 빈번하게 발생할 수 있습니다.

뿐만 아니라, HTTP 상태 코드는 개발자가 클라이언트와 서버 간의 통신을 디버깅(debugging)하고 모니터링(monitoring)하는데도 큰 도움이 됩니다. 비용이나 성능 등의 이유로 웹 로그를 확인해보면 응답 헤더(header)와 바디(body)는 생략하고 HTTP 상태 코드만 남기는 경우가 많은데요. 많은 모니터링 도구들이 HTTP 상태 코드를 기준으로 트래픽을 분류해서 알람도 보내고 통계도 만듭니다.

무엇보다 서버에서 올바른 HTTP 상태 코드를 응답하는 것이 중요한 가장 큰 이유는 이 것이 해당 웹 서비스의 사용성과 신뢰성에 직결된 문제이며 클라이언트의 경험에 큰 영향을 주기 때문입니다. 서버에서 처리 결과에 부합하지 않는 HTTP 상태 코드가 응답하면 클라이언트는 해당 API를 신뢰하기 어려워지고 클라이언트의 안정성도 떨어지게 됩니다. 또한, 클라이언트에서는 방어적으로 예외 처리를 해야하기 때문에 코드가 불필요하게 복잡해지고 결국 클라이언트의 성능에도 부정적인 결과를 초래할 수 있습니다.

상태 코드의 구조와 범주

3자리 숫자로 이루어진 HTTP 상태 코드는 크게 5개의 범주로 나누어지는데요. 그래서 수 많은 상태 코드를 일일이 다 기억하기는 힘들지만, 맨 앞 숫자만 보고도 대강 어떤 성격의 코드인지를 유추할 수는 있죠.

  • 1xx 범주의 상태 코드는 웹 개발자가 직접 사용할 일은 거의 없기 때문에 특별히 다루지는 않도록 하겠습니다.
  • 2xx 범주의 상태 코드는 클라이언트의 요청이 올바르게 전달되었고 서버가 요청을 성공적으로 처리하였음을 나타냅니다.
  • 3xx 상태 코드는 클라이언트가 요청한 리소스가 다른 위치에 있거나 재요청이 필요함을 나타냅니다.
  • 4xx 범주의 상태 코드는 처리가 실패하였는데 그 원인이 클라이언트 측에 있고 그래서 클라이언트에서 스스로 해결해야하는 상황을 나타냅니다.
  • 5xx 범주의 상태 코드는 처리가 실패하였는데 그 원인이 서버 측에서 있고 그래서 서버에서 조치를 취해줘야하는 상황을 나타냅니다.

대부분의 HTTP 클라이언트 라이브러리나 프레임워크는 따로 설정을 해주지 않으면 서버로 부터 처리 실패를 나타내는 4xx5xx 범주의 상태 코드가 응답되면 예외를 발생시킵니다. 따라서 서버에서 불필요하게 이 범주의 상태 코드를 응답하지 않도록 각별히 주의해야합니다.

자, 그럼 지금부터 웹 개발을 하면서 자주 접하게 되는 HTTP 상태 코드를 하나씩 살펴볼까요?

101 Switching Protocols

101 상태 코드는 Switching Protocols, 즉 HTTP에서 다른 프로토콜로 바뀌었음을 의미하는데요. 클라이언트가 서버와 웹소켓(WebSocket) 연결을 맺기 전에 가장 흔하게 볼 수 있습니다. 웹소켓 통신하라면 반드시 프로토콜을 HTTP에서 WebSocket으로 업그레이드해야 하기 때문입니다.

실시간 양방향 통신을 위해서 사용되는 웹소켓에 대해서는 별도 포스팅에서 자세히 다루고 있으니 참고 바랍니다.

200 OK

200 상태 코드는 OK, 즉 클라이언트의 요청이 성공적으로 처리되었음을 나타내는데요. 아마도 클라이언트 개발자들이 가장 좋아하는 상태 코드가 아닐까 싶습니다. 😇

서버에서는 클라이언트의 요청을 제대로 이해하고 문제없이 처리하였을 때 200 OK를 응답해야하며 응답 바디(response body)에 요청한 데이터를 담아줘야 합니다. 클라이언트에서는 응답 바디를 MIME type에 따라 적절히 처리 후에 읽어야 하며(parse), 해당 프로그래밍 언어에서 데이터를 다루기 쉬운 형태로 역직렬화(deserialization) 작업을 해줘야 합니다.

HTTP 통신에서 일반적으로 GET 방식으로 데이터를 요청하기 때문에 GET 방식의 요청에 대한 응답에서 200 OK를 많이 보게되는데요. 하지만 백엔드 개발자의 게으름이나 개발 편의를 핑계로 문제없이 요청이 처리되었다면 무조건 200 OK로 퉁쳐서 응답하는 서버도 종종 볼 수 있는데요. 이럴 경우 클라이언트에서 혼선이 발생할 수 있고 섬세한 캐싱(caching) 처리도 어려워지기 때문에 200 OK를 납용하지 않도록 주의해야겠습니다.

201 Created

201 상태 코드는 Created, 즉 클라이언트의 요청으로 인해 서버에 새로운 리소스(resource)가 생성되었음을 의미합니다.

웹 페이지든 REST API든 무언가를 생성하고 싶을 때는 보통 서버로 POST 방식의 요청을 보내죠? 예를 들어서, 새로운 글을 작성하려면 POST /posts를 요청하고, 20번 포스팅에 댓글을 달려면 POST /posts/20/comments를 요청하게 됩니다.

서버에서는 클라이언트의 요청에 따라 정상적으로 리소스가 생성되었을 때 201 Created 상태로 응답해야 하며, 이때 생성된 리소스의 URL은 Location 응답 헤더에 실어 보내는 것이 관례입니다. 왜냐하면 서버에 리소스가 성공적으로 생성되면 클라이언트에서는 다음 수순으로 바로 해당 리소스로 이동해야하는 경우가 대부분이기 때문입니다.

204 No Content

204 상태 코드는 No Content, 즉 서버가 응답할 데이터가 없음을 나타냅니다.

이 상태 코드는 리소스 삭제를 위한 DELETE 방식의 요청이나 리소스 수정을 위한 PUT 또는 PATCH 방식의 요청을 처리할 때 유용하게 쓰이는데요. 왜냐하면 리소스를 삭제한 후에는 서버가 딱히 돌려보낼 데이터가 없으며, 리소스를 수정하는 경우에도 클라이언트가 이미 수정 사항을 알고 있기 때문입니다.

따라서 서버에서는 204 No Content를 응답하여 클라이언트에게 명시적으로 요청이 정상적으로 처리되었지만 돌려줄 데이터는 없다고 알려주는 것입니다. 응답 바디에 요청한 데이터를 담아서 수신할 때 사용되는 200 OK와 대조되는 부분이죠. 서버로 부터 204 No Content이 돌아오면 클라이언트에서는 해당 리소스를 캐시에서 삭제하는 등의 적절한 후속 조치를 취할 수 있습니다.

301 Moved Permanently

301 상태 코드는 Moved Permanently, 즉 클라이언트가 요청한 리소스가 영구적으로 새로운 위치로 이동되었음을 의미하는데요. 이 경우, 클라이언트는 서버에서 반환된 Location 응답 헤더에 명시된 새로운 URL로 해당 리소스를 재요청해야 합니다.

301 Moved Permanently는 특히 SEO(검색 엔진 최적화) 측면에서 중요한데요. 검색 엔진은 크롤링(crawling)할 때 이 상태 코드를 통해서 웹 페이지의 URL이 바뀌었다는 것을 감지하여 인덱싱(indexing)에 반영할 수 있기 때문입니다. 그래서 웹사이트의 도메인 네임을 변경하였거나 대대적인 개편을 하여 사이트맵(sitemap)이 바뀌었을 때 매우 유용하게 사용됩니다. 또한, 기존 URL이 폐기(deprecated)된 상태에서 클라이언트의 트래픽(traffic)을 새로운 URL로 점진적으로 유도해야 할 때도 활용할 수 있습니다.

302 Found

302 상태 코드는 Found 또는 Moved Temporarily, 즉 클라이언트가 요청한 리소스가 일시적으로 다른 위치에 있음을 나타냅니다.

이 상태 코드는 인증되지 않은 사용자를 일시적으로 로그인 페이지로 이동시키기 위해서 많이 사용되는데요. 로그인이 끝나면 다시 원래 요청했던 URL에 접근할 수 있다는 점에서 301 Moved Permanently 상태 코드와 분명한 차이가 있습니다.

301 상태 코드와 302 상태 코드가 각각 언제 사용해야 하는지 헷갈릴 수 있는데요. 클라이언트가 다음에 같은 리소스를 요청할 때 새로운 URL을 사용하길 원하신다면 301 상태 코드를 사용해야합니다. 반면에 클라이언트가 다음에 같은 리소스를 요청할 때도 같은 URL을 사용하길 원하신다면 302 상태 코드를 사용하시면 됩니다.

304 Not Modified

304 상태 코드는 Not Modified, 즉 클라이언트가 이전에 요청한 리소스가 서버에서 아직까지 수정된 적이 없음을 의미합니다.

이 상태 코드는 클라이언트 측 캐싱(caching)을 지원하는데 핵심적인 역할을 담당하는데요. 클라이언트가 이전에 이미 요청한 리소스를 다시 요청하면, 서버에서는 실제 데이터를 다시 전송하지 않고 304 Not Modified 상태 코드를 반환할 수 있습니다.

이를 통해 서버는 불필요한 데이터 전송을 피하여 네트워크 대역폭(bandwidth)을 아낄 수 있으며, DB 부하도 줄일 수 있고, 결국적으로 더 많은 동시 요청을 처리할 수 있게 되고요. 클라이언트는 캐시된 리소스를 재사용할 수 있기 때문에 성능을 향상시켜 사용자에게 더 나은 경험을 제공할 수 있습니다.

이러한 캐싱 메커니즘을 제대로 이해하려면 ETag에 대해서도 알아야 하며, 다른 여러 부분을 이해해야 합니다. 따라서 추후에 별도의 포스팅에서 자세히 다루도록 하겠습니다.

400 Bad Request

400 상태 코드는 Bad Request, 즉 클라이언트가 잘못된 요청을 보냈다는 것을 나타냅니다.

클라이언트는 경로, 요청 헤더, 쿼리 스트링, 요청 바디 등 다양한 방법을 통해서 서버로 입력 데이터를 송신할 수 있는데요. 서버에서는 잘못된 데이터가 유입되지 않도록 보통 입력값 검증을 수행하며 이 검증이 실패할 경우 400 Bad Request를 응답해야합니다. 그리고 추가적으로 응답 바디에 구체적으로 어떤 입력 파라미터가 형식에 맞지 않는지 클리이언트에게 정확한 피드백을 주는 것이 좋습니다.

예를 들어, 데어터 검색을 위해서 클라이언트가 ?query={검색어} 형태로 쿼리 스트링을 보내야하는데 ?q={검색어} 형태로 보낼 경우 서버는 해당 요청을 제대로 처리할 수 없을 것입니다. 이 때 서버는 400 Bad Request를 응답하고 응답 바디를 통해서 query 파라미터가 누락되어 있고 불필요하게 q 파라미터를 보냈다고 클라이언트에게 알려줄 수 있습니다.

401 Unauthorized

401 상태 코드는 Unauthorized, 즉 클라이언트가 미인증 상태에서 요청을 했다는 것을 의미합니다.

이 상태 코드는 보안 측면에서 매우 핵심적인 역할을 담당하는데요. 서버에서 요구하는 인증 절차를 따르지 않고 들어온 요청에 대해서는 서버에서는 401 Unauthorized을 응답해야합니다.

예를 들어, 서버에서 Bearer 토큰 방식의 인증을 하고 있는데 클라이언트가 Authorization 요청 헤더에 유효하지 않거나 만료된 토큰을 설정하였다면, 서버에서는 401 Unauthorized을 응답하여 처리를 거부할 수 있습니다.

403 Forbidden

403 상태 코드는 Forbidden, 즉 클라이언트가 보호된 리소스를 요청할 권한이 없다는 것을 나타내는데요. 단순히 사용자 인증에서 그치는 것이 아니라 리소스 별로 사용자의 역할에 따라 정교한 접근 통제를 해야하는 서버에서 많이 사용됩니다.

401 상태 코드는 아예 인증이 되지 않는 요청을 거부하기 위해서 사용되는 반면에, 403 상태 코드는 인증(authentication)에는 성공하였더라도 특정 리소스를 접근할 권한이 없을 때, 즉 인가(authorization)에 실패하였을 때 사용됩니다. 예를 들어서, 일반 사용자 역할로 인증을 한 클라이언트가 관리자 권한으로만 접근할 수 있는 리소스를 요청하면 서버에서는 403 Forbidden을 응답해야 합니다.

404 Not Found

404 상태 코드는 Not Found, 즉 클라이언트가 요청한 리소스를 찾을 수 없음을 의미하는데요. 웹에서 404 페이지를 워낙 흔하기 볼 수 있기 때문에 일반인들에게도 상당히 친숙한 상태 코드입니다.

이 상태 코드는 클라이언트가 서버 측에 존재하지 않거나 삭제된 리소스를 요청할 때 사용되는데요. 예를 들어서, 클라이언트가 GET /posts/101 요청을 했는데 서버에 ID가 101인 게시물이 없다면 404 Not Found를 응답해야 합니다.

단, 클라이언트가 여러 리소스를 요청한 경우에는 404 Not Found 대신에 200 OK와 빈 배열을 반환하는 편이 좋은데요. 왜냐하면 200 상태 코드를 응답하면 클라이언트에서 단순히 배열의 크기만 확인하면 되는데 404 상태 코드를 응답하면 별도로 예외 처리까지 해줘야하기 때문입니다.

흔한 예로, 클라이언트가 GET /posts?search=xyz 요청을 보냈는데 서버에 xyz가 포함된 게시물이 없는 상황을 생각할 수 있겠습니다. 클라이언트 입장에서 서버로 부터 200 상태 코드가 응답될 수도 있고 404 상태 코드가 응답될 수도 있다면 처리가 상당히 번거로워질 수 있습니다. UI 측면에서는 응답된 데이터가 0건이든 100건이든 크게 달라지는 부분이 많지 않기 때문이죠.

429 Too Many Requests

429 상태 코드는 Too Many Requests, 즉 클라이언트가 일정 시간 내에 너무 많은 요청을 보냈음을 의미합니다.

보통 REST API 서버에서는 과부하를 방지하고 서비스의 안정성을 보장하기 위해서 Rate Limit, 즉 클라이언트 별로 일정 시간 동안 요청할 수 있는 횟수에 제한을 두는데요. 만약에 클라이언트가 주어진 최대 요청 회수를 초과하는 요청을 보내면, 서버에서는 429 Too Many Requests를 응답해야 합니다. 또한 유료 사용자와 무료 사용자와의 API 사용량의 차등을 두기 위해서도 이 상태 코드를 활용할 수 있습니다.

서버에서 Rate Limit을 구현할 때는 프레임워크에서 제공하는 기능을 쓰거나 별도의 라이브러리를 사용하는 경우가 많습니다. 따라서 실제로 백엔드 개발자가 직접 이 상태 코드를 쓸 일은 그리 많지는 않을텐데요. 하지만 프런트엔드 개발자에게는 알아두면 유용한 상태 코드입니다.

500 Internal Server Error

500 상태 코드는 Internal Server Error, 즉 서버에서 발생한 내부적인 오류로 인해 요청을 처리할 수 없음을 의미합니다.

500 Internal Server Error는 서버의 코드나 설정에 문제가 있어서 정상적으로 요청을 처리할 수 없는 상태를 나타내는 대표적인 5xx 범주에 속하는 상태 코드인데요. 클라이언트에서는 이 상태 코드를 받게 되면 서버 운영팀에 즉각 연락하여 빠른 조치가 될 수 있도록 도와주는 것이 좋습니다.

서버 개발자가 의도적으로 500 상태 코드를 응답하도록 코드를 짤 수도 있지만, 그 보다는 프레임워크 수준에서 의도치 않게 발생한 예외나 에러를 잡아서 자동으로 500 상태 코드를 응답해주는 경우가 더 많습니다.

503 Service Unavailable

503 상태 코드는 Service Unavailable, 즉 서버가 일시적으로 서비스를 할 수 없는 상태를 의미합니다.

이 상태 코드는 서버가 과부하 상태에 빠졌거나 유지보수를 위해 다운된 상태일 때 많이 볼 수 있습니다. 대표적인 예로 DDoS 공격을 받은 서비스가 503 Service Unavailable을 응답하는 경우를 들 수 있겠습니다.

뿐만 아니라 애플리케이션을 컨테이너화(containerized)하여 배포하는 경우 컨테이너 쿠버네티스(Kubernetes)와 같은 오케스트레이션(Container Orchestration) 플랫폼이 자동으로 문제가 있는 서버 인스턴스를 새로운 인스턴스로 교체해주는데요. 이 때 보통 생존 여부(liveness)나 가용 여부(readiness)를 응답해주는 HTTP 엔드포인트(endpoint)를 주기적으로 찔러봄으로써 각 서버 인스턴스가 건강한 상태인지를 지속적으로 확인하게 됩니다. 따라서 서버에서 문제가 발생하면 503 상태 코드를 응답되도록 해놓으면 서비스의 가용성 확보에 큰 도움이 됩니다.

마치면서

지금까지 웹 개발자로서 HTTP 상태 코드의 중요성을 상기하고 반드시 알고 있어야 하는 HTTP 상태 코드를 정리해보았습니다.

사실 HTTP 스펙에는 이 것보다 훨씬 많은 상태 코드가 있지만 대부분 잘 안 쓰이지 않거나 스펙 상으로만 존재하는 경우가 많은데요. 혹시 본 포스팅에서 다루지 않은 HTTP 상태 코드를 마주치신다면, 관련 MDN 레퍼런스 문서를 찾아보시면서 추가 학습을 하시면 좋을 것 같습니다.

이 글이 HTTP 상태 코드를 올바르게 사용하는 데 있어서 많은 개발자분들에게 좋은 안내서가 되기를 바라며 이만 줄이겠습니다.