REST API에서의 HTTP 상태 코드, 상태 메시지 (응답 코드, 응답 메시지)

HTTP와 REST

REST API 관점에서 HTTP를 해석해보자.

  • HTTP(HyperText Transfer Protocol)는 웹 환경에서 정보를 주고받기 위한 프로토콜(통신 규약, 약속)이다.
  • 클라이언트는 HTTP의 상태 코드를 확인하여 요청의 성공 및 실패를 확인할 수 있다.
  • 이것은 HTTP를 사용하는 클라이언트와 서버 간의 약속(프로토콜, 통신 규약)인 것이다.
  • REST(Representational State Transfer)는 분산 하이퍼미디어 시스템을 위한 소프트웨어 아키텍처이다.
  • HTTP는 웹 환경에서 정보를 송수신할 때 사용하는 약속이고, REST는 소프트웨어 아키텍처다. REST에 반드시 HTTP가 필요한 것은 아니다. WAPWebRTCMQTT 등 다른 프로토콜로도 이용 가능하다.
  • REST는 소프트웨어 아키텍처(설계 지침, 원리 등등)고 REST에서 클라이언트-서버 간 통신 시 HTTP를 사용한 것이다.

REST에서 HTTP를 주로 사용하는 이유

REST에서 HTTP는 필수가 아니라고 했지만, 웹 환경 통신의 대부분이 HTTP를 사용한다.

만약, 어떤 API의 요청응답 데이터 포맷이 YAML이면 어떨까? 물론 YAML이 JSON 포맷의 데이터를 대체하여 표현할 수 있고 JSON이 갖는 한계를 보완한 부분도 있지만, 일반적이지 않다. 일반적이지 않은 것은 불편함을 야기한다. 이러한 불편함을 감수하면서 일반적이지 않는 것을 사용하려면 매우 설득력 있는 근거가 필요하다. REST의 경우엔 현재로선 없다.

REST는 HTTP를 사용하는 것이 일반적이다. 필수가 아니라고 했지만, 거의 필수다. REST의 개념/원리에는 HTTP가 필수는 아니지만, 그 사용에 있어 필수적으로 변한 경우라 생각하면 좋을 거 같다. 누군가 만든 REST API가 HTTP를 사용하지 않는다면 이것부터 고치라고 지시할 것이다.

따라서 REST를 이용해 API를 설계하려면 HTTP에 대해서 알고 있어야 한다.

HTTP 상태 코드(Status Code)란?

HTTP 상태 코드(Status Code)는 HTTP 요청이 성공했는 지 실패했는 지를 서버에서 알려주는 코드이다.

HTTP 상태 코드(Status Code)의 기능, 목적

상태 코드의 기능은 정해져 있다. 하지만 목적은 정보 제공자와 소비자가 선택한 약속에 의해 다르게 적용될 수 있다. 예를 들면 상태 코드 200은 요청이 성공적으로 완료되었다는 메세지를 전달하는 기능을 갖고 이것을 이용해 클라이언트에 다음 작업을 이어 나가도 좋다는 신호의 목적으로 쓰일 수 있다. 따라서 각 상태 코드는 제공자에 따라 다른 목적으로 클라이언트에 제공될 수 있다.

앞으로 설명의 상태 코드들은 HTTP 상에서 갖는 기능에 대해서 설명하고 이것이 REST에서 어떤 목적으로 쓰이는지 위주로 설명할 것이다.

예제

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/51dc0a94-d60e-4ef0-bbb1-6a8c50e6243c/Untitled.png

  • Collection: /users/1 에서 users, 집합
  • Document: /users/1 에서 1, 집합에 속한 자원

2XX Success

2xx 번대의 상태 코드들은 서버가 클라이언트의 요청을 성공적으로 처리했다는 의미다.

200 OK : 클라이언트의 요청을 서버가 정상적으로 처리했다.

→ 대표적인 예 : 데이터 조회 성공

성공에 대한 모든 상태 코드를 200으로 응답해도 크게 상관없다. (200 상태 코드는 클라이언트에게 요청이 성공했다는 것을 응답하는 기능을 갖기 때문에) 많은 REST API에서 2xx 상태 코드를 세분화하여 사용하지 않는다.

하지만, 클라이언트에게 더 정확하고 자세한 정보를 제공하기 위해선 적절한 상태 코드를 보내는 것이 좋다. (2xx 상태 코드들은 각각 세분화된 목적을 갖는다.)

하지만, 아래 설계는 무조건 잘못된 것이니 수정하자!

