복붙노트

[SPRING] Spring을 기반으로 강력하게 형식화 된 언어로 올바르게 패치하는 법 - 예제

SPRING

Spring을 기반으로 강력하게 형식화 된 언어로 올바르게 패치하는 법 - 예제

내 지식에 따르면 :

꽤 간단한 HTTP 서버를 구현하기 위해 Spring을 사용하고 있습니다. 사용자가 자신의 데이터를 업데이트하고자 할 때, 사용자는 일부 끝점에 HTTP 패치를 만들어야합니다 (api / user라고 가정 해 봅시다). 그의 요청 본문은 다음과 같은 @RequestBody를 통해 DTO에 매핑됩니다.

class PatchUserRequest {
    @Email
    @Length(min = 5, max = 50)
    var email: String? = null

    @Length(max = 100)
    var name: String? = null
    ...
}

그런 다음이 클래스의 객체를 사용하여 사용자 객체를 업데이트 (패치)합니다.

fun patchWithRequest(userRequest: PatchUserRequest) {
    if (!userRequest.email.isNullOrEmpty()) {
        email = userRequest.email!!
    }
    if (!userRequest.name.isNullOrEmpty()) {
        name = userRequest.name
    }    
    ...
}

내 의심 : 클라이언트 (예 : 웹 앱)가 속성을 지우려면 어떻게해야합니까? 나는 그러한 변화를 무시할 것이다.

사용자가 속성을 지우려면 (의도적으로 null을 보낸 경우) 아니면 변경하지 않으려는 경우 어떻게 알 수 있습니까? 두 경우 모두 내 객체에서 null이됩니다.

여기서 두 가지 옵션을 볼 수 있습니다.

REST 및 모든 우수 사례와 조화하여 이러한 사례를 어떻게 적절하게 처리해야합니까?

편집하다:

이 예에서는 PATCH를 사용해서는 안되며 PUT을 사용하여 사용자를 업데이트해야합니다. 그러나 모델 변경 사항 (예 : 새 속성 추가)은 어떻게됩니까? 모든 사용자가 변경된 후에 API (또는 사용자 엔드 포인트 만)를 버전 화해야합니다. 예 : 이전 요청 본문에 PUT을 허용하는 api / v1 / user 끝점과 새 요청 본문에 PUT을 수락하는 api / v2 / user 끝점이 있습니다. 나는 그것이 해결책이 아니고 패치가 이유가 있다고 생각합니다.

