티스토리 뷰

많은 기업들은 rest-api 를 어느정도까지 restful하게 구현해서 사용하고 있을까?

진정 rest-api 라고 부를수 있으려면 self-descriptive 와 hateos를 만족해야한다고 한다.

self-descriptive 란 메세지가 스스로 설명이 되어야한다는 뜻인데, 요청과 응답 메세지만 봐도 해당 메세지가 무엇인지 알 수 있어야 한다는 것이다. rest-api를 설계할때는 url에 동사를 사용하지말고 명사만 사용하고, 행위에 대한 메세지는 HTTP 메소드를 이용하는 방식으로 api를 설계하라고 한다. (이 부분은 사람마다 의견이 갈린다. 내부용 api는 제약없이 편하게 쓰자고 하는 사람도 있고..)

그 중 데이터를 수정할 때 사용하는 HTTP 메소드 방식으로는 PUT과 PATCH가 있는데 PUT은 데이터 전체를 교체할때 사용하고 PATCH는 부분적인 데이터 수정시에 사용한다. 그런데 PATCH 같은 경우는 부분적인 수정이므로 굉장히 다양한 케이스가 나올 수 밖에 없고 구현하는 방법도 다 제 각각일것이다. 그래서 이 문제를 해결(표준화)하기 위해 국제인터넷표준화기구(IETF)에서는 RFC6902RFC7396 2개의 문서를 게시하였다. 두가지 모두 JSON기반의 PATCH 형식을 처리하기 위한 방법에 대한 문서인데, RFC6902는 JsonPatch에 대해, RFC7396은 JsonMergePatch 에 대한 내용이다. 두가지 방법에 대해 간단하게 알아보고, Java 를 이용한 JsonPatch 는 어떻게 구현하는지 정리해본다.

JsonPatch

  • JsonPatch 방식은 커맨드 방식으로 동작한다.
  • op, path, value 3개의 항목으로 구성되어 있으며 각 항목이 의미하는것은 아래와 같다. (순서는 관계없다.)
  • op : 작업유형 (add, remove, replace, move, copy or test 중에 하나만 사용가능)
  • path : 변경할 데이터 경로
  • value : 변경할 값
  • content-type : application/json-patch+json
//변경전 기존데이터
{
    "users": [
        { "id": 1, "name" : "Alice", "email" : "alice@example.org" },
        { "id": 2, "name" : "Bob", "email" : "bob@example.org" }
    ]
}

 

//PATCH 메소드를 이용한 변경요청
PATCH /users/2

[
    {
        "op": "replace",
        "path": "/name",
        "value": "kildong"
    },
    {
        "op": "replace",
        "path": "/email",
        "value": "kildong@test.com"
    }
]

 

//변경 후 데이터
{
    "users": [
        { "id": 1, "name" : "Alice", "email" : "alice@example.org" },
        { "id": 2, "name" : "kildong", "email" : "kildong@test.com" }
    ]
}

JsonMergePatch

  • JsonMergePatch 는 JsonPatch보다 단순하고 간단하다.
  • 그냥 변경하려는 데이터를 json 형식으로 작성하여 던지면 해당되는 데이터들이 merge되는 방식이다.
  • 키를 null로 변경하는것은 삭제를 의미한다.
  • content-type : application/merge-patch+json
//변경 전 기존데이터
{
    "a": "b",
    "c": {
        "d": "e",
        "f": "g"
    }
}

 

//PATCH 메소드를 이용한 변경요청
{
    "a": "z",
    "c": {
        "f": null
    }
}

 

//변경 후 데이터
{
    "a": "z",
    "c": {
        "d": "e"
    }
}

JSR-353, JSR-374는 무엇인가?

  • 둘 다 Json 형식의 데이터를 처리하기 위한 프로세서 스펙
  • JSR-353 이 초기버전 1.0
  • JSR-374 가 후속버전 1.1

JSR-374에서 무엇이 달라졌나?

  • JSON포인터와 JSON패치를 지원함
  • JSON포인터는 JsonPath와 비슷하다. json 데이터를 탐색하고 가져오는 방법을 말한다. (예 : /a/b/c) 

JsonPatch를 사용하기 위한 설정은 무엇을 해주어야 하나?

1. 의존성 추가

//JSR-374 API(인터페이스) 의존성 추가  
implementation 'javax.json:javax.json-api:1.1.4'

//JSR-374 API-Implements(구현체) 의존성 추가  
implementation 'org.glassfish:javax.json:1.1.4' // 기본  
//implementation 'org.apache.johnzon:johnzon-core:1.2.14' //서드파티 (의존성 추가시 서비스로더에 의해 우선 선택됨, 없으면 기본 glassfish를 선택)

//Json 라이브러리로 Jackson 사용시 JSR-374 Serialize/Deserialize 를 처리하기 위한 의존성 추가  
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr353'

 

2. JsonPatch 스펙의 요청이 올때 JsonPatch 로 컨버팅해줄 컨버터 추가

import javax.json.Json;
import javax.json.JsonPatch;
import javax.json.JsonReader;
import org.apache.commons.lang3.NotImplementedException;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.stereotype.Component;

@Component
public class JsonPatchHttpMessageConverter extends AbstractHttpMessageConverter<JsonPatch> {

