응답이 왔다는 것만으로는 아무것도 알 수 없다. 그 응답이 성공인지 실패인지, 실패라면 누구의 잘못인지를 알려 주는 것이 상태 코드다. 세 자리 숫자에 불과하지만, 이 숫자 하나가 요청의 처리 결과 전체를 요약한다. 상태 코드를 정확히 읽고 정확히 돌려주는 능력은 웹을 다루는 사람에게 필수 소양이다.
상태 코드는 체계적으로 설계되어 있어서, 첫 자리 숫자만 봐도 결과의 성격을 알 수 있다. 그 안에서 세부 숫자가 더 구체적인 상황을 지목한다. 이번 편은 다섯 계열의 의미를 훑고, 실무에서 자주 마주치는 코드들을 정리하며, 서버를 설계할 때 어떤 코드를 언제 돌려주어야 하는지까지 다룬다. 상태 코드는 외워야 할 목록이 아니라 이해해야 할 체계다.
상태 코드의 다섯 계열
상태 코드는 첫 자리 숫자에 따라 다섯 계열로 나뉜다. 1로 시작하는 코드는 처리가 진행 중이라는 정보성 응답이고, 2로 시작하면 성공, 3으로 시작하면 다른 곳으로 이동하라는 지시, 4로 시작하면 요청을 보낸 쪽의 잘못, 5로 시작하면 서버 쪽의 문제다. 이 다섯 갈래만 머릿속에 넣어 두면 처음 보는 코드도 대략 해석할 수 있다.
이 계열 구분이 상태 코드 체계의 뼈대다. 세부 숫자를 다 외우지 못해도, 첫 자리만으로 응답이 성공인지 실패인지, 실패라면 요청을 고쳐야 하는지 서버를 살펴야 하는지를 판단할 수 있다. 예컨대 앞자리가 4라면 요청 자체에 문제가 있다는 뜻이므로 클라이언트 쪽을 먼저 살피고, 앞자리가 5라면 서버 쪽을 살핀다.
정보성 응답인 1 계열은 실무에서 직접 다룰 일이 드물다. 대개 하위 계층이나 서버가 알아서 처리하기 때문이다. 그래서 이번 편에서는 성공, 이동, 클라이언트 오류, 서버 오류의 네 계열에 집중한다. 이 네 계열이 우리가 매일 마주치고 직접 돌려주게 되는 응답의 대부분을 차지한다.
계열의 의미를 지키는 것은 서버 설계자의 책임이다. 실패했는데 성공 계열을 돌려주거나, 서버 잘못인데 클라이언트 잘못으로 응답하면, 그 코드를 신뢰한 클라이언트가 잘못된 판단을 내린다. 상태 코드는 서버가 클라이언트에게 보내는 신호이므로, 그 신호가 정직해야 통신 전체가 신뢰를 유지한다.
계열을 나눈 설계에는 실용적인 이유가 있다. 클라이언트나 중간 장비가 모든 코드를 일일이 알지 못해도, 첫 자리만 보고 대략적인 처리 방침을 정할 수 있게 하려는 것이다. 처음 보는 코드가 오더라도 앞자리가 2면 성공으로, 4면 요청 오류로 취급하면 크게 어긋나지 않는다. 이 관대한 설계 덕분에 새로운 코드가 나중에 추가되어도 기존 시스템이 그럭저럭 대응할 수 있다. 확장을 염두에 둔 구조라는 점에서 상태 코드 체계는 잘 다듬어진 설계의 본보기다.
성공을 알리는 2xx
2로 시작하는 계열은 요청이 성공적으로 처리되었음을 알린다. 가장 흔한 코드는 200 으로, 요청이 성공했고 그 결과가 본문에 담겨 있다는 뜻이다. 조회 요청이 정상적으로 처리되면 대개 이 코드가 돌아온다. 성공 계열 중에서도 기본값에 해당한다.
새 자원이 만들어졌을 때는 201 을 돌려주는 것이 규약에 맞다. 생성 요청이 성공하면 단순히 200 을 돌려주기보다, 자원이 새로 생겼음을 명시하는 201 을 쓰고 그 자원의 위치를 헤더에 담아 알려 주는 것이 좋다. 이렇게 하면 클라이언트가 방금 만들어진 자원이 어디에 있는지 곧바로 알 수 있다.
요청은 성공했지만 돌려줄 본문이 없을 때는 204 를 쓴다. 삭제가 성공했거나 변경이 반영되었지만 특별히 보여 줄 데이터가 없는 경우가 여기에 해당한다. 이 코드는 본문이 비어 있음을 명시하므로, 클라이언트는 응답 본문을 읽으려 시도하지 않는다. 성공을 알리되 데이터는 없다는 상황을 정확히 표현하는 코드다.
성공 계열을 세분해 쓰면 응답이 더 많은 정보를 담는다. 무조건 200 만 돌려주는 서버보다, 생성에는 201 을 빈 응답에는 204 를 쓰는 서버가 클라이언트에게 더 정확한 신호를 준다. 물론 모든 상황을 세분할 필요는 없지만, 의미가 분명히 다른 성공은 다른 코드로 구분하는 편이 낫다.
이동을 지시하는 3xx
3으로 시작하는 계열은 원하는 자원이 다른 곳에 있으니 그리로 가라는 지시다. 이 계열의 응답을 받으면 클라이언트는 대개 지정된 새 주소로 요청을 다시 보낸다. 이동에는 영구적인 이동과 일시적인 이동이 있고, 둘을 구분하는 것이 중요하다.
자원이 완전히 새 주소로 옮겨졌을 때는 영구 이동을 뜻하는 301 을 쓴다. 이 코드를 받으면 클라이언트와 검색 엔진은 옛 주소가 더 이상 쓰이지 않는다고 이해하고, 새 주소로 기록을 갱신한다. 주소 체계를 개편할 때 옛 주소가 검색에서 사라지지 않도록 하려면 이 코드를 정확히 써야 한다.
반면 잠시만 다른 곳으로 보내야 할 때는 일시 이동을 뜻하는 코드를 쓴다. 점검 중이라 임시 페이지로 보내거나, 로그인이 필요해 잠깐 다른 곳으로 안내하는 경우다. 이 경우 옛 주소는 여전히 유효하므로, 클라이언트는 옛 주소를 버리지 않는다. 영구와 일시를 뒤바꿔 쓰면 검색 기록이 엉뚱하게 갱신되어 오래 고생한다.
3 계열에는 이동 외에 캐시와 관련된 코드도 있다. 클라이언트가 가진 사본이 아직 유효하니 새로 받지 말고 그대로 쓰라는 304 가 그것이다. 이 코드는 본문 없이 돌아오며, 이미 받아 둔 자원을 재사용하게 해 전송량을 크게 줄인다. 캐시 편에서 이 코드의 동작을 자세히 다룬다.
클라이언트 잘못을 알리는 4xx
4로 시작하는 계열은 요청을 보낸 쪽에 문제가 있다는 뜻이다. 가장 유명한 코드는 자원이 없음을 뜻하는 404 다. 요청한 주소에 해당하는 자원이 존재하지 않을 때 이 코드가 돌아온다. 주소를 잘못 입력했거나, 자원이 삭제되었거나, 애초에 없는 자원을 요청한 경우다.
인증과 권한에 관련된 코드도 이 계열에 있다. 신분을 증명하지 않아 누구인지 알 수 없을 때는 401 이 돌아오고, 누구인지는 알지만 그 동작을 할 자격이 없을 때는 403 이 돌아온다. 이 둘은 자주 혼동되는데, 앞의 것은 인증의 문제이고 뒤의 것은 권한의 문제다. 로그인하지 않은 상태와 로그인은 했지만 권한이 없는 상태를 구분하는 셈이다.
요청의 형식 자체가 잘못되었을 때는 400 을 쓴다. 서버가 이해할 수 없는 형식으로 데이터를 보냈거나 필수 값이 빠졌을 때가 여기에 해당한다. 이 코드는 요청을 고쳐서 다시 보내라는 신호이므로, 무엇이 잘못되었는지를 본문에 함께 담아 주면 클라이언트가 문제를 바로잡기 쉽다.
요청이 너무 잦을 때 돌려주는 코드도 이 계열에 있다. 정해진 시간 안에 허용된 횟수를 넘겨 요청을 보내면 서버는 요청이 너무 많다는 코드를 돌려주고, 얼마 뒤에 다시 시도하라는 정보를 헤더에 실어 준다. 이 코드는 서버를 보호하기 위한 장치이며, 클라이언트는 이 신호를 받으면 요청의 속도를 늦추어야 한다. 무시하고 계속 몰아붙이면 더 강한 차단으로 이어질 수 있다.
4 계열의 코드는 요청을 보낸 쪽이 무언가를 고쳐야 한다는 공통점을 가진다. 그래서 이 계열의 응답을 받으면 같은 요청을 그대로 재시도하는 것은 의미가 없다. 요청을 고치지 않는 한 결과는 같기 때문이다. 이 점에서 4 계열은 재시도로 해결되는 5 계열과 성격이 다르다.
서버 문제를 알리는 5xx
5로 시작하는 계열은 요청은 문제가 없었는데 서버 쪽에서 처리에 실패했다는 뜻이다. 대표적인 코드는 서버 내부에서 예상치 못한 오류가 발생했음을 뜻하는 500 이다. 코드에 버그가 있거나, 처리 도중 예외가 발생했거나, 서버가 감당하지 못하는 상황이 벌어졌을 때 이 코드가 돌아온다.
서버가 일시적으로 요청을 처리할 수 없을 때는 503 을 쓴다. 점검 중이거나 과부하로 잠시 응답할 수 없는 상황이다. 이 코드는 잠시 후 다시 시도하면 성공할 수 있다는 뉘앙스를 담으므로, 얼마 뒤에 재시도하라는 정보를 헤더에 함께 실어 주기도 한다. 일시적 문제임을 알리는 것이 500 과의 차이다.
여러 서버가 연결된 구조에서는 중간 서버가 뒤쪽 서버로부터 잘못된 응답을 받거나 응답을 받지 못하는 경우도 있다. 이럴 때 돌아오는 코드들은 문제가 우리 서버가 아니라 그 뒤에 연결된 다른 서버에 있음을 가리킨다. 배포망이나 중계 서버를 거치는 구조에서 이런 코드를 만나면, 문제의 위치를 앞이 아니라 뒤에서 찾아야 한다.
5 계열은 클라이언트의 잘못이 아니므로 같은 요청을 다시 보내면 성공할 수 있다. 그래서 이 계열의 오류에는 재시도 전략이 유효하다. 다만 무작정 즉시 재시도하면 이미 힘겨운 서버에 부담을 더하므로, 간격을 점점 늘려 가며 재시도하는 방식이 권장된다. 서버가 회복할 시간을 주면서 재시도하는 것이 요령이다.
자주 혼동되는 코드들
실무에서 가장 흔히 헷갈리는 것이 401 과 403 의 구분이다. 인증되지 않은 상태에는 401 을, 인증은 되었으나 권한이 없는 상태에는 403 을 쓴다는 원칙을 기억해 두면 된다. 이 둘을 뒤바꿔 쓰면 클라이언트가 다시 로그인해야 하는지 아니면 애초에 접근이 막힌 것인지 판단하지 못한다.
없는 자원에 대해 404 를 쓸지 403 을 쓸지도 판단이 필요한 지점이다. 자원이 존재하지만 접근 권한이 없는 경우, 그 존재 자체를 숨기고 싶다면 권한 없음을 뜻하는 코드 대신 없음을 뜻하는 코드를 돌려주기도 한다. 자원의 존재 여부조차 노출하지 않으려는 보안적 판단에서 나온 선택이다.
성공했지만 본문이 없는 상황에서 200 에 빈 본문을 담을지 204 를 쓸지도 자주 논의된다. 규약의 의도에 맞추면 본문이 없을 때는 204 가 정확하지만, 클라이언트가 항상 본문을 기대하도록 만들어졌다면 빈 본문의 200 이 더 안전할 수 있다. 정답이 하나로 정해지지 않는 이런 지점에서는 클라이언트와의 약속이 기준이 된다.
혼동을 줄이는 방법은 각 코드의 의미를 상황이 아니라 원칙으로 기억하는 것이다. 이 코드가 누구의 잘못을 가리키는지, 재시도가 유효한지, 클라이언트가 무엇을 해야 하는지를 기준으로 삼으면, 비슷해 보이는 코드들 사이에서도 올바른 선택을 할 수 있다. 코드를 외우기보다 코드가 담는 신호를 이해하는 편이 오래 남는다.
상태 코드를 설계하는 관점
서버를 만들 때 상태 코드는 클라이언트와의 계약이다. 어떤 상황에 어떤 코드를 돌려줄지를 정해 두면, 클라이언트는 그 코드만 보고 다음 행동을 결정할 수 있다. 이 계약이 일관되면 클라이언트 쪽 코드가 단순해지고, 계약이 들쭉날쭉하면 클라이언트가 온갖 예외를 따로 처리해야 한다.
가장 흔한 잘못은 모든 응답을 200 으로 돌려주고 실제 결과는 본문 안에만 적는 설계다. 이렇게 하면 상태 코드가 담아야 할 정보가 본문으로 옮겨져, 중간의 캐시나 자동화 도구가 결과를 판단하지 못한다. 성공과 실패는 상태 코드로 구분하고, 세부 설명을 본문에 담는 것이 규약의 의도에 맞다.
상태 코드를 정확히 쓰면 로그와 모니터링도 쉬워진다. 4 계열과 5 계열의 응답 비율만 지켜봐도 시스템에 어떤 문제가 일어나는지 파악할 수 있기 때문이다. 실패를 성공으로 위장한 서버는 이런 관측의 기회를 스스로 없앤다. 정직한 상태 코드는 문제를 조기에 발견하는 눈이 된다.
상태 코드를 설계할 때는 지나친 세분화도 경계해야 한다. 규약에 정의된 코드가 수십 가지에 이르지만, 실제로 자주 쓰이는 것은 계열마다 몇 개로 좁혀진다. 클라이언트가 구분해서 다룰 수 없을 만큼 코드를 잘게 나누면, 그 세밀함은 정보가 아니라 혼란이 된다. 클라이언트가 실제로 다르게 반응할 상황만 다른 코드로 나누는 것이 실용적인 균형점이다. 코드는 많이 아는 것보다 적절히 골라 쓰는 것이 중요하다.
이번 편은 상태 코드의 다섯 계열과 자주 쓰이는 코드, 그리고 혼동되는 코드의 구분과 설계 관점을 다루었다. 상태 코드가 응답의 결과를 요약한다면, 그 응답의 세부 성격을 실어 나르는 것은 헤더다. 다음 편에서는 헤더가 어떤 종류로 나뉘고 각각 무슨 일을 하는지 살펴본다.