해결법

  1. ==============================

    1.patchy는 Spring에서 PATCH를 적절히 처리하는 데 필요한 주요 상용구 코드를 처리하는 작은 라이브러리입니다. 예 :

    patchy는 Spring에서 PATCH를 적절히 처리하는 데 필요한 주요 상용구 코드를 처리하는 작은 라이브러리입니다. 예 :

    class Request : PatchyRequest {
        @get:NotBlank
        val name:String? by { _changes }
    
        override var _changes = mapOf<String,Any?>()
    }
    
    @RestController
    class PatchingCtrl {
        @RequestMapping("/", method = arrayOf(RequestMethod.PATCH))
        fun update(@Valid request: Request){
            request.applyChangesTo(entity)
        }
    }
    

    PATCH 요청은 자원에 적용 할 변경 사항을 나타 내기 때문에 명시 적으로 모델링해야합니다.

    한 가지 방법은 클라이언트가 제출 한 모든 키가 리소스의 해당 속성에 대한 변경 사항을 나타내는 일반적인 오래된 Map 을 사용하는 것입니다.

    @RequestMapping("/entity/{id}", method = arrayOf(RequestMethod.PATCH))
    fun update(@RequestBody changes:Map<String,Any?>, @PathVariable id:Long) {
        val entity = db.find<Entity>(id)
        changes.forEach { entry ->
            when(entry.key){
                "firstName" -> entity.firstName = entry.value?.toString() 
                "lastName" -> entity.lastName = entry.value?.toString() 
            }
        }
        db.save(entity)
    }
    

    위의 내용은 매우 쉽습니다.

    위의 내용은 도메인 레이어 객체에 유효성 검사 주석을 추가하여 완화 할 수 있습니다. 이것은 간단한 시나리오에서 매우 편리하지만 도메인 객체의 상태 나 변경을 수행하는 프린시 펄의 역할에 따라 조건부 유효성 검사를 도입하자 마자 비현실적입니다. 더욱 중요한 것은 제품 수명이 다한 후에 새로운 유효성 검사 규칙이 도입되어 사용자가 아닌 사용자 편집 컨텍스트에서 엔티티를 업데이트 할 수있게하는 것이 매우 일반적이라는 사실입니다. 도메인 계층에 불변량을 적용하는 것이 더 실용적인 것으로 보이지만 가장자리에서 유효성을 유지하십시오.

    이것은 실제로 태클하기 매우 쉽습니다. 그리고 80 %의 경우 다음과 같이 작동합니다 :

    fun Map<String,Any?>.applyTo(entity:Any) {
        val entityEditor = BeanWrapperImpl(entity)
        forEach { entry ->
            if(entityEditor.isWritableProperty(entry.key)){
                entityEditor.setPropertyValue(entry.key, entityEditor.convertForProperty(entry.value, entry.key))
            }
        }
    }
    

    Kotlin의 위임 된 속성 덕분에 Map 주위에 래퍼를 만드는 것이 매우 쉽습니다.

    class NameChangeRequest(val changes: Map<String, Any?> = mapOf()) {
        @get:NotBlank
        val firstName: String? by changes
        @get:NotBlank
        val lastName: String? by changes
    }
    

    Validator 인터페이스를 사용하여 다음과 같이 요청에없는 속성과 관련된 오류를 필터링 할 수 있습니다.

    fun filterOutFieldErrorsNotPresentInTheRequest(target:Any, attributesFromRequest: Map<String, Any?>?, source: Errors): BeanPropertyBindingResult {
        val attributes = attributesFromRequest ?: emptyMap()
        return BeanPropertyBindingResult(target, source.objectName).apply {
            source.allErrors.forEach { e ->
                if (e is FieldError) {
                    if (attributes.containsKey(e.field)) {
                        addError(e)
                    }
                } else {
                    addError(e)
                }
            }
        }
    }
    

    분명히 우리는 아래에서했던 HandlerMethodArgumentResolver를 사용하여 개발을 간소화 할 수 있습니다.

    위에서 설명한 내용을 사용하기 쉬운 라이브러리에 포장하는 것이 합리적이라고 생각했습니다. patchy를 사용하면 강력한 형식화 된 요청 입력 모델을 선언적 유효성 검사와 함께 사용할 수 있습니다. @Import (PatchyConfiguration :: class) 구성을 가져 와서 모델에 PatchyRequest 인터페이스를 구현하기 만하면됩니다.

  2. ==============================

    2.나는 동일한 문제를 겪었으므로 여기에 나의 경험 / 해결책이있다.

    나는 동일한 문제를 겪었으므로 여기에 나의 경험 / 해결책이있다.

    패치를 구현하는 것이 좋습니다. 그렇다면

    그렇게하지 않으면 곧 이해하기 힘든 API를 얻을 수 있습니다.

    그래서 첫 번째 옵션을 버리 겠어.

    두 번째 옵션은 제 의견으로는 좋은 선택입니다. 그리고 그것은 우리가 한 일이기도합니다.

    유효성 검사 속성을이 옵션과 함께 사용할 수 있는지 잘 모르겠지만이 유효성 검사를 도메인 계층에 적용하지 않아야합니까? 이로 인해 나머지 레이어에서 처리하고 잘못된 요청으로 변환 된 도메인에서 예외가 발생할 수 있습니다.

    이것이 우리가 한 가지 용도로 사용한 방법입니다.

    class PatchUserRequest {
      private boolean containsName = false;
      private String name;
    
      private boolean containsEmail = false;
      private String email;
    
      @Length(max = 100) // haven't tested this, but annotation is allowed on method, thus should work
      void setName(String name) {
        this.containsName = true;
        this.name = name;
      }
    
      boolean containsName() {
        return containsName;
      }
    
      String getName() {
        return name;
      }
    }
    ...
    

    json 디시리얼라이저는 PatchUserRequest를 인스턴스화하지만 현재 존재하는 필드에 대해서만 setter 메소드를 호출합니다. 누락 된 필드에 대한 contains boolean은 false로 유지됩니다.

    다른 앱에서 우리는 같은 원칙을 사용했지만 조금 다릅니다. (나는 이것을 선호한다)

    class PatchUserRequest {
      private static final String NAME_KEY = "name";
    
      private Map<String, ?> fields = new HashMap<>();;
    
      @Length(max = 100) // haven't tested this, but annotation is allowed on method, thus should work
      void setName(String name) {
        fields.put(NAME_KEY, name);
      }
    
      boolean containsName() {
        return fields.containsKey(NAME_KEY);
      }
    
      String getName() {
        return (String) fields.get(NAME_KEY);
      }
    }
    ...
    

    PatchUserRequest extends Map을 사용하여 동일한 작업을 수행 할 수도 있습니다.

    또 다른 옵션은 자신의 json 디시리얼라이저를 작성하는 것이지만, 나 자신을 시도하지 않았습니다.

    나는 이것에 동의하지 않는다. 나는 또한 당신이 진술 한 것과 같은 방법으로 PATCH & PUT을 사용한다.

  3. ==============================

    3.주된 문제점을 지적했듯이 우리는 명시 적 null과 암시 적 null을 구별하기 위해 null과 같은 값을 여러 개 가지지 않습니다. 이 질문에 Kotlin 태그를 지정 했으므로 Delegated Properties 및 Property References를 사용하는 솔루션을 만들려고했습니다. 중요한 제약 조건 중 하나는 Spring Boot가 사용하는 Jackson과 투명하게 작동한다는 것입니다.

    주된 문제점을 지적했듯이 우리는 명시 적 null과 암시 적 null을 구별하기 위해 null과 같은 값을 여러 개 가지지 않습니다. 이 질문에 Kotlin 태그를 지정 했으므로 Delegated Properties 및 Property References를 사용하는 솔루션을 만들려고했습니다. 중요한 제약 조건 중 하나는 Spring Boot가 사용하는 Jackson과 투명하게 작동한다는 것입니다.

    아이디어는 위임 된 속성을 사용하여 필드가 명시 적으로 null로 설정된 정보를 자동으로 저장하는 것입니다.

    먼저 델리게이트를 정의하십시오.

    class ExpNull<R, T>(private val explicitNulls: MutableSet<KProperty<*>>) {
        private var v: T? = null
        operator fun getValue(thisRef: R, property: KProperty<*>) = v
        operator fun setValue(thisRef: R, property: KProperty<*>, value: T) {
            if (value == null) explicitNulls += property
            else explicitNulls -= property
            v = value
        }
    }
    

    이것은 프롭퍼티의 프록시와 같이 동작 합니다만, 지정된 MutableSet에 null 프롭퍼티를 포함합니다.

    이제 DTO에서

    class User {
        val explicitNulls = mutableSetOf<KProperty<*>>() 
        var name: String? by ExpNull(explicitNulls)
    }
    

    사용법은 다음과 같습니다.

    @Test fun `test with missing field`() {
        val json = "{}"
    
        val user = ObjectMapper().readValue(json, User::class.java)
        assertTrue(user.name == null)
        assertTrue(user.explicitNulls.isEmpty())
    }
    
    @Test fun `test with explicit null`() {
        val json = "{\"name\": null}"
    
        val user = ObjectMapper().readValue(json, User::class.java)
        assertTrue(user.name == null)
        assertEquals(user.explicitNulls, setOf(User::name))
    }
    

    이것은 Jackson이 두 번째 경우에 명시 적으로 user.setName (null)을 호출하고 첫 번째 경우에 호출을 생략하기 때문에 작동합니다.

    물론 조금 더 공상적이고 DTO가 구현해야하는 인터페이스에 몇 가지 메소드를 추가 할 수 있습니다.

    interface ExpNullable {
        val explicitNulls: Set<KProperty<*>>
    
        fun isExplicitNull(property: KProperty<*>) = property in explicitNulls
    }
    

    user.isExplicitNull (User :: name)을 사용하여 검사를 약간 더 멋지게 만듭니다.

  4. ==============================

    4.일부 응용 프로그램에서 수행하는 작업은 값의 설정 여부를 구별 할 수있는 OptionalInput 클래스를 만드는 것입니다.

    일부 응용 프로그램에서 수행하는 작업은 값의 설정 여부를 구별 할 수있는 OptionalInput 클래스를 만드는 것입니다.

    class OptionalInput<T> {
    
        private boolean _isSet = false
    
        @Valid
        private T value
    
        void set(T value) {
            this._isSet = true
            this.value = value
        }
    
        T get() {
            return this.value
        }
    
        boolean isSet() {
            return this._isSet
        }
    }
    

    그런 다음 요청 클래스에서

    class PatchUserRequest {
    
        @OptionalInputLength(max = 100L)
        final OptionalInput<String> name = new OptionalInput<>()
    
        void setName(String name) {
            this.name.set(name)
        }
    }
    

    @OptionalInputLength를 생성하여 속성을 검증 할 수 있습니다.

    용도는 다음과 같습니다.

    void update(@Valid @RequestBody PatchUserRequest request) {
        if (request.name.isSet()) {
            // Do the stuff
        }
    }
    

    참고 : 코드는 groovy로 작성되었지만 아이디어를 얻을 수 있습니다. 이미 몇 가지 API에 대해이 접근 방식을 사용했으며 작업을 잘 수행하고있는 것으로 보입니다.

  5. from https://stackoverflow.com/questions/36907723/how-to-do-patch-properly-in-strongly-typed-languages-based-on-spring-example by cc-by-sa and MIT license