개발

jackson objeckMapper List 타입 역직렬화(deserialize) 분석

동고킴 2023. 7. 16. 07:29
반응형

동일한 값을 반환하는 이름만 다른 getter는 어떻게 deserialize 될까?

 

자바에서 직렬화, 역직렬화 시, 많이(거의) 사용되는 jackson 라이브러리의 deseialize 처리에 관한 글이다. 예상하지 못한 결과가 발생해서 jackson 라이브러리 코드 구현부를 살펴보았다.

 

현상

 

위는 name, itemList 변수가 선언되어 있는 Store 클래스이다. 각 변수에 대한 getter는 lombok으로 정의되어 있다. 알 수 없는 프로퍼티를 무시하는 @JsonIgnoreProperties(ignoreUnknown = true) 어노테이션도 추가되어 있다. 그리고 name을 대문자로 반환하는 getUpperName, itemList를 반환하는 getDiscountedItemList가 정의되어 있다. getDiscountedItemList 함수는 itemList를 그대로 반환하는 getter 함수이다.

 

그리고 아래와 같은 json이 있다. 이 json을 Store 클래스로 역직렬화하면 어떻게 될까? 

{"name":"store1", "upperName":"STORE1", "itemList":["apple, banana"],"discountedItemList":["banana"]}

 

역직렬화 시, 필드 인식을 setter -> field -> getter 순으로 한다는건 배경지식으로 이미 알고 있었다. 하지만 getUpperName과 getDiscountedItemList는 @JsonIgnoreProperties(ignoreUnknown = true) 옵션으로 인해 자연스럽게 무시될 거라 생각했다. 그래서 결과는 아래처럼 나올 거라 생각했지만, 결과는 그게 아니었다.

ObjectMapperTest.Store(name=store1, itemList=[apple, banana])

 

실제 결과는 아래처럼 itemList에 banana가 2번 나왔다. itemList에 discountedItemList의 데이터가 추가된 것이다. 왜 이런 결과가 나온 것일까?

 

분석

jackson 라이브러리를 코드를 분석해 보자

 

1) @JsonIgnoreProperties(ignoreUnknown = true) 오해

먼저 JsonIgnoreProperties 어노테이션이 내가 생각한 것처럼 동작하지 않았다. ignoreUnknown = true 속성을 주면 json에 있는 upperName과 discountedItemList 모두 무시될 거라 생각했다. @JsonIgnoreProperties을 빼서 돌려보자

 

위 코드는 이전 코드에서 Store 클래스에 @JsonIgnoreProperties 옵션만 뺀 코드이다. 결과가 어떻게 나올까?

예상했던 대로 에러가 나온다. 그런데 에러에 이상한 메시지가 보인다. 3개의 알려진 프로퍼티에 discountedItmeList 항목이 보인다. json에서 upperName을 제외해서 다시 돌려보자.

 

 

@JsonIgnoreProperties이 없어도 discountedItem을 찾을 수 없다는 에러가 발생하지 않고, 결과도 처음과 동일하게 나온다. 왜 discountedItem은 @JsonIgnoreProperties가 없어도 에러가 발생하지 않을까?

 

1-1) addBeanProps

@JsonIgnoreProperties 속성은 BeanDeserializeFactory 클래스의 addBeanProps 함수에서 설정된다. 이 함수는 빈 역직렬화기가 사용하기 위한 설정가능한 속성을 파악하기 위한 함수이다. ignoreUnknown = true로 설정했다면 builder.setIgnoreUnknownProperties 함수에서 _ignoreAllUnknown을 true로 설정하고 나중에 이 속성으로 upperName 같은 알 수 없는 json 키를 예외처리한다.

 

이후에 BeanDeserializer 클래스에서  _beanProperties에서 해당 키가 있는지 체크하고, 없으면 handelUnkownProperty 함수를 호출한다. handelUnkownProperty 함수에서는 최종적으로 _ignoreAllUnknown 검사를 한번 더 수행하고, 설정되지 않았으면 에러를 발생시킨다.

 

 결과적으로 _beanProperties에 역직렬화하려는 json의 키가 있어야 한다는 이야기인데, _beanProperties에 upperName은 없지만 discountedItmeList은 있어서 에러가 발생하지 않는 것이다. _beanProperties은 json을 파싱하기 전에 이미 설정된다.

 

 

2) 왜 discountedItmeList은 있고, upperName은 없을까?

