본문 바로가기
Programming

[HTTP] POST, PUT, PATCH 그리고 멱등성

by kghworks 2024. 1. 2.

HTTP의 멱등성

 동일한 요청을 한번 보내던, 여러번 보내던 동작과 서버의 상태 (DB 값 등)가 동일한 것을 의미한다. 즉 멱등한 API는 한번 요청하던, 여러 번 요청하던 그 결과(HTTP 응답이 아님, 동작의 결과임)가 같아야 한다. 그리고 이와 같은 HTTP API를 멱등성을 가진다고 말한다. 멱등성을 가진 API일지라도 그 응답은 매번 다를 수 있다.

 

 HTTP의 멱등성에 대해 얘기할 때 예시를 들면 좋은 것이 티켓팅이다. A라는 유저가 좌석 B 선점 요청을 한번 하던 여러번 하던 그 결과는 동일해야한다. 유저 A는 좌석 B를 1개 선점한다. 동일한 요청을 여러번 보내도 유저 A는 좌석 B를 1개만 선점한다.

 

 PUT과 DELETE는 멱등성을 가지도록 구현해야 한다. 멱등성을 가지기 위해 거의 필연적으로 멱등성 key(멱등 동작을 구현하기 위해 구별하는 식별 값)를 사용한다. URI 경로, header, payload에 명시하면 된다. IETF에서는 멱등성 key를 헤더에 포함하기를 권장하고 있고, 토스 페이먼츠도 아래처럼 header에 멱등성 key를 담는다고 한다.

https://blog.tossbusiness.com/articles/dev-1

curl --location --request DELETE 'http://localhost:8080/students/1' \
--data ''

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

curl --location --request DELETE 'http://localhost:8080/students/1' \
--data ''

HTTP/1.1 404 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

 

students/1 리소스에 DELETE를 요청하면 두 번째 요청부터 404를 응답한다. 매번 students/1을 제거하도록 동작되기 때문에 멱등하게 구현되었다. 멱등성 key는 student의 고유 식별번호로 URI path에 명시했다.

 

멱등성을 보장하지 않는 POST 예시를 보자.

curl --location 'http://localhost:8080/students' \
--header 'Content-Type: application/json' \
--data '{
    "student": {
        "name": "홍길동",
        "age": 19
    }
}'

HTTP/1.1 201 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "id": 1
}

curl --location 'http://localhost:8080/students' \
--header 'Content-Type: application/json' \
--data '{
    "student": {
        "name": "홍길동",
        "age": 19
    }
}'

HTTP/1.1 201 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "id": 2
}

 

동일한 요청을 할때마다 결과(동작)가 바뀐다. 매 요청마다 새로운 리소스를 생성했다. (id 1, 2) 이 메서드는 멱등성을 보장하지 못했다. POST는 HTTP 명세 상 멱등성을 보장하지 않아도 되기 때문에 문제없다. 

 

참고로 HTTP 명세상 메서드별 준수해야 하는 멱등성은 아래와 같다. (출처 : RFC 7231)

https://www.rfc-editor.org/rfc/rfc7231#section-8.1.3

 

멱등성을 보장하지 않는 POST

 HTTP에서 POST는 서버로 데이터 (request body)를 전송한다. non-idempotent, 즉 멱등성을 보장하지 않는다. 같은 요청을 여러번 하면 각각 매번 다른 동작을 한다.

curl --location 'http://localhost:8080/students' \
--header 'Content-Type: application/json' \
--data '{
    "student": {
        "name": "홍길동",
        "age": 19
    }
}'

HTTP/1.1 201 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "id": 1
}

curl --location 'http://localhost:8080/students' \
--header 'Content-Type: application/json' \
--data '{
    "student": {
        "name": "홍길동",
        "age": 19
    }
}'

HTTP/1.1 201 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "id": 2
}

curl --location 'http://localhost:8080/students' \
--header 'Content-Type: application/json' \
--data '{
    "student": {
        "name": "홍길동",
        "age": 19
    }
}'

HTTP/1.1 201 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "id": 3
}

 

 위 curl을 보면 매번 동일한 요청을 보내지만 요청마다 서버의 동작은 서로 다른 것을 알 수 있다. 요청 별로 서로 다른 새로운 리소스를 생성했다.

 

