Search

PATCH는 정말 idempotent 하지 않을까?

Intro

이번 글에서 사용하는 메소드는 HTTP method를 나타냅니다.
REST에 대해 공부하다 보면 자연스럽게 PUTPATCH의 차이점에 대해 궁금증을 가지게 됩니다.
두 개의 HTTP 메소드 모두 CRUD의 U인 Update에 사용되는 메소드지만 명백한 차이가 있습니다.
PUTPATCH의 차이점을 가장 단순하게 표현하면
PUT전체를 업데이트 하는 것(update)이고,
PATCH부분을 업데이트 하는 것(partial update)입니다.
여기서 조금 더 공부해보면 다음과 같은 차이점이 있다는 것도 알 수 있습니다.
PUT은 idempotent 하지만,
PATCH는 idempotent 하지 않다.
하지만 RESTful API를 개발하다보면 "PATCH가 정말 idempotent 하지 않은가?"에 대한 의문이 생깁니다.
실제 개발하고 운영되고 있는 PATCH API들은 대부분 idempotent 하다고 느껴지기 때문입니다.
RFC 문서에는 아래와 같이 명확히 적혀 있습니다:
PATCH is neither safe nor idempotent as defined by [RFC2616], Section 9.1. - RFC5789
그렇다면 저는 그동안 RESTful 하지 않은 방식으로 개발하고 있던 것일까요?
이번 글에서는
PATCH가 idempotent 하지 않은 메소드인지
하지만 실제로는 왜 idempotent 하다고 느껴지는지에 대해서 자세히 얘기해보겠습니다.

What is Idempotence?

먼저 idempotent 메소드가 뭔지 알아봅시다.
An HTTP method is idempotent if an identical request can be made once or several times in a row with the same effect while leaving the server in the same state. - MDN Web Docs

idempotent; 멱등(冪等)의

idempotent 메소드라 하면,
몇 번의 동일한 요청을 해도 언제나 동일한 resource의 상태를 보장하는 메소드를 말합니다.
네트워크 상의 문제로 혹은 클라이언트의 실수로 중복 요청해도 문제가 되지 않는 메소드를 뜻합니다.
간혹 idempotent한 메소드를 동일한 response를 반환하는 메소드라고 잘못 알고 있는 경우가 있습니다.
하지만 서버 resource의 상태가 동일하다면 response가 달라도 idempotent한 메소드입니다.
가장 대표적인 예시가 DELETE 메소드입니다.
예를 들어, 1번 유저를 삭제하기 위해 다음과 같은 request를 보냈다고 하겠습니다: DELETE /users/1
이에 따른 response status code는 resource 삭제를 알리는 204 No Content가 반환될 것 입니다.
여기에 다시 DELETE 요청을 하면 이미 삭제된 resource이기 때문에 404 Not Found가 반환됩니다.
두 번의 요청이 서로 다른 response와 status code를 반환하지만
DELETE 요청을 수차례 반복해도 해당 resource(1번 유저)가 삭제된 상태임은 변함이 없기 때문에 DELETE 메소드는 idempotent 합니다.
일반적으로 idempotent한 메소드는 다음과 같습니다: GET, HEAD, PUT, DELETE
그럼 POST 메소드는 어떨까요?
보통 CRUD의 Create를 담당하는 POST 메소드는 동일 request가 반복 요청되면 (중복 생성을 별도로 막지 않은 경우) 서버에 동일한 resource를 계속 생성할 것입니다.
그렇기 때문에 POST 메소드는 idempotent 하지 않습니다.
그럼 PATCH는 어떨까요?
다음과 같은 유저 데이터가 존재한다고 합시다:
{ "id": 1, "name": "original_name" }
JSON
복사
그리고 위 유저의 이름을 바꾸기 위해서 다음과 같은 PATCH request를 보냈다고 하겠습니다:
PATCH /users/1 { "name": "new_name" }
JSON
복사
PATCH의 결과는 다음과 같을 것입니다:
{ "id": 1, "name": "new_name" }
JSON
복사
동일한 PATCH 요청을 계속 반복해도 위의 상태를 변함이 없을 것입니다.
그렇다면 PATCH 메소드는 idempotent 한 것이 아닌가요?
아무리 생각해봐도 PATCH로 인해 idempotent 하지 않은 상태를 일으킬만한 상황이 떠오르지 않습니다.
다시 RFC 문서로 돌아가봅시다.

PUT과 PATCH의 차이

위에서 언급했던 RFC 5789의 바로 윗 문단에 다음과 같은 설명이 있습니다:
The difference between the PUT and PATCH requests is reflected in the way the server processes the enclosed entity to modify the resource identified by the Request-URI. In a PUT request, the enclosed entity is considered to be a modified version of the resource stored on the origin server, and the client is requesting that the stored version be replaced. With PATCH, however, the enclosed entity contains a set of instructions describing how a resource currently residing on the origin server should be modified to produce a new version. The PATCH method affects the resource identified by the Request-URI, and it also MAY have side effects on other resources; i.e., new resources may be created, or existing ones modified, by the application of a PATCH. - RFC 5789 section 2
문서에 따르면 PUT과 PATCH은 다음과 같은 차이점을 가지고 있습니다.

PUT

요청에 포함된 엔티티(=request body)는 서버에 저장된 원본 리소스의 수정된 버전으로 간주되고, 클라이언트는 원본을 대체하는 것을 요청합니다.
PUT 요청은 request body 전체로 원본 resource를 대체 해달라는 요청입니다.
새로 요청되는 값으로 기존 값 전체를 update 시키는 것입니다.
우리가 그 전까지 알던 사실과 크게 다르지 않습니다.
이제 PATCH로 넘어가봅시다.