질문에 대한 해답은 타입이다. addBeanProps 함수에서는 ignore 설정 세팅 외에 properties 세팅도 이루어진다. 클래스에 선언된 필드(name, itemList) 외에도 getter 함수도 properties 대상으로 선정된다. getUpperName 함수와 getDiscountedItemList 함수도 properties 대상이다. get으로 시작하는 함수는 앞에 get을 제외한 뒤에 있는 텍스트가 properties 이름으로 추가된다. 예를 들어 getTitle이라는 함수는 title로 properties에 후보에 추가된다. 후보라고 한 이유는 모두 역직렬화 properties로 대상으로 추가되는 게 아니라 마지막 최종 필터링 절차가 남아있기 때문이다. 이 필터링 절차에서 upperName이 대상에서 걸러지게 되는데 이 부분 코드를 보자.

 

위 코드에서 propDefs에는 Store 클래스에 있던 필드, getter 등의 함수에서 가져온 properties 후보들이 있다. 이 값들을 순회하면서 최종적으로 propertie를 선별한다. 조건은 3가지이다. 1) setter가 있는지, 2) 필드값인지, 3) getter가 있는지.

upperName 함수와 discountedItemList는 3번째 조건인 getter에 해당된다. 하지만 getter라고 충족되는 건 아니고 조건이 하나 더 있다. Collection 타입이거나 Map 타입일 경우에만 최종적으로 deserialize 할 수 있는 propertie에 추가된다. (주석에 JACKSON-88에 따른다고 나와있는데, 이게 뭔지 찾지 못했다. 혹시 아시는 분은 댓글로 공유 부탁드립니다.)

 

아래는 최종적으로 조건에 맞는 prop을 _properties에 추가하는 코드이다.

 

3) 왜 discontedItemList 데이터가 itemList 데이터에 add가 되는 것일까?

itemList와 discountedItemList의 propertie의 타입이 다르다.

itemList propertie는 필드에 직접 할당하도록 설정된 특성을 가진 FieldProperty 타입인 반면에 discountedItemList propertie는 특성 값을 가져와서 직접 수정하여 간접적으로 Collection 또는 Map 특성을 구현하는 SetterlessProperty 타입이다. 실제 필드가 아닌 getter로 정의된 propertie는 직접 할당이 아닌 값을 가져와서 수정하는 식으로 역직렬화하는 것이다. 역직렬화 코드를 살펴보자.

FieldProperty 클래스와 SetterlessProperty 클래스는 모두 SettableBeanProperty 추상클래스를 상속하고 있고, deserializeAndSet 함수를 구현하게 되어있다. 각 구현체를 비교해 보자.

 

3-1) FieldProperty 클래스

createUsingDefault 함수를 호출해서 기본 객체를 생성한 후, set 함수를 통해 instance에 값을 넣는다. 여기서 instance는 물론 Store 객체이다.

 

3-2) SetterlessProperty 클래스

SetterlessProperty 클래스는 FieldProperty 클래스와 다르다. 주석에도 "need to fetch Collection/Map to modify라고 쓰여있듯이 getter를 통해 수정할 객체(toModify)를 가져온다. 그러면 toModify 객체에는 getDiscountedItemList 함수 안에서 itemList를 반환하기 때문에 이전에 이미 역직렬화가 수행된 itemList의 값인 ["apple", banana"]가 세팅된다.

이후에는 FieldProperty와 동일하게 객체에 값을 add 한다. 

 

정리

{"name":"store1", "upperName":"STORE1", "itemList":["apple, banana"],"discountedItemList":["banana"]}

위 json을 역직렬화 Store 클래스로 역직렬화했을 때 itmeList 값이 "apple", "banana", "banana" 이렇게 나오는 이유는 discountedItemList가 역직렬화될 때 이미 역직렬화된 itemList 값에 add 되었기 때문이다. List 타입 field 객체와 getter 함수 역직렬화 차이를 요약하면 아래와 같다.

  • field 객체는 역직렬화 시, FieldProperty 클래스를 사용한다. FieldProperty는 새로운 객체를 생성하고, json 값을 add 한다.
  • getter 함수는 역직렬화 시, SetterlessProperty 클래스를 사용한다. SetterlessProperty는 getter에 instance를 주입하여 객체를 생성하고, json 값을 add 한다.

 

마지막으로 여기에서 알 수 있는 건, 만약 json에 discountedItemList 값만 있고, itemList 값이 없으면 에러가 발생할 것이다. 이유는 SetterlessProperty에서 getter에 invoke하여 생성한 객체가 null일 것이고, null에 값을 add 할 수 없기 때문이다. 

반응형