Jackson 이 객체를 만드는 법 (+ InvalidDefinitionException)

잼구·2023년 10월 16일
3

여러분은 처음 코딩 공부를 할때 어떻게 시작을 하셨나요?
저는 node.js 를 공부할때도 그랬었고, 무언가를 시작할때 강의나 책을 보며 공부를 하는 스타일이 아닙니다.
기본적인건 docs 에서 읽고 어느정도 번듯하게(?) 만들어진 프로젝트를 가져와서 코드리딩+ 복사붙여넣기 하며 뭔가를 만들며 배우는 타입이죠... 코딩 커비 타입 입니다.

이렇게 코딩을 하다보면 결과물이 빨리나와 기분은 좋지만 치명적인 구멍이 종종 생기곤 합니다. 저는 jasckson 의 직렬/역직렬화 메커니즘을 모르고 사용을 했었는데, 오늘은 거기에서 겪였던 오류를 소개하고자 합니다.

Dto의 생성자 차이

@Data
@NoArgsConstructor
public class ChatRequest{
    private Integer senderId;
    private String content;
    private String chatType;
}
@Data
@RequiredArgsConstructor
public class MenuOrderRequest{
    public final Integer ordererId;
    public final String menuName;
    public final String menuType;
}

해당 Dto 는 동일하게 메뉴를 주문할때 보내는 요청입니다.
다른 점이라면 1번은 기본 생성자가 있고, 2번은 전체 필드를 받는 생성자가 있다는 점 입니다.
보통 Dto 는 엄격하게 관리되는 Entity 와 다르게 큰 제한을 두지 않고 @Data , 기본 생성자 퍼블릭, @RequiredArgsConstructor 등등 을 쓰는 편입니다.

저는 평소 1번 방법으로 사용하는데 어느날 같이 협업을 하는 동료가 2번 방식으로 pr 을 올려서 "뭔가 저렇게 해도 되나보군..." 하고 머지를 했는데! 제 로컬에서 pull 하니 오류가 나는 사태가 났습니다...

InvalidDefinitionException

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `너의Dto` 
(no Creators, like default constructor, exist): 
cannot deserialize from Object value (no delegate- or property-based Creator)
 at `들어온 데이터`

들어온 데이터를 Dto 객체로 역직렬화 할 수 없다는 오류가 납니다.
분명 동료의 로컬에서는 굉장히 잘 돌아갔다는데...?

우선 기본 생성자가 없는 문제 인 것 같으니, 기본 생성자를 넣어 오류를 해결하였습니다.
그런데 동료 컴에서는 잘 되고 제 컴에는 안되는 마법같은 일이 너무 찝찝해서 jakson 의 원리를 알아보았습니다. (지금까지 모르고 써서 죄송합니다...)


Jackson 이 [직렬화+역직렬화]를 할때

우선 spring 에서 @RequestBody 로 들어온 데이터 형변환은 HttpMessageConverter가 하게 되고 그중 해당 컨텐트 타입이 json 형태이면 MappingJackson2HttpMessageConverter 가 형변환을 하게 됩니다. (우리가 사용하는 read() 함수는 AbstractJackson2HttpMessageConverter 에 있음)
해당 컨버터는 ObjectMapper 를 사용해서 데이터를 읽고, 쓰게 됩니다.
그렇기 때문에 밑에서는 ObjectMapper 의 직렬/역직렬화 메커니즘에 대해 설명하겠습니다.

1. 기본생성자 + getter + setter 조합

@Data
@NoArgsConstructor
public class ChatRequest{
    private Integer senderId;
    private String content;
    private String chatType;
}

1번의 경우 기본생성자 + getter + setter 의 조합으로 구성 되어 있습니다.
jackson 은 기본적으로 "기본 생성자" 를 통해 객체를 생성하는 방식으로 역직렬화 합니다.