    public JsonPatchHttpMessageConverter() {
        super(MediaType.valueOf("application/json-patch+json"));
    }

    @Override
    protected JsonPatch readInternal(Class<? extends JsonPatch> clazz, HttpInputMessage inputMessage) throws HttpMessageNotReadableException {

        try (JsonReader reader = Json.createReader(inputMessage.getBody())) {
            return Json.createPatch(reader.readArray());
        } catch (Exception e) {
            throw new HttpMessageNotReadableException(e.getMessage(), inputMessage);
        }
    }

    @Override
    protected void writeInternal(JsonPatch jsonPatch, HttpOutputMessage outputMessage) throws HttpMessageNotWritableException {
        throw new NotImplementedException("The write Json patch is not implemented");
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return JsonPatch.class.isAssignableFrom(clazz);
    }

}

 

import java.io.IOException;
import javax.json.Json;
import javax.json.JsonMergePatch;
import javax.json.JsonReader;
import javax.json.JsonWriter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.stereotype.Component;

@Component
public class JsonMergePatchHttpMessageConverter extends AbstractHttpMessageConverter<JsonMergePatch> {

    public JsonMergePatchHttpMessageConverter() {
        super(MediaType.valueOf("application/merge-patch+json"));
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return JsonMergePatch.class.isAssignableFrom(clazz);
    }

    @Override
    protected JsonMergePatch readInternal(Class<? extends JsonMergePatch> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
        try (JsonReader reader = Json.createReader(httpInputMessage.getBody())) {
            return Json.createMergePatch(reader.readValue());
        } catch (Exception e) {
            throw new HttpMessageNotReadableException(e.getMessage(), httpInputMessage);
        }
    }

    @Override
    protected void writeInternal(JsonMergePatch jsonMergePatch, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
        try (JsonWriter writer = Json.createWriter(httpOutputMessage.getBody())) {
            writer.write(jsonMergePatch.toJsonValue());
        } catch (Exception e) {
            throw new HttpMessageNotWritableException(e.getMessage(), e);
        }
    }
}

 

3. ObjectMapper에 모듈 추가

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new JavaTimeModule());
    objectMapper.registerModule(new JSR353Module()); //jsr-353,jsr-374 module
    return objectMapper;
}

 

4. Controller

@PatchMapping(value = "/api/person/{id}", consumes = "application/json-patch+json")
public ResponseEntity<PersonResponse> patchPerson(@PathVariable Long id, @RequestBody JsonPatch jsonPatch) {
    PersonResponse personResponse = personService.patchPerson(id, jsonPatch);
    return ResponseEntity.ok().body(personResponse);
}

 

5. Service

@Transactional
public PersonResponse patchPerson(Long id, JsonPatch jsonPatch) {
    Person originalPerson = findById(id);
    Person modifiedPerson = mergePerson(originalPerson, jsonPatch); //패치처리
    originalPerson.update(modifiedPerson); //변경감지
    logger.debug("modified person : {}", modifiedPerson);
    return PersonResponse.of(modifiedPerson);
}

private Person mergePerson(Person originalPerson, JsonPatch jsonPatch) {
    JsonStructure target = objectMapper.convertValue(originalPerson, JsonStructure.class);
    JsonValue patchedPerson = jsonPatch.apply(target);
    return objectMapper.convertValue(patchedPerson, Person.class);
}

정리

  • JSON 기반의 PATCH를 처리하기 위한 방법은 아주 다양할 수 있지만 IETF에서는 표준화를 위한 문서를 제공하고 있다. 표준이라고 하니 가능하면 지키는것이 좋지 않을까?
  • JsonPatch는 복잡하고 보다 명확한 부분적 수정이 필요할때 사용하면 되고, JsonMergePatch는 단순하고 간단한 데이터의 부분적 수정이 필요할때 사용하면 되겟다.
  • Java 진영에서는 JSON기반의 PATCH를 지원하기 위해 JSR-353 을 개선한 JSR-374 를 추가하였고, javax.json-api 라이브러리(JSR-374용)내에 JsonPatch 인터페이스가 존재한다. 이 API의 구현체로는 glassfish, johnzon 등의 라이브러리를 사용할 수 있다.
  • JPA를 이용할경우 부분적인 수정을 위한 API는 하나만으로 모두 처리가 가능할듯 하다. 어떤 필드던 수정이 가능하므로 어떤부분에 있어서는 위험할수도 있다. 가령 외부의 요인에 의해서 절대 변경되어선 안되는 데이터가 있다던지... 이게 좋은 방법인지는 잘 모르겠으나, 일단 필요에 따라 부분수정을 위한 API를 여러개 만들 필요가 없게 되어 귀차니즘이 해결된다..
    (특정필드를 패치대상에서 제외시키는 방법도 찾아보면 있을듯한데, JPA의 컬럼 매핑시에 updatable="false" 로 주어도 같은 효과를 낼수 있지 않을까?)
  • 이 PATCH API를 만들면서 알게된 것 중 또 하나, 영속상태의 엔티티의 변경이 감지되어 update 쿼리가 실행될 때 별도의 설정을 하지 않으면 모든 필드를 업데이트하는 sql이 실행된다. 변경된 필드만 업데이트를 하고 싶다면 도메인 클래스에 @DynamicUpdate 어노테이션을 달아주면 된다.
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함