멱등해야 하는 PUT

PUT은 요청 payload를 통해 리소스를 저장하거나, 대체 (업데이트)한다.

  1. 리소스가 없으면 생성 (후 201 Created 응답)
  2. 리소스가 있으면 교체 (후 200 Ok or  204 No Content 응답)
curl --location 'http://localhost:8080/students/1' \
--header 'Content-Type: application/json' \
--data '{
    "student": {
        "name": "홍길동",
        "age": 19
    }
}'

HTTP/1.1 201 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

curl --location 'http://localhost:8080/students/1' \
--header 'Content-Type: application/json' \
--data '{
    "student": {
        "name": "홍길동",
        "age": 19
    }
}'

HTTP/1.1 204 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

curl --location 'http://localhost:8080/students/1' \
--header 'Content-Type: application/json' \
--data '{
    "student": {
        "name": "홍길동",
        "age": 19
    }
}'

HTTP/1.1 204 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

 

 POST와 다른 점은 멱등성을 보장하기 위해 리소스에 대한 구분 값이 필요하다는 것이다. 위에서는 어떤 리소스를 생성할지 URI에 명시한다. 최초 요청 시에는 해당 리소스가 존재하지 않아 생성했다. 그 이후에는 같은 요청이 들어오면 해당 리소스가 존재하기 때문에 리소스를 payload의 데이터로 대체(업데이트) 하였다. 

 멱등하다. 위 요청을 여러 번 하여도 최초 요청이라면 리소스를 생성하고, 2번째 요청부터는 리소스를 payload로 대체한다. 멱등하다.

 

멱등해도 되고, 안 해도 되는 PATCH

리소스의 부분 수정 (partial update) 시 사용한다. PUT은 payload 데이터로 리소스를 완전히 대체하여 멱등성을 확보한다. 반면 PATCH는 멱등성을 보장하는 메서드가 아니다. 개발자는 PATCH를 구현할 때 payload로 리소르를 대체하지 않도록 구현하여도 된다. (멱등성 보장을 위해 PUT처럼 구현할 수도 있다)

 

먼저 멱등성을 보장하지 않도록 PATCH를 구현하는 것을 보자

curl --location --request PATCH 'http://localhost:8080/students/1' \
--header 'Content-Type: application/json' \
--data '{
    "student": {
        "operation": "add",
        "age" : 12
    }
}'

HTTP/1.1 204 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

 

 위 HTTP API는 요청 시 age만큼 나이를 더하도록 구현되어 있다. 요청을 반복하면 리소스의 상태가 계속 바뀌므로 멱등하지 않은 PATCH 메서드다.

 

PATCH가 멱등한 경우는 아래와 같다.

curl --location --request PATCH 'http://localhost:8080/students/1' \
--header 'Content-Type: application/json' \
--data '{
    "student": {
        "age": 19
    }
}'

HTTP/1.1 204 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

 

위 API는 요청 시 리소스의 수정할 필드만을 payload에 명시하도록 설계되어 있다. 위 메서드를 연속으로 호출해도, 한 번만 호출해도 그 결과는 항상 age가 19로 수정될 것이다. 따라서 위 PATCH는 멱등하다.

 


멱등하지 않은 메서드를 멱등하게 구현해야 할까 (결제 요청 API를 POST로 구현했다면)

 여기서부터는 설계와 구현의 문제다. HTTP 명세상 POST는 멱등하지 않아도 되니까 멱등성을 보장할 필요가 없는 메서드다. 그럼 POST는 맘 놓고 멱등하지 않게 구현해도 괜찮을까?

 

 POST로 결제 요청 API를 개발한다면, 멱등하지 않은 결제 요청 API는 '따닥' 문제가 발생할 수 있다.

curl --location 'http://localhost:8080/payments/pay' \
--header 'Content-Type: application/json' \
--data '{
    "info": {
        "userId": "user001",
		"payNo" : "PAY102",
        "price": 13000
    }
}'

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