데이터 바인딩
이후 JSON 필드의 이름을 Java 객체의 getter 및 setter 메소드와 일치시켜 JSON 객체의 필드를 Java 객체의 필드에 매핑합니다. Jackson은 getter 및 setter 메서드 이름의 "get" 및 "set" 부분을 제거하고 나머지 이름의 첫 번째 문자를 소문자로 변환하여 사용합니다.
ex)
Object { public String getName() { return this.name } } -> getName()을 인식 get 을 제거 한 후 Name 의 첫문자를 소문자로 변경해 key 값 사용 -> json {"name": "mimi"}

즉, 직접 필드에 접근하는 것이 아닌 getter 및 setter 메소드를 통해 필드 값을 유추해 매핑을 진행한다는 것 을 알 수 있습니다.

🤔 하지만 이때 setter 가 없더라도 역직렬화가 잘 되는 기묘한 일이 펼쳐집니다.

기본 생성자를 통해 인스턴스를 생성하고 setter 을 통해 필드값을 주입해 주는 줄 알았는데, setter 가 없어도 역직렬화가 잘 이루어집니다. 그렇다면 jackson 은 어떤식으로 데이터를 주입해 주는 걸까요?

setter 없이도 데이터 주입을 할 수 있는 이유

간단히 말해 reflection 을 사용해 값을 주입하기 때문에 setter 는 애초에 필드 값을 인식하는 곳에만 사용한다는 것 입니다. 즉, getter 가 있다면 setter 는 굳이 존재하지 않아도 됩니다.

Tip.
그리고 reflection 을 사용하면 기본 생성자 접근 제한자가 protected 이상이기만 하면 되기때문에 타이트한 관리를 위해 접근제한자를 protected 로 바꾸어도 좋습니다
굉장히 자세한 원리가 적힌 블로그

결론

해당 조합에서는 밑 방식으로 직렬/역직렬화를 하는 것을 알 수 있습니다.

  • 역직렬화
    1. 기본생성자로 인스턴스 생성
    2. getter 및 setter 를 통해 필드 인식
    3. 인식한 필드를 리플렉션을 통해 주입
  • 직렬화
    1. getter 및 setter 를 통해 필드 인식
    2. getter 을 통해 얻은 값으로 json 객체 생성

2. 전체 필드 생성자 + getter + setter 조합

@Data
@RequiredArgsConstructor
public class MenuOrderRequest{
    public final Integer ordererId;
    public final String menuName;
    public final String menuType;
}

위에서 jackson 은 기본적으로 "기본 생성자" 를 통해 객체를 생성하는 방식으로 역직렬화를 한다고 했습니다. 그럼 만약 기본생성자가 없다면 어떻게 되는 걸까요? (물론 저는 오류가 났습니다)

해당 사진은 기본 생성자가 없을때 호출 되는 함수입니다.
기본 생성자가 없다면 jackson 은 delegateSerializer 를 사용합니다. (제공된 property 사용 한다는 뜻)

delegate? 일반적으로 한 객체 또는 메서드가 일부 책임 또는 작업을 다른 객체 또는 메서드에게 위임하는 것을 의미.

public class Book {
    private String title;
    private String author;
   

    @JsonCreator
    public Book(@JsonProperty("name") String title, @JsonProperty("writer") String author) {
        this.title = title;
        this.author = author;
    }

   // getters and setters
}

위에서 우리는 delegate를 설정한 적도 없고, inner class 도 아니니 맨 마지막 오류 메세지가 떴던 겁니다.

그럼 동료 컴에서는 왜 잘 된걸까요???

스프링 부트 2.x 를 이후부터는 ObjectMapper 에 jackson-module-parameter-names 가 자동으로 등록 되어 있는 걸 볼 수 있습니다.
jackson-module-parameter-names는 기본 생성자 없이도 인식한 필드가 들어갈만한 "파라미터가 있는 생성자" 가 있으면 해당 생성자로 역직렬화를 바로 진행 합니다. 우리가 @JsonCreator 로 delegate를 설정한 것 처럼 말입니다.


