Lombok 과 Jackson Deserialize 관계

shinhyocheol·2022년 7월 26일
1
post-thumbnail

이슈


최근 회사에서 평소와 같이 업무를 보던 중 마케팅팀을 통해 특정 API에 오류가 있음을 전달 받았고,
해당 이슈에 대한 내용을 정리하고자 한다. 우선 이슈는 아래와 같다.

  • 크롤링 기능이 포함된 API 가 있다.
  • API는 요청을 받으면 특정 웹 사이트의 데이터를 크롤링하고, 결과를 원하는 데이터 객체에
    맞게 변환하여 클라이언트에게 응답한다.

이 API 가 갑자기 제기능을 못한 것이다. 분명 이전에는 정상적으로 작동했었는데 말이다.
원인이 뭘까?? 좀 더 편리한 확인을 위해 로컬환경에서 같은 상황을 재현시켰더니 아래와 같이 로그를 출력했다.

Cannot construct instance of `com.example.admin.model.crawling.Example` 
(no Creators, like default construct, exist): cannot deserialize from Object value 
(no delegate- or property-based Creator)

JSON을 파싱한 결과를 전달할 적절한 생성자를 찾지 못했다는 내용의 메세지였다.
문제가 발생한 Example 구조를 확인해보았다.

Example.java

@Data
@AllArgsConstructor
public class Example {
    private String name;
    private String message;
}

위와 같이  @AllArgsConstructor 가 붙어있고, 확인해보니 객체 모두를 인자로 받는 생성자를 통해
deserialize를 수행하고 있었다.

이슈와 Lombok의 관계


회사에서는 기존에 lombok을 1.16.6 버전을 사용하고 있었으며, 어느날 1.18.12 로
버전이 업그레이드 된것을 Git 로그를 통해 확인할 수 있었는데 이 부분에서 문제가 발생한 것을
확인할 수 있었다. 정확히는 각 버전의 @AllArgsConstructor 에서 차이가 발생한 것이다.
각각의 버전에서 Example 클래스가 컴파일 된 내용을 비교해보자

1.16.6

public class Example {
	... 코드 생략
    
    private String name;
    private String message;
    
    @ConstructorProperties({"name", "message"})
    public Example(String name, String message) {
        this.name = name;
        this.message = message;
    }
}

1.18.12

public class Example {
	... 코드 생략
    
    private String name;
    private String message;
    
    public Example(String name, String message) {
        this.name = name;
        this.message = message;
    }
}

차이점이 보이는가? 모든 인자를 받아 생성자를 생성할 때 @ConstructorProperties 어노테이션의
존재 차이가 눈에 띈다.

우선 Java 진영에서 Json 타입으로 serialize/deserialize 를 수행할 때 대부분 Jackson 계열
라이브러리를 사용한다. 그 중 deserialize(Json → Java)의 경우 생성자를 활용하는데
기본 생성자 또는 모든 인자를 받는 생성자(파라미터명을 알 수 있는) 를 찾아서 기능을
수행하는 방식이 이에 해당된다. 

Example 클래스 같은 경우 Lombok 에서 제공하는 @AllArgsConstructor을 이용하였으며,
이는 모든 인자를 받는 생성자(파라미터명을 알 수 있는) 에 해당된다.

여기서 중요한 것은 ”파라미터명을 알 수 있는” 이라는 문구다. 파라미터명을 알 수 있다는 이야기는
Jackson 에서 동일한 이름의 Json 속성 값을 생성자 에게 넘겨줄 수 있다는 의미가 된다.
위의 Lombok 1.16.6 코드에서 이를 도와주는 것이 @ConstructorProperties 이다.

@ConstructorProperties

JDK 1.6 부터 제공되었던 어노테이션으로 생성자의 파라미터 이름을 지정하는 것을 도와준다.
이 어노테이션을 활용하면 파라미터의 이름을 Reflection API를 통해 알 수 있음

Jackson은 2.7버전부터 @ConstructorProperties 을 인지한다고 나와있다. (링크 참고)

https://github.com/fasterxml/jackson-databind/issues/905

deserialize에 수행에 도움을 주던 @ConstructorProperties1.16.6 이 후 버전에서는 제거되었다.
이로인해 Jackson기본 생성자도 없고, 모든 인자를 받는 생성자(파라미터명을 알 수 없는)
존재하다보니 적절한 생성자를 찾지 못해서 deserialize 를 제대로 수행하지 못하는 상황이 발생한 것이다.

해결 방법


  • 기본 생성자
    @NoArgsConstructor 또는 직접 코드 작성을 통해 기본 생성자를 보장하여 해당 이슈를 방지할 수 있다.
  • Lombok Config
    lombok.anyConstructor.addConstructorProperties=true 기존처럼 
    모든 인자를 받는 생성자 에 자동으로 @ConstructorProperties 를 추가하라는 설정이다.

다만 클래스를 만들 때 가변값과 불변값을 구분할 경우 불변값 만으로 이루어진 생성자를 만들고 해당 생성자에 @Builder 를 정의해서 사용하곤 하는데 이 때는 모든 인자를 받는 생성자 가
기본적으로 생성되지 않기 때문에 Lombok.config 설정이 의미가 없어진다고 한다.
이때는 결국 deserialize 시 기본생성자를 이용해야 한다. 더군다나 Lombok 에서 
@ConstructorProperties 를 자동으로 생성하지 않도록 업데이트 한 이유가 있을 것이기 때문에
위와 같이 Lombok.config 를 통해 해당 설정을 커스텀 하는 것이 좋은 방법인지에 대한 의문점은
계속 붙어있다. 따라서 그냥 jackson deserialize기본생성자 를 활용하는 방법을 더 추천한다.

profile
놀고싶다

0개의 댓글