본문 바로가기
스프링

[Spring] Jackson의 @JsonDeserialize가 항상 기본 생성자를 생성할까?

by itstime0809 2024. 7. 24.
728x90

Jackson

 jackson 라이브러리는 스프링 프레임워크가 기본적으로 제공해 주는 직렬화 및 역직렬화를 도와주는 라이브러리이다.

프로젝트 개발을 진행하면서, Redis에 적재된 JSON 타입의 값을 직렬화 및 역직렬화할 필요성이 있게 되었다. 그래서 Jackson 라이브러리를 활용하여 objectMapper를 활용해서 역직렬화 과정을 수행하던 중. 논리적으로 맞지 않은 것 같은 동작을 목격했다. 다른 분들의 활용 예시를 보면서, @JsonDeserializer가 기본 생성자를 생성한다는 글을 보았다. 그러나 이것은 반은 부족한 설명이라 느끼게 되어 위 제목에 대한 고찰을 시작해 보려고 한다.

 

@JsonDeserialize

 간단하게 어노테이션에 대해서 알고만 넘어가자면 이 어노테이션은 역직렬화 시, using = {ClassName}으로 설정한 역직렬화 객체를 사용 할 수 있게 해 준다. 그럼 먼저, 어떤 코드에 대해서 고찰을 시작하게 되었는지부터 살펴보자. 

 

고찰의 대상

 

 위 코드사진은 커스텀으로 정의한 RedisTemplateManager가 수행하는 동작이다. 해당 동작은 mallName을 통해 Redis에서 값을 직렬화 해서 가지고 오고, 이후 serialized 된 JSON은 POJO 타입으로 변환하는 역직렬화 작업을 수행한다.

 

그런데, 일반적인 경우 즉 @JsonDeserialize가 설정된 클래스가 추상 클래스가 아니라면 설정한 클래스를 역직렬화의 대상으로 간주하는데 현재 RedisProduct.class의 경우 아래와 같이 추상클래스로 구현되어 있다.

 

RedisProduct.class

 

 추상클래스 자체만으로는 인스턴스화 할 수 없다는 것은 알 수 있다. 그렇기 때문에 @JsonDeserialize 어노테이션에 using 키워드를 사용하여 실제로 직렬화의 구체적인 클래스를 지정해 주었다. 그러면 objectMapper.readValue() 해당 부분에서 @JsonDeserialize를 어떻게든 만나야 할 것이다. 우선 여기까지만 살펴보도록 하자. 고찰을 위한 전제를 설정해야 하므로, 직렬화 대상이 어떤 클래스인지만 파악하면 무리 없을것 같다. 다음 내가 생각한 고민 및 전제이다.

 

그럼 어떻게 객체를 생성할까?

 우선, 제목에 작성했듯, @JsonDeserialize가 기본 생성자를 생성한다고 하자. ( 실제로 디버깅해보기 전까진 이것이 항상 기본 생성자를 생성한다는 것만 알고 있었으므로 ) 그러면 RedisProductJsonDeserializer.class를 생성하게 될 것이니까 해당 클래스를 살펴보자.

 

RedisProductJsonDeserializer.class

 

 해당 클래스는 별 문제 없어 보인다. deserialize 메서드를 구현하여 실제로 역직렬화를 수행한다는 역할을 잘 수행하고 있다. 그런데 무언가 좀 이상하지 않은가. lombok 어노테이션의 @RequiredArgsConstructor가 설정되어 있고, 이를 통해 ObjectMapper가 주입을 받고 있는 상황이다. 

 

 뭐가 문제인가? 위에서 기본 생성자를 생성한다고 했고, 그렇다면 자바의 기본부터 생각해 보면, 자바 컴파일러는 명시적으로 기본 생성자 이외의 생성자를 정의하지 않는 이상 기본 생성자는 자바 컴파일러가 생성한다. 그런데 lombok 어노테이션으로 인해 명시적으로 final 키워드가 부착된 필드를 주입받을 수 있는 생성자가 생성되었다. 그럼 여기서 도대체 어떻게 @JsonDeserialize가 기본 생성자를 만든다고 하는 것일까?  분명히 기본 '@JsonDeserialize가 기본 생성자를 선택 한다고 하는데..' 익히 우리는 알고 있다. 명시된 생성자가 존재하는 경우, 기본 생성자로 객체를 인스턴스화하려고 할 때, 그렇게 할 수 없다는 사실을 알고 있다.

 

 그럼 잠깐 해당 고민은 내려두고, 이 클래스가 정상적으로 작동을 하게 될까? 그렇다. (...?) 어디서 부터 뭐가 문제인지 굉장히 혼란스럽지만 그럼 이렇게 해보면 어떻게 될까...

 

 기본 생성자가 없으면 수행이 안된다고 봤을 때, 기본 생성자를 위해서 @NoArgsConstructor를 사용한다고 해보자. 그럼 기본 생성자가 만들어졌으니 제대로 수행이 될 거라고 예상을 해볼 수 있다. 그러나 objectMapper를 주입받지 못하기 때문에 deserialize 과정을 수행할 수 없다. @NoArgsConstructor는 필드들을 기본적으로 타입에 맞게 초기화하게 되므로 objectMapper가 null로 초기화된다. 그러므로 역직렬화 과정을 수행할 수 없다.

 

@NoArgsConstructor를 사용해보자

 

 그렇다면 다른 방법은 없을까..ObjectMapper를 빈으로 수동으로 등록한다던지 하는 방법 등이 있을 것이다. 그러나 어떤 방법을 수행해도 objectMapper를 주입받기 매우 힘들었다. 그래서 수동으로 new ObjectMapper()를 주입받는 방법을 생각했지만 ObjectMapper는 Autoconfiguration에 의해서 JacksonAutoconfiguration이 미리 등록해 둔다. 그러므로 이렇게 주입받을 이유도, 이렇게 주입받는 게 올바른 방법이 아니라는 것도 알고 있으며 ObjectMapper를 생성하기에 들어가는 비용이 매우 비싸기 때문에 해당 방식으로 해결하려고 하지 않았다.

 