curl --location 'http://localhost:8080/payments/pay' \
--header 'Content-Type: application/json' \
--data '{
    "info": {
        "userId": "user001",
		"payNo" : "PAY102",
        "price": 13000
    }
}'

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

 

위처럼 결제 요청 API를 보낼 수 있고 해당 API가 멱등하지 않다면 어떤 문제가 발생할까?

 

 사용자가 실수로 마우스를 두 번 클릭해서 (일명 '따닥') 동일한 결제 건에 대해 서버에 2번 요청이 들어간다면 결제가 총 2번 되는 불상사가 일어난다. 따라서 위와 같은 HTTP API는 POST 메서드임에도 반드시 멱등하게 구현되어야 할 필요가 있다. 

 

 아래처럼 POST일지라도 멱등하게 구현하여 API를 올바르게 동작할 수 있도록 해야 한다.

curl --location 'http://localhost:8080/payments/pay' \
--header 'Content-Type: application/json' \
--data '{
    "info": {
        "userId": "user001",
        "payNo" : "PAY102",
        "price": 13000
    }
}'

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

curl --location 'http://localhost:8080/payments/pay' \
--header 'Content-Type: application/json' \
--data '{
    "info": {
        "userId": "user001",
        "payNo" : "PAY102",
        "price": 13000
    }
}'

HTTP/1.1 400 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 02 Jan 2024 08:13:11 GMT
Keep-Alive: timeout=60
Connection: keep-alive

 

 두 번째 요청에는 400 Bad Request를 응답받는다.  서버에서는 동일한 payNo (멱등 key)에 대한 요청이 여러 번 들어와도 한 번만 결제가 요청될 수 있도록 멱등하게 구현되었다.


참고

https://developer.mozilla.org/ko/docs/Web/HTTP/Methods/POST

 

POST - HTTP | MDN

HTTP POST 메서드는 서버로 데이터를 전송합니다. 요청 본문의 유형은 Content-Type 헤더로 나타냅니다.

developer.mozilla.org

https://developer.mozilla.org/ko/docs/Web/HTTP/Methods/PUT

 

PUT - HTTP | MDN

HTTP PUT 메서드는 요청 페이로드를 사용해 새로운 리소스를 생성하거나, 대상 리소스를 나타내는 데이터를 대체합니다.

developer.mozilla.org

https://developer.mozilla.org/ko/docs/Glossary/Idempotent

 

멱등성 - MDN Web Docs 용어 사전: 웹 용어 정의 | MDN

동일한 요청을 한 번 보내는 것과 여러 번 연속으로 보내는 것이 같은 효과를 지니고, 서버의 상태도 동일하게 남을 때, 해당 HTTP 메서드가 멱등성을 가졌다고 말합니다.

developer.mozilla.org

https://developer.mozilla.org/ko/docs/Web/HTTP/Methods/PATCH

 

PATCH - HTTP | MDN

HTTP PATCH 메소드는 리소스의 부분적인 수정을 할 때에 사용됩니다.

developer.mozilla.org

https://www.inflearn.com/questions/110644/patch-%EB%A9%94%EC%84%9C%EB%93%9C%EA%B0%80-%EB%A9%B1%EB%93%B1%EC%9D%B4-%EC%95%84%EB%8B%8C-%EC%9D%B4%EC%9C%A0

 

Patch 메서드가 멱등이 아닌 이유 - 인프런

패치의 경우 멱등성을 갖지 않는 이유가 무엇인가요? 외부 요인에 의해 값이 변경되지 않는 이상 항상 같은 결과를 가져오는 것 아닌가요..? - 질문 & 답변 | 인프런

www.inflearn.com

https://blog.tossbusiness.com/articles/dev-1

 

멱등성이 뭔가요?

생소한 표현이지만 알고 보면 쉬워요. 컴퓨터 과학에서 멱등하다는 것은 첫 번째 수행을 한 뒤 여러 차례 적용해도 결과를 변경시키지 않는 작업 또는 기능의 속성을 뜻해요. 즉, 멱등한 작업의

blog.tossbusiness.com

https://www.rfc-editor.org/rfc/rfc7231#section-8.1.3

 

RFC 7231: Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content

 

www.rfc-editor.org

 

댓글