@RequestBody Object Mapping 원리

Jae-Baek Song·2023년 5월 2일
2

Object Mapping 동작 원리 (사전 지식)

Spring Json Object Mapping

HandlerMethodArgumentResolver

애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter
ArgumentResolver 를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다.

MappingJackson2HttpMessageConverter

스프링 부트 기본 메시지 컨버터

0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
  • ByteArrayHttpMessageConverter : byte[] 데이터를 처리한다.
    클래스 타입: byte[] , 미디어타입: */* ,
  • StringHttpMessageConverter : String 문자로 데이터를 처리한다.
    클래스 타입: String , 미디어타입: */*
  • MappingJackson2HttpMessageConverter : application/json
    클래스 타입: 객체 또는 HashMap , 미디어타입 application/json 관련


Jackson는 어떻게 Object Mapping 할까?

Jackson의 ObjectMapper는 Java Object ←→ JSON 파싱을 쉽게 해주는 클래스 입니다.
ObjectMapper 클래스를 살펴보면, serialize와 deserialize라는 단어가 포함된 메서드가 많이 있습니다.
Java 오브젝트를 Json으로 파싱하는 것을 직렬화(serialize)라고 하며, 반대는 역직렬화(deserialize)라고 합니다.

jackson은 java.lang reflection 라이브러리를 사용한다.


Reflection 이란?

구체적인 클래스 타입을 알지 못해도, 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API

자바에서 제공하는 리플렉션(Reflection)은 C, C++과 같은 언어를 비롯한 다른 언어에서는 볼 수 없는 기능이다. 이미 로딩이 완료된 클래스에서 또 다른 클래스를 동적으로 로딩(Dynamic Loading)하여 생성자(Constructor), 멤버 필드(Member Variables) 그리고 멤버 메서드(Member Method) 등을 사용할 수 있도록 한다.

java Reflection이 가져올 수 없는 정보 중 하나가 바로 생성자의 인자 정보들이다. 따라서 기본 생성자 없이 파라미터가 있는 생성자만 존재한다면 java Reflection이 객체를 생성할 수 없게 되는 것이다.

참고

Gradle 에서는 기본생성자 없이 동작하는 이유?

Spring에서 기본생성자가 없어도 Object Mapping이 정상적으로 동작한다.

JDK 8 이후 생성자의 인자 이름을 알 수 있게 되었고, 그런 일을 하는 모듈이 추가되었다.

  • public 생성자만으로 역직렬화가 가능하다.
  • 컴파일 시 -parameters 옵션을 주어야 정상 동작한다.

참고

인자가 한개인 생성자에서는 동작하지 않는 이유?

인수가 1개인 생성자(및 정적 팩터리 메서드)는 모호하기 때문에 바인딩에는 2가지 가능한 "모드"가 있습니다.
참고
참고

  • 속성 기반(Properties-based) : 입력은 JSON 객체여야 하며, 속성은 생성자 인자와 이름으로 일치해야 합니다(따라서 이름은 위와 같이 바이트 코드이거나 주석이 있어야 합니다); 속성 값은 일치하는 인수가 나타내는 유형으로 역직렬화됩니다. 이 모드는 인자 수에 관계없이 모든 생성자에서 작동합니다.

  • 위임(Delegating) : 입력은 모든 JSON 유형이 될 수 있으며, 생성자 인자 하나(유일한)의 유형으로 역직렬화됩니다(이름은 상관없지만 유형은 일치해야 함). 이 모드는 인수가 1개인 생성자에서만 작동합니다(실제로 @JacksonInject를 사용하는 특수한 경우가 있으며 정확한 규칙은 "주입되지 않은 매개변수가 하나뿐이어야 한다"는 것입니다).

// accepts JSON value like: "some text"
// and serializes back to JSON String
public class StringWrapper {
  private String value;
  @JsonCreator(mode = JsonCreator.MODE.DELEGATING)
  public StringWrapper(String v) {
    value = v;
  }
  // commonly serialized like so:
  @JsonValue
  public String getValue() {
    return value;
  }
}
// accepts JSON value like: { "id" : "xc974" }
public class IdToken {
  private String id;
  @JsonCreator(mode = JsonCreator.MODE.PROPERTIES)
  public IdToken(@JsonProperty(id") String id) {
    this.id = id;
  }
  // will serialize as JSON object with this property:
  public String getId() { return id; }
}

즉 인자가 하나인경우 JSON value 가 some text 와 같이 String-value 단순 문자열인지 {"name" : "value"} 인지 모호하다는 뜻인것 같다..

해결책

  • 기본 생성자를 생성한다.
  • @JsonCreator(mode = JsonCreator.MODE.PROPERTIES) 를 명시해준다.

기본생성자로 인스턴스를 생성할경우 내부 값은 어떻게 채워줄까?

Jackson은 JSON 필드의 이름을 Java 오브젝트의 getter 및 setter 메소드와 일치시켜 JSON 오브젝트의 필드를 Java 오브젝트의 필드에 매칭합니다. Jackson은 getter 및 setter 메소드 이름의 "get"및 "set"부분을 제거하고 나머지 이름의 첫 문자를 소문자로 변환합니다.
예를 들어 brand라는 JSON 필드는 getBrand () 및 setBrand ()라는 Java getter 및 setter 메소드와 일치합니다. engineNumber라는 JSON 필드는 getEngineNumber () 및 setEngineNumber ()라는 getter 및 setter와 일치합니다.

동일한 이름의 변수를 직접 찾는 것이 아니라, Getter와 Setter를 통해 찾아서 매칭한다는 것입니다.

reflection을 사용해서 값을 필드에 주입해주므로 Setter는 필요하지 않다.

참고

Response Serialize시 Getter가 필요하다.

Request 에서는 Deserialize시 Getter가 없어도 객체를 생성및 필드 할당이 가능하다.
Response 에서 Serialize시 Getter가 필요하다.

Response 만 Breaking Point에 걸린다.

결론

이외에도 intellij build 또는 @JsonProperty , @JsonCreator 등에 어노테이션에 따라서 결과가 달라진다.

위 내용을 전부 생각하면서 개발하는것은 비효율적이다.
다음과 같이 DTO 규칙을 정해서 기본생성자와 Getter를 모두 만들어주는것이 좋은것같다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ItemResponse {

    private final Long id;
    private final String name;
    @JsonProperty("image-url")
    private final String imageUrl;
    private final int price;

}

기본 생성자가 필요한 이유
@RequestBody에 ArgumentResolver(아규먼트 리졸버)가 동작하지 않는 이유
Jackson ObjectMapper에서 기본 생성자 없이 Deserialization 하기
ObjectMapper의 동작 방식과 SpringBoot가 제공하는 추가 기능들
https://bbbicb.tistory.com/46
https://velog.io/@wisdom08/RequestBody%EB%A1%9C-String-%ED%83%80%EC%9E%85%EC%9D%84-%EB%B0%9B%EB%8A%94-%EA%B2%BD%EC%9A%B0

0개의 댓글