최근 회사에서 평소와 같이 업무를 보던 중 마케팅팀을 통해 특정 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 구조를 확인해보았다.
@Data
@AllArgsConstructor
public class Example {
private String name;
private String message;
}
위와 같이 @AllArgsConstructor
가 붙어있고, 확인해보니 객체 모두를 인자로 받는 생성자를 통해
deserialize를 수행하고 있었다.
회사에서는 기존에 lombok을 1.16.6 버전을 사용하고 있었으며, 어느날 1.18.12 로
버전이 업그레이드 된것을 Git 로그를 통해 확인할 수 있었는데 이 부분에서 문제가 발생한 것을
확인할 수 있었다. 정확히는 각 버전의 @AllArgsConstructor
에서 차이가 발생한 것이다.
각각의 버전에서 Example 클래스가 컴파일 된 내용을 비교해보자
public class Example {
... 코드 생략
private String name;
private String message;
@ConstructorProperties({"name", "message"})
public Example(String name, String message) {
this.name = name;
this.message = message;
}
}
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에 수행에 도움을 주던 @ConstructorProperties
가 1.16.6 이 후 버전에서는 제거되었다.
이로인해 Jackson은 기본 생성자도 없고, 모든 인자를 받는 생성자(파라미터명을 알 수 없는) 만
존재하다보니 적절한 생성자를 찾지 못해서 deserialize 를 제대로 수행하지 못하는 상황이 발생한 것이다.
@NoArgsConstructor
또는 직접 코드 작성을 통해 기본 생성자를 보장하여 해당 이슈를 방지할 수 있다.lombok.anyConstructor.addConstructorProperties=true
기존처럼 @ConstructorProperties
를 추가하라는 설정이다.다만 클래스를 만들 때 가변값과 불변값을 구분할 경우 불변값 만으로 이루어진 생성자를 만들고 해당 생성자에
@Builder
를 정의해서 사용하곤 하는데 이 때는 모든 인자를 받는 생성자 가
기본적으로 생성되지 않기 때문에 Lombok.config 설정이 의미가 없어진다고 한다.
이때는 결국 deserialize 시 기본생성자를 이용해야 한다. 더군다나 Lombok 에서
@ConstructorProperties
를 자동으로 생성하지 않도록 업데이트 한 이유가 있을 것이기 때문에
위와 같이 Lombok.config 를 통해 해당 설정을 커스텀 하는 것이 좋은 방법인지에 대한 의문점은
계속 붙어있다. 따라서 그냥 jackson deserialize 시 기본생성자 를 활용하는 방법을 더 추천한다.