PATCH로 리소스 해방시키기: 부분 업데이트의 기술

redjen·2024년 8월 30일
1

월간 딥다이브

목록 보기
8/11
post-thumbnail

백엔드를 개발할 때, 해당 리소스 전체를 업데이트 하기 위해서는 PUT, 해당 리소스의 일부를 업데이트하기 위해서는 PATCH method를 사용하는 규약이 이제는 어느정도 많은 사람들에게 공감대를 얻고 있는 것 같다.

https://tecoble.techcourse.co.kr/post/2020-08-17-put-vs-patch/

이 글에서도 볼 수 있듯이, 리소스의 일부 / 전체의 차이 말고도 멱등성에 관련한 차이도 있지만 이를 다룬 다른 좋은 글들이 너무 많기 때문에 이 글에서는 다루지 않을 예정이다.

부분 업데이트 요청 payload는 무엇을 담아야 할까?

요지는 이렇다.

리소스의 부분을 업데이트 하는데, 업데이트 대상 리소스의 필드를 어떻게 전달할 것인가?

단순히 생각해보면 간단히 해결할 수 있을 것 같은 문제이지만, 최선의 해결책을 찾기는 생각보다 쉽지 않아서 글을 작성해 본다.

태초에 json patch 가 있었다

https://jsonpatch.com/

서문에도 쓰여 있듯이, 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.nameprovince.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 값과, 요청 값에 애초에 존재하지 않았던 경우를 구분해야 한다.

JSON Merge Patch

이 문제를 해결하기 위해 제안된 것이 rfc 7396 JSON Merge Patch 이다.

이 규약은 앞서 설명했던 것처럼 명시적으로 리소스의 필드를 null 로 설정하는 경우에 대해 해당 필드를 unset 하자~ 라고 제안하는 것이다.

물론 다른 규약과는 구분되어야 하기 때문에, Content-Type: application/merge-patch+json 를 consume 하는 api에 대해서만 적용하자는 제안이다.

꽤나 그럴듯해 보인다. 필드를 unset해야만 하는 use case는 너무나도 많기 때문이다. 가령 아래를 예시로 들 수 있다.

  • 분산 환경에서 다른 여러 서버로의 요청을 날리다가 실패했을 때, 기존에 성공했던 요청에 대해 롤백하기 위한 경우
  • 리소스에 대해 큰 (admin과 같은) 권한을 가진 애플리케이션에서 데이터를 임의로 unset하는 기능을 구현하기 위해

spring 을 사용하고 있다면 jackson extension module을 쉽게 적용하여 역직렬화 하는 단게에서 명시적으로 null 로 채워진 필드를 구분할 수 있다.
https://github.com/OpenAPITools/jackson-databind-nullable

kotlin / spring / jpa에 적용을 원한다면 잘 설명된 블로그 글 도 있다.

다만 이 모듈 자체가 널리 유명해지진 않았기 때문에, 다른 라이브러리와의 호환성 문제가 몇 가지 있는 것으로 보인다.

  • openapi-generator와의 호환 문제가 있다. required nullable 필드를 정상적으로 인식하지 못한다.
  • mapstruct와 같은 mapper 라이브러리를 사용할 때 dto <-> entity 변환이 매끄럽지 않다.

그럼에도 불구하고 아직까지는 (내가 생각하기에) 가장 합리적이고 깔끔한 방법이라고 생각된다.

다른 방법을 사용한다면

업데이트하고자 하는 리소스의 특정 필드를 unset하기 위해서는 naive 하게 접근하는 방법도 많이 사용 중인 것 같다.

  • 업데이트 대상 필드가 enum 이라면 unset / default null 지원을 위한 별도 enum 값을 둔다던지 (NONE)
  • 업데이트 대상 필드가 string / integer / long과 같은 primitive 한 값이라면 MAX_VALUE, MIN_VALUE와 같은 잘 사용하지 않는 값을 (위험하지만) 세팅한다던지

아직까지는 이거다! 정답이다! 할 만큼 뚜렷한 해결책이 없다고 느껴진다.

정리

  • 리소스의 일부분만 업데이트하기 위해 json patch 규약이 등장하였으나 많은 호응을 얻지는 못함
  • json merge patch 를 사용하면 업데이트 대상이 되는 필드를 명시적으로 null로 설정하여 업데이트할 수 있지만 이는 라이브러리 및 생태계 지원이 미흡한 상태
  • 별도 상수 값을 사용하여 업데이트 대상 필드를 unset하기 위한 trigger로써 사용하는 방법 등을 사용할 수 있지만 아직까지 완벽하게 해당 문제를 해결하기 위한 방법은 찾지 못함
profile
make maketh install

0개의 댓글

관련 채용 정보