기본 생성자를 만들어 두면서... ObjectMapper까지 초기화를 그럼 어떻게 해야 되냐는 문제로 다시 바라보자. 이 것만 가능하다면 이제 기본 생성자가 있으면서(JsonDeserialize 가 기본 생성자를 생성한다고 했으므로), ObjectMapper도 초기화가 가능하면서 deserialize도 정상적으로 수행이 될 것이다. 

 

그렇게 고민하다. 문득, 기본 생성자를 생성한다고? 그럼 이 부분이 뭔가 문제가 있지 않을까 생각이 들었다. 그래서 나는 @JsonDeserialize가 기본 생성자만을 생성하지 않는다는 전제로 다시 생각해보았다. 그렇다면 기본 생성자만 생성하지 않기 때문에 @RequiredArgsConstructor를 사용해서 생성자를 만들어 볼 수 있지 않을까 생각했다. 그렇게 수행 후 실제로 해당 객체가 잘 동작되는 것이다. 이게 무슨 일인가.. 분명 기본 생성자가 없다고 해서 기본 생성자를 만들려는 생각만 했었지만 이렇듯 필드를 초기화할 수 있는 생성자 방법도 가능한 것이다. 즉 항상 JsonDeserialize가 기본 생성자만 생성하는 게 아니라는 것이다. 그렇다면 실제 readValue 시점부터 @JsonDeserialize 어노테이션이 구체 Deserializer를 선택해서 인스턴스를 생성하는 과정까지 어떻게 진행되는지 살펴보았다. 그것이 아래와 같은 결론을 도출할 수 있도록 인도했다.

 

@JsonDeserialize 어떻게 인스턴스를 생성하는거야?

readValue 부터 살펴보자.

 

  readValue() 메서드부터 살펴보자. readValue는 objectMapper 클래스로부터 수행되고, 실제로 내부 코드는 아래와 같이 진행된다.

 

readValue 내부 코드

 

  content는 redis에서 직렬화된 값이 들어있고 valueType에는 RedisProduct.class가 전달된다. 그럼 이제 내부 동작을 더 들어가 보자.

 

_readMapAndClose

 

 메서드를 타고 들어가다 보면 해당 메서드가 수행이 된다. 그러면 가장 먼저 Global  setting 되어 있는 Config 설정 파일로부터 설정을 DeserializationConfig 객체로 받아오고 DefaultDeserializationContext를 리턴한다. 중점적으로 살펴볼 부분은 더 들어가서이다.

 

 

 

 이 부분이 실제로 Deserializer 구체 타입을 정하는 부분이다. introspect에 의해서 BeanDescription을 얻어오고, findDeserializerFromAnnotation 메서드에 BeanDescription 객체의 getClassInfo()를 같이 넘겨준다. BeanDescription이 무엇을 담고 있길래 넘겨주는 걸까 보면, 클래스에 부착된 어노테이션의 값들이 있는 것을 확인할 수 있다. 그래서 findDeserializerFromAnnotation은 어노테이션으로부터 Deserializer를 찾는 메서드라고 할 수 있다. 실제로 deser가 반환받은 객체는 위에서 @JsonDeserialize에서 using 키워드로 설정한 구체 타입 클래스라는 것을 알 수 있다. 그러나 오해하면 안 되는 것은 객체가 초기화되어서 반환하는 게 아니라는 것이다.

 

JsonDeserialize 어노테이션 찾았다.

 

 

findDeserializerFromAnnotation 메서드가 수행하는 것은 JsonDeserializer의 instance를 얻는 부분이다. 실제로 여기서 JsonDeserializer가 어떻게 수행되는지를 알 수 있었다. deserializerInstance 메서드를 타고 들어가다 보면 또 아래 메서드를 수행한다.

 

찾았다.

 

ctor 파라미터에는 RedisProductJsonDeserializer Constructor 타입으로 넘어오게 되고, args가 가변 인자로 넘어오게 되는데,  아래 부분을 보다 보면 parameterCount를 얻는 모습을 볼 수 있다. ( 여기다! ) getParameterCount() 메서드로 넘어온 인자의 개수가 몇 개인지를 확인한다. parameterCount : 1이라고 되어 있는 것은 ObjectMapper라는 것을 알 수 있고 만약, 파라미터의 카운트가 0이라면 ctor.newInstance()로 인해서 기본 생성자를 생성한다는 것을 알 수 있고 만약 그렇지 않다면 argsWithDefaultValues [i]에 인수를 넘긴 다음, 해당 객체 배열을 가지고 newInstance()를 호출한다는 것을 알 수 있다. 

 

즉 파라미터의 개수를 instantiateClass 메서드에서 검사 해준 다음 파라미터의 값에 따라 생성자를 생성하는 방향이 달라진다는 것을 알 수 있으며 앞서 설정했던 @JsonDeserialize가 항상 기본 생성자를 생성하는 게 아니라는 것을 알 수 있었다. 그로 인해 @RequiredArgsConstructor가 정상적으로 수행된 이유 또한 설명이 가능해졌다. 메서드를 더 타고 들어가다 보면 NativeConstructorAccessorImpl의 newInstance를 호출하게 되고, 생성된 인스턴스를 재사용할 수 있도록 만드는 작업들이 포함된다. 또한 리플렉션에 의해서 수행이 되는 부분까지 포함해서 결론이 위와 같이 기본 생성자만 생성하지 않는다는 것으로 귀결될 수 있었다.