HTTP/1.1 200 OK
{
    "result" : false
    "status" : 400
}
  • 상태 코드는 200으로 성공인데, body 내용엔 실패에 관한 내용을 리턴하고 있다.
  • 모든 응답을 200으로 처리하고 body 내용으로 성공 및 실패를 판단하는 구조에서 사용된다.

    → API가 아닌 HTML 웹 프로젝트에서 대부분 이렇게 사용한다.

    → 웹 프로젝트에선 크게 문제가 되지 않으나 API에선 많이 이상한 구조다.

    → 웹의 설계를 그대로 사용하는 경우 많이 하는 실수다.

201 Created : 클라이언트의 요청을 서버가 정상적으로 처리했고 새로운 리소스가 생겼다.

→ 대표적인 예 : 데이터 생성 성공 (ex: 게시글 작성, 댓글 작성 등등), 데이터 수정 성공

  • 201 상태 코드는 POST, PUT 요청에 대한 응답에 주로 사용된다.
  • 클라이언트의 요청이 성공적으로 이루어졌다는 의미까지는 200과 동일한데, 성공과 동시에 새로운 리소스가 생성되었다는 의미를 포함한다.
POST /users HTTP/1.1
Content-Type: application/json
{
		"name": "hak"
}
HTTP/1.1 201 Created
{
		"id" : 1,
		"name" : "hak"
}

물론 새로운 리소스를 생성하는 POSTPUT 요청의 응답으로 200을 보내줘도 된다. 또한 아직 많은 API의 상태 코드가 이렇게 응답한다. 하지만, 더 정확한 의미를 전달하기 위해 201 상태 코드를 쓸 것을 추천한다.

TIP!

HTTP 헤더의 Content-Location를 이용하여 만들어진 리소스 생성된 위치를 알려주면 더할 나위 없이 좋다.

HTTP/1.1 201 Created
Content-Location: /users/1
{
    "id" : 1,
    "name" : "hak"
}

202 Accepted : 클라이언트의 요청은 정상적이나, 서버가 요청을 완료하지 못했다.

202 상태 코드는 비동기에 대한 개념이 없다면 생소할 수 있다.

클라이언트의 요청이 정상적이면 서버에선 작업의 성공 및 실패를 응답하는 게 일반적이나, 작업 완료를 위한 일련의 작업들이 오래 걸리기 때문에 나중에 알려주겠다는 의미다.

  • 비동기에 대한 예시

202 상태 코드에서 중요한 것은 작업의 확인이다. 비동기 작업은 해당 요청이 언제 완료되는지 알 수 없다. 클라이언트가 요청의 완료 여부를 확인할 수 있는 방법을 제공해야 한다. 2가지 방법이 있다.

  1. Callback
  2. Polling

콜백은 서버가 작업이 완료되면 클라이언트에게 알려주는 것이다.폴링은 클라이언트가 주기적으로 해당 작업의 상태를 조회하는 것이다.(HATEOASContent-Location 등으로 작업의 상태를 확인할 수 있는 URI를 응답해야 한다.)콜백과 폴링에 대한 이야기는 길어질 수 있기 때문에 위에 설명한 개념 정도만 알고 넘어간다.

  • 콜백, 폴링에 대한 예시

결론은 비동기 요청(202 상태 코드)은 콜백이든 폴링이든 클라이언트가 요청의 완료 여부를 확인할 수 있는 방법을 제공해야 한다는 것이다.

둘 다 제공하는 것이 좋다.

204 No Content : 클라이언트의 요청은 정상적이다. 하지만 컨텐츠를 제공하지 않는다.

→ 대표적인 예 : 데이터 삭제 성공, 데이터 수정 성공

  • 202 상태 코드와 유사하게 이상한 응답이라 생각할 수 있다. 자원을 제공하지 않는 경우가 뭘까?
  • 204 상태 코드는 자원의 삭제 요청에 응답할 수 있다.
  • 204로 응답하는 순간, 별도의 데이터를 같이 실어서 보낼 수가 없다. 데이터를 같이 실어서 응답을 하려 해도 웹 자체에서 데이터가 없는 것처럼 처리해버린다.
DELETE /users/1 HTTP/1.1
HTTP/1.1 204 No Content

자원 삭제 요청을 했고 이 요청이 유효하니 서버는 해당 자원을 삭제했다. 더 이상 응답할 컨텐츠가 없기 때문에 컨텐츠가 없는 204로 응답한다.

여기서 주의할 점은 200으로 응답하고 응답 body에 null{}[]false 등으로 응답하는 것과 다르다는 것이다.204의 경우 HTTP Response body가 아예 존재하지 않는 경우다.

