May 17, 2021
HTTP 상태코드란 클라이언트가 보낸 요청의 처리 상태를 응답에서 알려주기 위해 정의한 기능이다. 즉 클라이언트는 HTTP 상태코드를 보고 내가 보낸 요청이 어떻게 처리되었는지 알 수 있다. 따라서 서버는 요청 결과에 알맞은 상태코드를 보내줘야한다.
클라이언트가 인식할 수 없는 상태코드를 서버가 반환한다면?
클라이언트는 상위 상태코드로 해석해서 처리한다. 미래에 새로운 생태 코드가 추가되어도 클라이언트를 변경하지 않아도 된다.
ex)
299 ??? -> 2xx(Successful)
451 ??? -> 4xx(Client Error)
(1xx 코드는 거의 사용되지 않으므로 패스)
200번대 상태코드는 클라이언트의 요청을 성공적으로 처리했음을 의미한다.
예시 요청-응답
요청
GET /members/100 HTTP/1.1
Host: localhost:8080
응답
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 34
{
"username": "awesomeo184",
"age": 50
}
예시 요청-응답
요청
POST /members HTTP/1.1
Content-Type: application/json
{
"username": "awesomeo184",
"age": 50
}
응답
HTTP/1.1 201 Created
Content-Type: application/json
Content-Length: 34
Location: /members/100
{
"username": "awesomeo184",
"age": 50
}
생성된 리소스는 응답의 Location 헤더 필드로 식별
자주 사용되지는 않는다.
클라이언트가 요청이 성공했다는 결과만 받으면 되는 경우, 결과 내용이 없어도 204 메시지만으로 성공을 인식할 수 있다.
예) 웹 문서 편집기에서 save 버튼
300번대 코드는 요청을 완료하기 위해 유저 에이전트(주로 웹브라우저)의 추가 조치가 필요하다는 것을 의미한다.
웹 브라우저는 3xx 응답 결과에 Location
헤더가 있으면, Location 위치로 자동 이동한다.
영구 리다이렉션 - 특정 리소스의 URI가 영구적으로 이동한 경우
일시 리다이렉션 - 일시적인 변경
특수 리다이렉션
리소스의 URI가 영구적으로 이동되었음을 의미한다. 기존의 URL을 더이상 사용하지 않을 때 사용한다.
요청한 리소스가 Location 헤더에 주어진 URL로 완전히 변경되었음을 나타낸다. 브라우저는 이 페이지로 리다이렉트하고, 검색 엔진은 해당 리소스로 연결되는 링크를 갱신한다 명세에는 요청과 본문이 변하면 안된다고 되어있지만 이를 따르지 않는 서버가 많으므로 GET과 HEAD 메서드의 응답으로만 사용하고 POST의 경우 308 Permanent Redirect를 사용하는 것이 좋다.
301과 기능은 같으나 리다이렉트시 요청 메서드와 본문이 유지되어야 함을 명시적으로 나타낸다. 예를 들어, 처음 POST를 보내면 리다이렉트도 POST로 유지되고 메시지 바디도 변경되지 않는다.
리소스의 URI가 일시적으로 변경된 경우 사용한다. 따라서 검색 엔진 등에서 URL을 변경하면 안된다. 실무에서 많이 쓰는 상태코드이다.
301 Moved Permanently와 마찬가지로 명세에는 요청과 본문이 변하면 안된다고 되어있지만 요청 메서드가 GET으로 변하고 본문이 제거될 수 있다.
302와 기능은 같으나 리다이렉트시 요청 메서드와 본문이 반드시 유지되어야 한다.
302와 기능은 같으나 요청 메서드가 반드시 GET으로 변경되어야 한다.
PRG는 POST 요청이 중복되지 않도록 하는 디자인 패턴이다. POST 요청은 멱등하지 않기 때문에 중복 요청되어선 안된다.
예를 들어 상품을 주문하는 POST 요청을 보낸 뒤, 클라이언트에서 새로고침을 할 경우 서버에서 중복 요청을 막기위한 조치를 해놓지 않았다면 상품 요청이 두번 전달되는 참사가 일어날 수 있다. 이를 막기 위한 기본적인 디자인 패턴이 바로 PRG이다. 물론 결제같은 중요한 로직은 PRG 외에도 추가적으로 중복 요청을 막기 위한 조치들을 해주어야한다.
PRG는 매우 간단하다. 주문 로직을 예로 들면 다음과 같다.
POST /order HTTP/1.1
Host: localhost:8080
itemId=mouse&count=1
HTTP/1.1 302 Found
Location: /order-result/19
GET /order-result/19 HTTP/1.1
Host: localhost:8080
HTTP/1.1 200 OK
<html>주문완료</html>
이렇게하면 클라이언트에서 주문 이후 새로고침을 하더라도 서버에는 GET 요청이 전달되어 안전하다. 새로고침을 하면 마지막 요청을 다시 보내기 때문에, GET 요청으로 리다이렉트하지 않았을 경우 자신의 마지막 요청이었던 POST 요청을 다시 보내게 되어 주문 요청이 두 번 들어갈 수 있다.
캐시를 목적으로 사용한다.
클라이언트: 캐시가 만료된 것 같은데? 서버야 캐시좀 다시 줘
서버: 클라이언트야, 캐시 아직 만료 안됐어, 브라우저에 있는 캐시 그냥 써 (304 Not Modified)
304 응답은 “리소스가 수정되지 않았으니 로컬에 저장된 캐시를 써라”라는 의미를 전달한다. 따라서 클라이언트는 캐시로 리다이렉트하게 된다. 캐시를 쓰기때문에 응답에 메시지 바디를 포함해서는 안된다.
클라이언트의 요청에 문제가 있어 서버가 요청을 수행할 수 없을 때 사용한다.
클라이언트에서 이미 잘못된 요청을 하고 있기 때문에 같은 요청을 몇번이고 보내도 응답은 똑같다. 500번대 응답의 경우, 서버 문제로 요청이 처리되지 않는 것이기 때문에 같은 요청이라도 서버의 문제가 해결된다면 응답이 달라질 수 있다.
요청 구문, 메시지 등으로 서버가 요청을 처리할 수 없는 경우 사용한다. 클라이언트는 요청 내용을 다시 검토한 후 재요청해야한다.
클라이언트가 해당 리소스에 대한 인증(Authentication)이 필요할 경우 사용한다. 401 오류 발생 시 WWW-Authenticate 헤더에 인증 방법에 대한 정보를 담아서 응답을 보낸다.
Authentication
: 인증. 본인이 누군지 확인(ex. 로그인)
Authorization
: 인가. 권한 부여(ex. admin, superuser)
401은 인증과 관련된 응답이지만 이름이 Unauthorized이다. 헷갈리지 않도록 주의
서버가 요청을 이해했지만 승인을 거부. 주로 인증 자격 증명은 있지만 접근 권한이 불충분한 경우 사용한다.
ex) 어드민 등급이 아닌 사용자가 로그인은 했지만, 어드민 등급의 리소스에 접근하는 경우.
요청 리소스를 찾을 수 없음. 클라이언트가 요청한 리소스가 존재하지 않거나, 클라이언트가 권한이 부족한 리소스에 접근하는데 해당 리소스를 숨기고 싶을 때 사용한다.
클라이언트의 요청에는 문제가 없으나 서버 오류로 요청을 처리할 수 없음.
서버 문제로 오류 발생. 가장 범용적인 응답.
서비스 이용 불가. 서버가 일시적인 과부하 또는 예정된 작업으로 잠시 요청을 처리할 수 없음.
Retry-After 헤더 필드로 얼마 뒤에 복구되는지 보낼 수도 있다.
김영한님 Q&A 답변 내용 정리
실무에서 어떤 응답 코드를 줘야할지 애매한 경우가 있을 수 있다.
예를 들어, 고객 정보가 20살 이상일 경우에만 통과하는 로직이 있는데 클라이언트에서 15살 고객의 정보가 넘어왔을 때, 어떤 상태코드로 응답해야할까.
일단 500은 아니다. 20살 미만을 걸러내는 것은 정상적인 비즈니스 로직이지 서버 에러와는 무관하기 때문이다.
그럼 4xx 응답을 줘야할까? 만약 클라이언트와 서버가 HTTP API 스펙을 정할 때, 20살 미만은 넘겨서는 안된다고 합의했다면 클라이언트가 약속을 지키지 않은 것이기 때문에 4xx 응답을 주는 것이 맞다.
그런데 클라이언트에서 HTTP API 스펙을 모두 준수했을 때 발생하는 비즈니스 예외들은 어떻게 처리해야 할까.
여러 방법이 있을 수 있는데 그 중 하나는 200 OK 응답을 주면서 메시지 바디에 결과를 전달하는 방법이다.
만약 비즈니스 로직이 복잡하다면 봉투 패턴으로 비즈니스 코드를 전달한다.
{
"code": "fail" ("success", "fail", "hold", "deny" ...)
"data": {memberId: ... 결과 데이터}...
}
만일 비즈니스 로직이 거의 없는 단순한 경우라면 봉투 없이 간단하게 전달한다.
{
memberId: ...
username: ...
}
김영한님 인프런 강의 - 모든 개발자를 위한 HTTP 웹 기본 지식