공부내용공유

RequestBody는 기본생성자가 필요없다.(feat: ParameterNamesModule) 본문

Spring/Spring MVC

RequestBody는 기본생성자가 필요없다.(feat: ParameterNamesModule)

forfun 2024. 1. 7. 16:59

서론


REST API 서버를 만들면 controller의 request DTO를 한 개쯤은 만들게 되고 편한 사용을 위해 여러 어노테이션을 사용하게된다.

 

지금까지 “RequestBody가 DTO로 맵핑될 때 그냥 대충 기본 생성자랑 리플렉션 사용해서 만들어진다“ 정도로만 알고 있었다..

코드 리뷰를 받다가 해당 부분 관련해서 질문을 받았는데 대답을 하지 못했고 굉장히 부끄러웠다..

 

수치심을 지식으로 바꿔보자.

 

 

본론


리뷰를 받았던 코드를 임의의 예시로 구현하였다.

@Getter
@Builder
public class request {
	
	private final String name;

	private final int age;

	private final List<String> options;
}

 

리뷰어님 : 어 @NoArgsConstructor가 없네요? 왜 안썼어요?

나: 어 그렇네요.. 깜빡하고 빼먹은거 같습니다.. 기본 생성자가 없는데도 맵핑이 됐네요.. 왜지?

 

내가 쓴 코드는 어떻게 작동했는지, 왜 그렇게 썼는지 말할 수 있어야 한다… 항상 명심하자..

 

일반적인 requestBody의 json을 entity로 맵핑하는 방식은 jackson2HttpMessageConverter가 불러와 리플렉션을 이용하여 맵핑을 한다.

 

맵핑 과정을 보기위해 AbstractMessageConverterMethodArgumentResolverreadWithMessageConverters에 break point를 찍고 디버깅을 하였다.

 

대략적인 과정을 설명하자면

 

readWithMessageConverter

readWithMessageConverter에서 각종 메타 정보들을 확인하고 mapping을 해줄 converter를 결정한다.

converter list 값들

 

converters 를 디버깅으로 확인해보면 이러한 종류의 converter들이 있고 우리의 예시에서는MappingJackson2HttpMessageConverter가 선택이 된다.

 

converter가 결정되면 코드에서 볼 수 있듯이 read 메서드를 통해 body 값을 읽어온다.

 

Read

이 다음부턴 계속 메서드 안의 메서드를 타고 들어간다.

이렇게 javaType을 가져와 readJavaType으로 값을 읽는다.

 

readJavaType

ENCODINGS 를 타고 들어가면 utf-8을 확인할 수 있다.
그에 따라 uniCode가 true가 되고 아래 분기문을 타게된다.

 

위 코드에서처럼 인코딩 방식을 UTF-8을 사용하고 있기 때문에 unicode가 true이고 objectMapper에 readValue 메소드를 불러온다.

 

etc…

그 뒤로는 연속적인 메소드 호출인데

  1. readValue()
  2. _readMapAndClose()
  3. readRootValue()
  4. JsonDeserializer 클래스의 deserialize() 
  5. deserializeFromObject() - 해당 메소드에서 우리가 눈여겨 봐야할 분기처리가 있다.
    1. 기본 생성자가 있는 경우 (createUsingDefault)
    2. 기본 생성자가 없는 경우 (deserializeFromObjectUsingNonDefault)

각 분기에 따른 메서드를 살펴보자!

 

createUsingDefault

  1. createUsingDefault (기본 생성자, 리플렉션을 사용)

createUsingDefault 메소드를 통해 bean을 만든다.
안으로 들어 가 보면 _defaultCreator.call()을 함을 볼 수 있다.

 

이렇게 기본 생성자를 호출하여 dto를 만들고 필드를 읽어와 하나씩 값을 넣어서 객체를 완성시킨다.

 

위 과정을 보면 당연히 기본 생성자가 있어야 작동해야한다.

하지만 리뷰를 받았던 코드의 request에는 @Getter, @Builder 밖에 없다.

 

즉 기본 생성자가 만들어지지 않는다.

 

하지만 request 객체는 정상적으로 만들어졌다. 왜그럴까?

정답은 아래 메서드에 있다.

 

deserializeFromObjectUsingNonDefault

위에서 얘기했듯이 deserializeFromObject 클래스에서 이렇게 기본 생성자가 없는 경우 deserializeFromObjectUsingNonDefault 메서드로 분기처리가 된다.

해당 메서드에서 나는 따로 Deserialize를 만들지 않았기 때문에 첫 번째 분기문은 통과하고 두 번째 분기문으로 들어가게 된다.

 

deserializePropertyBased

/**
     * Method called to deserialize bean using "property-based creator":
     * this means that a non-default constructor or factory method is
     * called, and then possibly other setters. The trick is that
     * values for creator method need to be buffered, first; and
     * due to non-guaranteed ordering possibly some other properties
     * as well.
     */
이런식으로 설명이 나와있다.

 

해당 메서드는 설명에 나와있는 것처럼 기본 생성자가 아닌 생성자를 이용하여 역직렬화를 한다.

 

하지만 여기서 한 가지 의문이 생긴다.

 

나는 @JacksonCreator@JacksonProperty와 같은 어노테이션을 사용해서 사용할 생성자와 필드를 지정(위임)한 적이 없는데 왜 propertyBased 메소드가 호출되고 잘 작동 되었을까??

 

ParameterNamesModule

자동 위임 모듈 매개변수 이름을 사용하여 Java 개체를 직렬화 및 역직렬화하는 방법을 제공하는 Java 라이브러리이다.

 

모듈 gihub 링크: https://github.com/FasterXML/jackson-modules-java8/tree/master/parameter-names

 

즉 getter,setter 혹은 @JsonCreator, @JsonProperty 없이 매개변수 이름으로 직렬화, 역직렬화를 도와주는 라이브러리이다.

 

해당 모듈을 사용하려면

implementation 'com.fasterxml.jackson.module:jackson-module-parameter-names:2.10.3'

이렇게 의존성을 추가해주거나

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new ParameterNamesModule());

이런 식으로 objectMapper config에 넣어주기도 한다.

 

나의 경우에는 요즘 대부분이 사용하는 스프링 부트의 spring-boot-starter-web에는 이미 위의 의존성이 추가 되어서 따로 추가하지 않아도 알아서 적용이 되었던 것이다.

 

하지만 사용할 때 주의해야 할 점 몇가지가 있다.

 

ParameterNamesModule 주의점

  • 생성자의 인자가 하나일 경우 작동이 안된다.
    • 직접 @JsonCreator를 달아줘야 한다.
  • 기본 생성자가 없고 다른 생성자가 여러개 있다면 @JsonCreator를 직접 달아서 어떤 생성자를 위임할건지 정해줘야 한다.
  • Person class가 java 8 호환 컴파일러와 동작할 때는 program argument로 -parameters를 설정해야 한다.

등등이 있다.

 

 

결론


제일 안전하고 간단한 방법은 기본 생성자를 작성하는 것이라고 생각한다.

 

기본 생성자를 사용하기 싫다 하면 Property names를 통해 @JsonCreator, @JsonProperty를 사용하지 않아도 맵핑은 잘 되겠지만 개인적으로 시간 조금 더 들여서 다 작성해주어 예측 가능한 코드를 작성하는 것이 낫지 않을까 싶다.

 

이번 기회로 reqeusetBody가 어떻게 맵핑되고 jackson의 내부 동작과정을 어느정도 알게 되었다.

더 깊숙히 들어가면 끝도 없겠지만 일단 오늘은 이정도에서 마무리를 한다.