사실 204를 응답하는 API는 흔하지 않다.

**204로 응답하는 예시다. 절대적이진 않다.**

PUT

  • 자원 수정 요청의 결과가 기존의 자원 내용과 동일하여 변경된 내용이 없을 때 204로 응답할 수 있다.
  • 만약 수정 요청으로 자원의 내용이 변경된다면 201로 응답할 것이다.

DELETE

  • 삭제 요청으로 자원을 삭제하여 더 이상 존재하지 않고 그 자원을 참조하는 모든 자원도 삭제되어 더 이상 HTTP body를 응답하는 것이 무의미해졌을 때 사용한다.

❌ 4XX Client errors

4XX의 상태 코드들은 클라이언트의 요청이 유효하지 않아 서버가 해당 요청을 수행하지 않았다는 의미다.

400 Bad Request : 클라이언트의 요청이 유효하지 않아 더 이상 작업을 진행하지 않는 경우

API 서버는 클라이언트 요청이 들어오면 바로 작업을 진행하지 않고 요청이 서버가 정의한 유효성에 맞는지 확인 후 진행한다. 다음과 같은 사전 유효성 검증 작업을 진행할 수 있다.

  • 필수 여부
  • 유효 여부

    → 범위

    → 패턴

  • 문법 오류
  • 잘못된 형식의 요청
  • 전달 인자가 잘못되어 구문을 인식하지 못하는 경우
  • 서에서 지정한 구문을 충족시키지 않은 경우

대부분의 API는 사전에 유효성 검증을 통해 400 상태 코드로 클라이언트에게 유효하지 않은 요청임을 응답한다. (유효성 검증 없이 진행하면 5xx 서버 오류가 발생할 수 있기 때문에 대부분 사전에 막는 로직을 추가한다.)

그러나, 400 상태 코드로 응답하는 것만으로는 부족하다. 오류 발생 시 파라미터의 위치(pathquerybody), 사용자 입력 값, 에러 이유를 꼭 명시하는 것이 좋다.

Bad

HTTP/1.1 400 Bad Request

Good case 1

HTTP/1.1 400 Bad Request
{
    "message" : "'name'(body) must be String, input 'name': 123"
}

Good case 2

HTTP/1.1 400 Bad Request
{
    "errors": [
        {
        "location": "body",
        "param": "name",
        "value": 123,
        "error": "TypeError",
        "msg": "must be String"
        }
    ]
}

Good case 3

HTTP/1.1 400 Bad Request
{
    "errors": {
        "message": "'name'(body) must be String, input 'name': 123",
        "detail": [
            {
            "location": "body",
            "param": "name",
            "value": 123,
            "error": "TypeError",
            "msg": "must be String"
            }
        ]
    }
}

잘 만들어진 API는 대부분 문서 정리도 잘되었기 때문에 문서만으로 파라미터의 어떤 값들이 유효하고 유효하지 않은지 쉽게 알 수 있다. 그러나, 대부분 API는 기능 위주로 만들어지기 때문에 문서에 많은 시간을 쓸 수 없다. 이러한 API를 사용하면서 4XX 오류를 만나면 꽤 골치 아프다. 분명 클라이언트(요청자)의 잘못인데 어떤 부분이 잘못된 지 알려주지 않으니 유효한 요청을 하나씩 찾아야 한다. 400 상태 코드는 서버에서 잘못됐다고 판단하는 응답이기 때문에 서버는 그 요청이 왜 잘못된 지도 알 수 있다. 서버에선 쉽게 알 수 있지만 클라이언트는 알려주지 않으면 알기 어려운 내용인 것이다.

커피숍을 예시로 들면, 고객(클라이언트)이 바닐라라떼 더블샷(파라미터)을 주문(요청) 한다. 근데 이 커피숍에 바닐라라떼는 메뉴에 있지만, 샷 추가는 불가하다. 메뉴판(API 문서)엔 이 내용이 없다. 이때, 카운터(API)의 직원(URI)이 고객에게 해당 주문은 불가합니다라고만 하면 고객은 어떤 반응일까? 바닐라라떼가 메뉴가 없나(404 상태 코드)? 아니면 바닐라라떼는 주문이 많이 밀려서 불가한가(429 상태 코드)? 등의 생각을 할 것이다.카운터(API)의 직원(URI)은 고객에게 바닐라라떼에 샷 추가는 불가합니다라고 대답해야 한다.

401 Unauthorized : 클라이언트가 인증이 되지 않아 작업을 진행할 수 없는 경우

→ 대표적인 예 : 로그인조차 하지 않아서 접근할 수 없는 상태, 아이디나 비밀번호가 틀려서 로그인 실패 시

