본문 바로가기

Language & Framework/삽질기록

삽질 기록 (18) 또 너냐 Jackson.. JsonMappingException 해결하기

 

이번에도 여느때와 다름 없이 열심히 테스트 코드를 작성하고 있었다.

근데 터졌다.

 

Caused by: java.lang.NullPointerException: null
	at com.example.momobe.user.domain.Role.getRoles(Role.java:31) ~[main/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
	at com.fasterxml.jackson.databind.deser.impl.SetterlessProperty.deserializeAndSet(SetterlessProperty.java:121) ~[jackson-databind-2.13.4.2.jar:2.13.4.2]
	... 110 common frames omitted

 

언제나 그렇듯 제일 밑의 Caused by를 먼저 확인했는데 NullPointerException이 나오고 있다.

엥? 그럴리가 없는데?

레디스 테스트 코드 처음 작성해보는 것도 아니고 이게 무슨 일이래..

 

가능성이 없어보였지만 그래도 저렇게 얘기하길래 디버깅을 해봤고, 역시나 아니였다.

그래서 위로 올라가봤다.

 

 

com.fasterxml.jackson.databind.JsonMappingException: N/A
 at [Source: (String)"{"id":1,"nickname":{"nickname":"nickname"},"email":{"address":"user@test.com"},"role":{"roles":["ROLE_USER"]}}";
 line: 1, column: 96] (through reference chain: com.example.momobe.user.dto.RedisUserDto["role"]
 ->com.example.momobe.user.domain.Role["roles"])

 

또 너구나.. Jackson..

 

??

근데 읽어봐도 무슨 말인지 잘 모르겠다.

through reference chain: RedisUserDto -> Role..

직렬화 혹은 역직렬화 과정에서 Role을 변환 시에 문제가 있는 것 같다.

 

Source가 String인 걸 보니 아마 역직렬화에서 문제인 것 같기는 하지만 확실하게 하기 위해서 직렬화까지만 테스트해봤다.

 

 

 

잘 된다.

직렬화는 잘되는데 역직렬화가 안되는구나.. 이유가 뭘까?예외 메세지에 이유가 적혀있지 않을리가 없다. 다시 한 번 읽어보자.

 

 

 

처음에는 NullPointerException이라길래 객체만 확인하고 대충 넘어갔었는데

역직렬화가 문제라는 걸 알고보니 StackTrace 제일 아래의 문구가 눈에띈다.

 

"com.fasterxml.jackson.databind.deser.impl.SetterlessProperty.deserializeAndSet(SetterlessProperty.java:121)"

 

여기에 답이 있을 것 같다.

 

 

 

 

주석이 대화체로 작성되어 있어서 재미있다.

이런 건 처음 보네..

 

디버깅을 찍어보니 내 문제가 발생하는 곳은 _getter.invoke(어쩌고 저쩌고)이다.

never gets here에 도달하고 있는데 하필 저 부분만 별다른 설명이 없다.

 

그냥 불친절해서 설명이 없는 경우도 있지만, 대개 이렇게 많은 주석들 사이에 덩그러니 아무말이 없다는 건 설명이 필요 없다는 뜻이다.  그러면 _getter는 뭘까?

 

 

 

이녀석 리플렉션으로 나의 Getter를 찾고 있다.

원인은 바로 이것이였다.

 

 

 

Role 클래스는 기존에 N : M 매핑으로 유저에게 권한 처리를 하던 것이 쿼리 낭비라는 생각이 들어서, 필드에 문자열로 권한 처리를 하기 위해 만든 클래스다. (정규화 위반이 불편하다면 죄송합니다.. ㅎ 이번에만 이렇게 했어요..)

저장은 문자열로 하지만, 실질적으로 SpringSecurity에서 Authentication 객체에게 Role을 넘길 때는 List로 넘겨야하기 때문에 어차피 JwtToken 만들 때 말고는 사용하지 않는 게 Role 객체라서 애초에 List 형태로 반환하고 있다.

 

아마 Getter를 이용하는 원리라면 저걸 굳이 getRoleNames로 바꿔도 안되겠지만, 그래도 궁금해서 해봤다.

 

com.fasterxml.jackson.databind.exc.MismatchedInputException: 
Cannot deserialize value of type `java.lang.String` from Array value (token `JsonToken.START_ARRAY`)

 

끄덕끄덕.. 직렬화할 때는 String으로 직렬화해놓고 왜 역직렬화는 Array로 받냐고 따진다.

자기 마음대로 내 Getter 도둑질해서 써놓고 ㅎㅎ 웃기는 녀석이군

 

이제 어떻게 해결해야할까?

Jackson 때문에 불필요한 Getter를 만드는 것은 참을 수 없는 일이다.

 

https://www.baeldung.com/jackson-jsonmappingexception

 

 

역시 Baeldung 최고~~

Getter가 없으면 필드를 찾지 못하기 때문에 Getter가 없는 필드도 Desirialize하기 원한다면 fieldVisibility를 설정해줘야 한다고 한다.

 

 

붙이자마자 바로 성공했다.

 

성공했다고 바로 넘어가면 고생한 게 아까울만큼 쉽게 잊을테니 뭐하는 친구인지 주석이라도 읽어보자.

 

오토-디텍션으로부터 감지될 메서드의 종류와 최소한의 접근 단계를 지정할 수 있는 어노테이션입니다.
오토-디텍션은 데이터 바인딩을 위해 사용할 메서드를 찾기 위해 네임 컨벤션을 사용하고, 시그니처 템플릿을 찾는 것을 의미합니다.
예를 들어 Getter는 오토-디텍디드 될 수 있습니다. public method이고, 값을 반환하며 인자를 가지지 않고 전치사로 "get"을 가지는 것으로.
모든 접근자의 기본 설정은 JsonAutoDetected.Visibility.DEFAULT이다. 이것은 순서대로 의미한다. 그것이 글로벌로 기본 사용된다는 것을 의미합니다.
기본 설정은 액세서 타입마다 다릅니다.(게터는 필요로 한다 퍼블릭을, 세터는 어떤 접근 제어자든 가질 수 있다)
만약 네가 다른 JsonAutoDetect.Visivility 타입을 할당한다면. 이것은 글로벌 설정을 오버라이드할 것입니다.
예를 들어 모든 세터가 반드시 퍼블릭이여야할 것을 필요로 한다면. 
그러면 넌 @JsonAuthDetect(setterVisibility=Visibility.Public_ONLY)라고 쓸 수 있습니다.

 

그렇다고 한다.

더 들어가서 찾아봤는데..

 

 

POJOPropertiesCollector의 _addFields라는 메서드에서 해당 어노테이션이 있는지 찾는다.

이것 외에도 getter가 있는지, setter가 있느니 등등도 찾고 있는데 복잡하고 어질어질해서 오늘은 볼만큼 본 것 같아 그냥 나왔다.

궁금하면 찾아보삼

 

나중에 더 들여다볼 볼 날이 있겠지.. 벌써 새벽 3시 반이라 오늘은 여기까지다.