백엔드를 개발할 때, 해당 리소스 전체를 업데이트 하기 위해서는 PUT, 해당 리소스의 일부를 업데이트하기 위해서는 PATCH method를 사용하는 규약이 이제는 어느정도 많은 사람들에게 공감대를 얻고 있는 것 같다.
https://tecoble.techcourse.co.kr/post/2020-08-17-put-vs-patch/
이 글에서도 볼 수 있듯이, 리소스의 일부 / 전체의 차이 말고도 멱등성에 관련한 차이도 있지만 이를 다룬 다른 좋은 글들이 너무 많기 때문에 이 글에서는 다루지 않을 예정이다.
요지는 이렇다.
리소스의 부분을 업데이트 하는데, 업데이트 대상 리소스의 필드를 어떻게 전달할 것인가?
단순히 생각해보면 간단히 해결할 수 있을 것 같은 문제이지만, 최선의 해결책을 찾기는 생각보다 쉽지 않아서 글을 작성해 본다.
서문에도 쓰여 있듯이, json patch는 json 문서의 변화를 기술하기 위한 포맷이다.
리소스의 일부분이 변화했을 때 replace 하기 위해 전체 리소스를 보내지 않아도 된다는 번거로움을 해결해준다.
예시에도 나와 있듯이..
{
"baz": "qux",
"foo": "bar"
}
리소스가 있다고 할 때, 다음과 같은 json patch를 가한다.
[
{ "op": "replace", "path": "/baz", "value": "boo" },
{ "op": "add", "path": "/hello", "value": ["world"] },
{ "op": "remove", "path": "/foo" }
]
그럼 결과물 리소스는 이렇게 수정된다.
{
"baz": "boo",
"hello": ["world"]
}
replace
op는 해당 path의 value를 대체한다.add
op는 해당 path에 value를 추가한다.remove
op는 해당 path의 value를 제거한다.간단하게 나열한 것처럼 규약의 자세한 사용법은 위 링크에 잘 설명되어 있다. 나라면 java 기반 프로젝트에 이를 적용한다고 했을 때 star 수가 제일 많은 json-patch 라이브러리를 사용할 것 같다.
하지만 이 규약은 (나를 포함한) 많은 사용자들에게 공감대를 얻지 못하였는데, 그도 그럴게 리소스의 일부를 수정하기 위해 op enum을 보고 판단해야 하는 번거로움이 있다.
nested depth가 커지고, 복잡한 리소스를 수정한다면 이를 수정하기 위해서 더 복잡한 요청이 필요한 것은 덤이다.
{
"address": {
"zibun": 13561,
"province": {
"name": "경기도",
"city": {
"name": "성남시",
// ...
}
},
}
}
위와 같은 리소스를 수정하기 위해서는 path의 depth도 증가해야 하는데, 이것보단 직접 nested property에 접근하여 지정하는 방식이 훨씬 직관적이라고 느껴진다.
동일 depth의 property를 수정함에 있어서도 또 다른 replace op가 필요한 것도 번거롭다.
(가령 위 예시에서 province.name
와 province.city.name
을 동시에 수정한다고 했을 때 path에 중복되는 부분이 필연적으로 생긴다)
application.properties 대신 application.yml 으로 spring 설정을 하는 것과 비슷하다고 말하면 와닿을지 모르겠지만.. 나는 적어도 리소스의 변화에 있어서는 nested depth가 직접 요청에 들어가 있는 쪽이 읽기 쉽다고 느껴진다.
가령 '다른 건 다 그대로 내버려 두되 나이만 줄어들고 싶어' 라는 요구사항을 구현하기 위한 api를 만든다고 가정해보자. 아래와 같이 payload를 가지는 api를 간단하게 생각할 수 있다.
PATCH /users/{userNo}
{
"age": "20"
}
쉽다. 변경 대상이 되는 age
필드 외의 다른 필드는 변경하지 않고 해당 필드만 20이라는 값으로 변경하겠다는 요청 자체도 이해하기 쉽다.
하지만 특정 케이스에서 이 age를 초기화 해야 하는 일이 있다고 생각해보자. 즉 age 필드의 unset이 필요한 경우이다.
PATCH /users/{userNo}
{
"age": null // ?
}
PATCH /users/{userNo}
{}
앗! 그런데 서버에서는 age
를 명시적으로 null
값으로 보낸 것과, 아예 age
필드를 보내지 않은 것이 둘 다 null로 매핑이 된다.
partial update, 즉 리소스의 일부만 업데이트하기 위해서는 업데이트할 리소스의 필드만 보낸다고 하면 이런 요구사항이 새롭게 생기는 것이다.
명시적으로 설정한 필드의 null 값과, 요청 값에 애초에 존재하지 않았던 경우를 구분해야 한다.
이 문제를 해결하기 위해 제안된 것이 rfc 7396 JSON Merge Patch 이다.
이 규약은 앞서 설명했던 것처럼 명시적으로 리소스의 필드를 null
로 설정하는 경우에 대해 해당 필드를 unset 하자~ 라고 제안하는 것이다.
물론 다른 규약과는 구분되어야 하기 때문에, Content-Type: application/merge-patch+json
를 consume 하는 api에 대해서만 적용하자는 제안이다.
꽤나 그럴듯해 보인다. 필드를 unset해야만 하는 use case는 너무나도 많기 때문이다. 가령 아래를 예시로 들 수 있다.
spring 을 사용하고 있다면 jackson extension module을 쉽게 적용하여 역직렬화 하는 단게에서 명시적으로 null
로 채워진 필드를 구분할 수 있다.
https://github.com/OpenAPITools/jackson-databind-nullable
kotlin / spring / jpa에 적용을 원한다면 잘 설명된 블로그 글 도 있다.
다만 이 모듈 자체가 널리 유명해지진 않았기 때문에, 다른 라이브러리와의 호환성 문제가 몇 가지 있는 것으로 보인다.
그럼에도 불구하고 아직까지는 (내가 생각하기에) 가장 합리적이고 깔끔한 방법이라고 생각된다.
업데이트하고자 하는 리소스의 특정 필드를 unset하기 위해서는 naive 하게 접근하는 방법도 많이 사용 중인 것 같다.
NONE
)MAX_VALUE
, MIN_VALUE
와 같은 잘 사용하지 않는 값을 (위험하지만) 세팅한다던지아직까지는 이거다! 정답이다! 할 만큼 뚜렷한 해결책이 없다고 느껴진다.