상태 코드 이름만 보면 권한(authorized)에 대한 내용처럼 보이지만, 사실 인증(authenticated)에 대한 이야기다. 401은 비인증(인증이 안 된 상태)이다. 결론적으로 401은 인증이 안 돼 자원을 이용할 수 없는 상태고, 의미상 unauthorized보다 unauthenticated가 더 정확하다.

  • 인증(authenticated)과 승인(authorized)의 차이를 설명하는 예시 1

403 Forbidden : 클라이언트가 인증은 됐으나 권한이 없기 때문에 작업을 진행할 수 없는 경우

→ 대표적인 예 : 로그인을 해서 인증은 됐지만(User에 대한 정보는 확실하게 알고 있지만), 관리자의 권한이 없어서 접근할 수 없는 상태

403은 권한(authorized)에 대한 내용이다. 401의 상태 코드명이 Unauthrozied라 혼동의 여지가 있으나, 권한에 대한 내용이다. 인증된 클라이언트가 권한이 없는 자원에 접근할 때 응답하는 상태 코드이다.

  • 인증(authenticated)과 승인(authorized)의 차이를 설명하는 예시 2

404 Not Found : 클라이언트가 요청한 자원이 존재하지 않다.

IT 분야에서 일하지 않는 사람이라도 인터넷을 하다 보면 404 오류는 한 번쯤 만나봤을 것이다. 지금 당장 https://www.google.com/hakhak2 이렇게 존재하지 않는 페이지를 방문하면 404 오류가 발생한다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/eb633043-4ba9-423d-bb7b-2dae02c0f8f8/Untitled.png

브라우저(클라이언트) 입장에선 자원이 웹 페이지 경로고 존재하지 않는 경로(자원)를 요청했기 때문에 404 상태 코드를 응답했다. REST API에선 크게 2가지 경우에서 404 상태 코드를 응답한다.

  1. 경로가 존재하지 않음
  2. 자원이 존재하지 않음
    • ‘경로’와 ‘자원’의 차이

사실 위에서 설명한 경로, 자원이 정확한 용어와 의미는 아니다. REST에선 URI가 자원이기 때문에 경로가 곧 자원이다. 404 오류의 경우 두 가지 모두 확인을 해야 한다는 설명을 위한 표현이다.

405 Method Not Allowed : 클라이언트의 요청이 허용되지 않는 메소드인 경우

메소드란 POST, GET, PUT, DELETE 등 HTTP Method를 말한다. 즉, 자원(URI)은 존재하지만 해당 자원이 지원하지 않는 메소드일 때 응답하는 상태 코드이다.

405 상태 코드는 OPTIONS 메소드와 HTTP header의 Allow와 연관되어 있다.

OPTIONS는 API가 허용하는 메서드가 어떤 것들이 있는 지 확인하는 메서드이다. 405 오류를 사전에 방지하기 위한 용도에 주로 쓰인다. 이 때, 응답 HTTP header의 Allow에 지원하는 메서드를 나열하여 응답한다.

RESTful API 설계 가이드에서도 말한 내용이지만 완성도 높은 API를 위해 제공하길 추천한다.(최근 몇몇 API 프레임워크에선 자동으로 허용되지 않는 메소드에 대해 405 상태 코드와 Allow 헤더를 응답하기도 한다.

OPTIONS /users/1 HTTP/1.1
HTTP/1.1 200 OK
Allow: GET,PUT,DELETE,OPTIONS,HEAD

/users/1 자원은 POST 메서드를 제공하지 않는다는 정보를 확인할 수 있다.

  • 405 상태 코드는 404 상태 코드와 혼동될 수 있기 때문에 규칙을 잘 정해야 한다.

409 Conflict : 클라이언트의 요청이 서버의 상태와 충돌이 발생한 경우

→ 주로 POST 요청 시 이미 존재하는 자원이 있는 경우에 대한 응답으로 사용. ex) 이미 회원가입이 되어있는데, 또 회원가입을 시도하는 경우 (= 중복된 아이디 또는 이메일인 경우)

충돌이라는 것은 매우 추상적이다. 앞에서 알아본 400, 401, 403, 404, 405 상태 코드들은 사용이 꽤 명확하다. 하지만, 충돌이라는 것은 정의하기 나름이다.그렇기 때문에 필자는 400, 401, 403, 404, 405 상태 코드에 속하기 애매한 오류의 상황들을 409로 응답한다.