PATCH

요청에 포함된 엔티티(=request body)는 새로운 버전의 생성을 위해 서버에 있는 원본 리소스가 어떻게 수정되어야 하는지를 나타내는 지시(또는 설명)의 집합이다.
뭔가 조금 이상합니다.
PATCH를 설명하는데 partial이라는 단어를 사용하지 않고, body로 원본 resource가 어떻게 수정되어야 하는 지에 대한 지시(instruction)를 받는다고 합니다.
그리고 PATCH는 다른 resource에 대해 side-effects를 일으킬 수도 있다고 합니다.
PATCH /file.txt HTTP/1.1 Host: www.example.com Content-Type: application/example If-Match: "e0023aa4e" Content-Length: 100 [description of changes]
JSON
복사
Request body에 담긴 내용을 [description of changes] 로 설명하고 있습니다.
// RFC 6902 PATCH /my/data HTTP/1.1 Host: example.org Content-Length: 326 Content-Type: application/json-patch+json If-Match: "abc123" [ { "op": "test", "path": "/a/b/c", "value": "foo" }, { "op": "remove", "path": "/a/b/c" }, { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }, { "op": "replace", "path": "/a/b/c", "value": 42 }, { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }, { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" } ]
JSON
복사
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }에 해당하는 부분이 바로 resource를 어떻게 수정할 지에 대한 지시(instruction)입니다.
예시에 따라 add operation을 수행하면 서버에 새로운 resource를 생성하게 됩니다.
또 move, copy와 같은 작업을 수행하면 다른 resource에 side-effects를 발생시킬 가능성이 있습니다.
위와 같이 PATCH를 반복 요청을 했을 때 서버가 항상 같은 상태를 유지하지 않을 수 있기 때문에 PATCH 메소드는 idempotent 하지 않은 것입니다.

JSON Merge Patch

약간 무서워지기 시작합니다. 그럼 그동안 사용했던 PATCH는 도대체 무엇일까요?
바로 JSON Merge Patch 포맷입니다.
다음과 같은 데이터가 있다고 하겠습니다:
// RFC 7396 { "title": "Goodbye!", "author" : { "givenName" : "John", "familyName" : "Doe" }, "tags":[ "example", "sample" ], "content": "This will be unchanged" }
JSON
복사
JSON Merge Patch 포맷에 따르면 다음과 같은 PATCH 요청이 가능합니다:
PATCH /my/resource HTTP/1.1 Host: example.org Content-Type: application/merge-patch+json { "title": "Hello!", "phoneNumber": "+01-123-456-7890", "author": { "familyName": null }, "tags": [ "example" ] }
JSON
복사
request에 의한 resource의 상태는 다음과 같을 것입니다:
{ "title": "Hello!", "author" : { "givenName" : "John" }, "tags": [ "example" ], "content": "This will be unchanged", "phoneNumber": "+01-123-456-7890" }
JSON
복사
위의 request는 반복 요청 했을 때 resource의 상태가 일정하므로 idempotent 합니다.
PATCH 메소드가 반드시 non-idempotent 해야만 하는 것은 아닙니다.
A PATCH request can be issued in such a way as to be idempotent, which also helps prevent bad outcomes from collisions between two PATCH requests on the same resource in a similar time frame. - RFC 5789

Conclusion

여기까지 글을 읽고나면 매우 혼란스러울 것입니다.
"그래서 PATCH는 idempotent 하다는거야? idempotent 하지 않다는거야?"라고 생각 할 수 있습니다.
REST를 제창한 Roy T. Fielding은 자신의 트위터에 다음과 같은 글을 남겼습니다.
그는 partial PUT이 RESTful 하지 않다고 생각했기 때문에 대신 PATCH를 만들었습니다.
PATCH를 이용하여 partial update 또는 idempotent 하지 않은 작업을 처리하라는 의도였습니다.
따라서 위의 내용을 모두 정리하면 다음과 같습니다:
최초에 의도한 PATCH는 idempotent 하지 않지만 idempotent하게 설계 되어도 괜찮다.
RESTful API를 설계할 때 중요한 부분 중 하나는 visibility입니다.
REST is designed to be visible and simple. Visibility of the service means that every aspect of it should self-descriptive and follow the natural HTTP language according to principles 3, 4, and 5. — RESTful Web API Design with Node.js 10 - Third Edition by Valentin Bojinov
클라이언트-서버 간 암묵적으로 따르는 규칙이 있고 해당 규칙에 따라 API가 설계 되어야 합니다.
PATCH는 기본적으로 idempotent하지 않은 메소드입니다.
별도의 약속을 하지 않아도 클라이언트와 서버 모두 그렇게 전제할 것입니다.
그렇기 때문에 통상적으로 PATCH 메소드를 설계하고 사용할 때 idempotent 하지 않다고 가정해야합니다.
하지만 PATCH 메소드가 idempotent하게 설계 되었다고 RESTful 하지 않은 것은 아닙니다.
오히려 의도하지 않은 side effects 막기 위해서는 idempotent하게 설계하는 편이 더 낫습니다.
이상으로 PATCH가 정말 idempotent 하지 않은지 알아봤습니다.
PATCH의 변천사를 통해 PATCH의 idempotence를 설명했는데 문서를 해석하는 과정에서 잘못 이해한 부분이 있을 수도 있습니다.
적극적인 피드백은 언제나 환영합니다.

References