내용을 요약하자면, Jackson Modules: Java 8 부터는 JsonProperty 없이도 알아서 바인딩 잘 해준다는 뜻입니다.
대신 조건이 있음 (이게 진짜 개중요;;; 모르면 삽질 합니다)

  • 표시되는 생성자가 여러 개 있고 기본 생성자가 없는 경우 @JsonCreator 역직렬화를 위한 생성자를 선택해야 합니다.
  • jackson-databind보다 낮은 값 과 함께 사용하는 경우 가 필요합니다 2.6.0. @JsonCreator실제로는 에서 해결될 생성자 검색 문제로 인해 @JsonCreator종종 가 필요합니다 .2.6.0+2.7
  • Person 클래스에 단일 인수 생성자가 있는 경우 해당 인수에 주석을 달아야 합니다 @JsonProperty("propertyName"). 이는 레거시 동작을 보존하기 위한 것입니다.

파라미터가 있는 생성자가 여러개거나, 파라미터가 1개인 생성자면 여전히 어노테이션을 명시해 줘야한다는 것입니다.
해당 속성은 jackson-module-parameter-names 으로 자동 매핑할때 쓰는 속성인데 저기 단일 인자면 작동 안된다고 써있답니다. jackson3에서 고쳐질 수도 있다는 소문이... 소문의 근원

그럼 동료 컴에서 되면 님 컴에서도 돼야 하는거 아님?

맞습니다... 사실 되어야 합니다!!!!!
저걸 다 알아보고도 안돼서 기묘해서 아예 레포를 싹 지우고 다시 클론하니 되었습니다...
저는 뭔가 내부적으로 모듈이 꼬인건지 그레들을 새로 로딩 하니 되더라구요...ㅠㅠ 하 내 시간...
만약 안되시는 분은 삭깔 한번 해보세요...!

결론

해당 조합에서는 밑 방식으로 직렬/역직렬화를 하는 것을 알 수 있습니다.

  • 역직렬화
    1. getter 및 setter 를 통해 필드 인식
    2. jackson-module-parameter-names 가 찾은 파라미터가 있는 생성자에 필드를 넣고 생성
  • 직렬화
    1. getter 및 setter 를 통해 필드 인식
    2. getter 을 통해 얻은 값으로 json 객체 생성

정리

  • jackson 은 기본적으로 역직렬화 시 기본 생성자를 필요로 한다.
  • 스프링 상에서는 jackson-module-parameter-names 모듈 덕에 기본생성자 없이 파라미터가 있는 생성자로 역직렬화가 가능하다.
  • 하지만 위 방식은 파라미터가 1개인 경우는 동작하지 않는 한계가 있으므로 웬만하면 1번으로 하자.
  • 1번을 할때는 getter 로도 충분하니 setter 는 굳이 쓰지 말자

ref
파라미터가 1개인 경우 자동 매핑 실패 스택오버플로우

https://beaniejoy.tistory.com/76

https://velog.io/@conatuseus/RequestBody에-왜-기본-생정자는-필요하고-Setter는-필요-없을까-2-ejk5siejhh

https://velog.io/@conatuseus/RequestBody에-기본-생성자는-왜-필요한가

https://jenkov.com/tutorials/java-json/jackson-objectmapper.html#jackson-databind

https://fasterxml.github.io/jackson-annotations/javadoc/2.9/com/fasterxml/jackson/annotation/JsonCreator.Mode.html#PROPERTIES

https://github.com/FasterXML/jackson-modules-java8/tree/master/parameter-names

https://bbbicb.tistory.com/46#2.%20%EA%B8%B0%EB%B3%B8%EC%83%9D%EC%84%B1%EC%9E%90%20%EC%97%86%EC%9D%B4%20%EC%96%B4%EB%96%BB%EA%B2%8C%20%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94%ED%95%A0%EA%B9%8C%3F-1

https://sedangdang.tistory.com/307

profile
잼구입니다

2개의 댓글

comment-user-thumbnail
2023년 10월 16일

포스팅 잘 읽었습니다...!
제가 알기론 @Data에 @RequiredArgsConstructor가 들어가있는걸로 아는데
그러면 되도록 1번처럼 @Data + @NoArgsConstructor도 필수로 다는게 좋을까요?

1개의 답글