물론, 나름의 규칙을 갖고 409로 응답한다.

  • 해당 요청의 처리 중 비지니스 로직상 불가능하거나 모순이 생긴 경우

    예를 들어, 다음의 경우

      DELETE /users/1 HTTP/1.1
      X-TOKEN: password
    
    • 자원(URI) /users/1에 존재하는 메소드이므로 405는 아니다.
    • /users/:id에서 :id가 유효한 형식이므로 400은 아니다.
    • 1 사용자도 존재하므로 404은 아니다.
    • 헤더의 인증(X-TOKEN)도 정확하므로 401은 아니다.
    • 삭제 권한도 있다고 가정했을 때 403은 아니다.

    클라이언트의 삭제 요청은 받아들여져서 200 혹은 204로 응답해야 하지만, 사용자의 게시물이 존재하는 경우 사용자를 삭제할 수 없다는 비지니스 로직이 있을 수 있다

    이렇게 API 사용에 있어 비지니스 로직상 모순이 발생하여 처리가 불가능한 경우 응답하는 상태 코드다.

      HTTP/1.1 409 Conflict
    

400 오류 상황과 마찬가지로 응답 시 오류의 원인을 알려야 한다. 추가적으로 HATEOAS를 이용해 클라이언트가 다음 상태로 전이될 수 있는 링크를 함께 응답하면 좋다.

HTTP/1.1 409 Conflict
{
		"message" : "First, delete posts"
		"links": [
				{
						"rel": "posts.delete",
						"method": "DELETE",
						"href": "https://api.rest.com/v1/users/1/posts"
				},
		]
}

429 Too Many Requests : 클라이언트가 일정 시간 동안 너무 많은 요청을 보낸 경우

비정상적인(DoS attackBrute-force attack) 방법으로 자원을 요청하는 경우 응답한다.

DoS는 가용성에 대한 공격이고 Brute-force는 기밀성에 대한 공격이다. 하지만, 서버 입장에서 두 공격 모두 가용성에 피해를 입을 수 있다. 서버가 감당하기 힘든 요청이 지속적으로 들어오면 서버는 해당 요청을 처리하기 위해 다른 작업을 처리하지 못할 수 있다.

429 상태 코드는 이러한 경우 일정 시간 뒤 요청할 것을 나타내는 것이다. 따라서 다음과 같이 HTTP header Retry-After을 이용한다.

HTTP/1.1 429 Too Many Requests
Retry-After: 3600
  • 클라이언트는 3600초 후에 다시 해당 자원에 대한 작업을 요청할 수 있다.

두 가지 상황을 예로 설명한다.

  • Case 1 : 기밀성에 대한 공격
  • Case 2 : 가용성에 대한 공격

💻 5XX Server errors 💻

5XX 상태 코드들은 서버 오류로 인해 요청을 수행할 수 없다는 의미다. 클라이언트의 요청은 유효하여 작업을 진행했는데 도중에 오류가 발생한 경우다. 404 오류와 마찬가지로 인터넷을 하다 보면 500, 502, 503 등의 오류를 만나봤을 거다. API 서버의 응답에서 5XX오류가 발생해서는 안된다. 보통 개발 과정에서 유효하지 않은 요청을 사전에 처리하지 않은 경우(400)에 많이 발생한다.

특히 500 오류는 개발자의 실수로 발생할 여지가 크다.

POST /users HTTP/1.1
{
    "name": 123
}

요청에 대해 4XX 오류를 발생시킬 가능성이 있는데 사전에 확인 작업을 하지 않은 경우

  • 파라미터 필수 값, 유효성 확인 없이 비지니스 로직 진행하는 경우
  • 외부 API에서 받은 객체를 확인하지 않고 비지니스 로직 진행하는 경우
    • 예시

API를 사용하는 클라이언트에게 5XX 상태 코드는 나타내지 말아야 한다! 최신의 웹 애플리케이션 프레임워크는 자체 웹서버를 내장하고 있어서 웹서버(Apache, Nginx) 없이 운영할 수 있다. 그러나, 보통 운영 레벨에서 이렇게 하는 경우는 드물고 앞에 웹서버를 두고 웹 애플리케이션을 연결해서 운영한다.

따라서, 상단의 웹서버(Apache, Nginx)에서 발생하는 어쩔 수 없는 오류를 제외하고 API에선 5XX 상태 코드가 응답되선 안된다. API 레벨에선 완벽한 예외처리를 통해 5XX 서버 오류 상태 코드를 방지해야 한다.

그리고 서버에 대한 에러를 클라이언트와 소통을 하지 않는 것이 보안적인 측면에서 더 좋기 때문에 서버 에러를 대부분 500으로 통일해서 응답한다.