메이쁘

[Spring][intellij] "No HttpMessageConverter.." 및 "NoClassDefFoundError.." RestTemplate 트러블슈팅 정리 본문

Technology/Web - Spring

[Spring][intellij] "No HttpMessageConverter.." 및 "NoClassDefFoundError.." RestTemplate 트러블슈팅 정리

메이쁘 2021. 12. 23. 00:42

안녕하세요?

 

개발 도중 발생했던 트러블 슈팅에 대해 

원인을 찾고자 끝까지 팠던 경험을 기록하고

 

왜 저 에러가 발생했는지,

어떻게 해결했는지 정리하려고 합니다.


0. 시작

  - RestTemplate Class를 활용해 API 통신하려고 postForEntity() 함수 호출 시 에러 발생

 

** RestTemplate?

Spring 3.0 부터 지원, 스프링이 제공하는 HTTP 통신에 유용하게 사용 할 수 있는 템플릿이며, HTTP 서버와의 통신을 단순화하고 RESTful 원칙을 지키고 있다.

RestTemplate는 Spring에서 제공하고 있는 JdbcTemplate같은 Template로, RESTful Service 호출과 응답에 관련된 여러 메소드를 제공하고, REST 클라이언트를 쉽게 개발할 수 있도록 만들어진 Template이다.

 

제가 사용하는 버전은 Spring 5.x 버전 대 이기 때문에

굳이 RestTemplate를 사용하지 않고 WebClient를 사용해도 되지만

 

추후 4버전 대에서도 활용할 수 있는 기능을 만들고 있어 

RestTemplate를 사용했었습니다.

 

RestTemplate는 Javadoc에서 추후 deprecated 될 예정이지만, 추후 버전에 해당되는 내용이고,

동기식으로 구현하기 떄문에 WebClient를 생각하지 않았습니다. (WebClient는 비동기식, 동기식 둘 다 가능하긴 함)

 

 

다시 돌아와서,

현재 발생한 상황은

 

 

RestTemplate + postForEntity()를 활용해 특정 URL에 객체 전송

 

입니다.

 

 

이 때,

"No HttpMessageConverter for ~" 에러 발생했습니다.

 

이를 디버깅하며 깊게 들어가보니

 

 

RestTemplate 클래스 안에 있는 doWithRequest() 함수를 호출했습니다.

이 때, ClientHttpRequest 객체를 꺼내 HttpHeader에 설정된 Content-Type을 확인해보니

 

934 라인을 보시면

사용할 수 있는 messageConverter 들을 가지고 해당 body 값을 적절하게 Converting 시켜주는데,

 

적절한 Converting이 이뤄지지 않아서? 라고 생각했습니다.

 

즉, 적절한 Content-Type을 찾지 못해서? 라고 생각했습니다.


1. Content-Type = 'application/json' 추가

RestTemplate에 Header를 넣기 위해 HttpHeader 객체 생성해서 HttpHeader안에 Content-type = ‘application/json’ 를 추가했습니다.

이후 RestTemplate 통신 시 Header를 넣어 Request Header 안에 Content-type이 포함되어 송신하게 만들었습니다.

하지만,

 

No HttpMessageConverter for ~E and content type "application/json”

에러가 발생했습니다.

즉, 이전 에러에서 and content type ~ 이 추가되었네요.

 

여러 방면으로 찾아본 결과,

body 값을 JsonObject로 바꿔주기 위해 MappingJackson2HttpMessageConverter 를 활용하는데,

이 Converter를 사용할 수 없다고 나왔습니다.

 

 

그래서,

RestTemplate에 넣을 body class 값을 사전에 ObjectMapper를 통해 Json으로 변경해서 넣으면 되지 않을까?

해서 테스트해봤습니다.

 

 

테스트 결과,

→ java.lang.ClassNotFoundException 발생(ObjectMapper)

→ Maven dependency 추가 / 라이브러리 import 확인했는데도 발생

음.. 왜지?

싶어서 JsonString으로 변경하기 위해 Gson 라이브러리를 활용해보자! 했지만..

 

→ Gson 라이브러리 활용해서 Object to Json String 으로 변경

→ Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError 발생(Gson)

 

갑자기 Class가 없다고 하네요..

분명 maven으로 땡겨왔는데??

 


2. Spring framework의 버전 문제인가?

혹시나 Spring framework 버전이 만들어진 날짜 이후에 만들어진 버전의 라이브러리들을 사용해서 발생하는 오류일 수도 있다 해서 

 

Mvn Repository 홈페이지에서 각 버전의 Date를 확인했습니다.

 

 

 

  → Spring framework 5.2.6 RELEASE : Sep, 2020

(https://mvnrepository.com/artifact/org.springframework/spring-core)

 

 

Sep, 2020 날짜 이전에 만들어진 버전으로 Gson의 버전을 변경했습니다.

  → Gson 라이브러리 버전 수정(2.8.6 : Oct, 2019)

 

 

 

하지만, 결과는 실패했습니다.

 

 

그래서 Spring boot 프로젝트를 만들어서 진행했더니, 정상 동작하더라구요..

 

 

다시 Spring framework로 돌아와서 하나하나 디버깅하면서 breakPoint를 찍어봤습니다.

 

 

이 시점에 Breakpoint를 찍은 다음

Evaluate를 통해 현재 사용하는 MessageConverter를 확인해보니, Content-Type이 application/json인 경우에 대한 Converter가 없습니다. (RestTemplate : spring-web: 5.2.9 RELEASE) 

  → 즉, 위에서 적은 MappingJackson2HttpMessageConverter 가 없어서 Json으로 변경하지 못해 발생하는 이슈같았습니다.

(application/octet-stream, text/plain, application/xml, application/x-www-form-urlencoded) 만 존재

 

반면, Spring boot에서는 application/json인 경우에 대한 GenericHttpMessageConverter가 존재해서 JSONObject로 body class converting이 가능했고, 이는 곧 위의 MappingJackson2HttpMessageConverter가 있었기에 가능했었습니다.(RestTemplate: spring-web: 5.3.12)

 

 

 

(Spring Boot인 경우)

→ (Spring boot) RestTemplate.doWithRequest() 호출 시 allSupportedMediaTypes에 application/json 포함되어있음.

 

→ MessageConverter 안에 이미 MappingJackson2HttpMessageConverter 객체가 존재하고, 이덕분에 JSONObject로 변환이 가능했습니다.

 

 

즉,

Spring boot 버전 변경(2.3.4.RELEASE = spring-core 5.2.9.RELEASE) 으로 Spring framework의 버전과 일치시킨 spring-web으로 RestTemplate 테스트를 진행했고, 버전 차이로 인한 이슈는 아니었습니다.

 


3. RestTemplate의 MessageConverters 전역변수 List에 담기는 생성자 함수 추적

→ Spring boot는 jackson2Present boolean 값이 true고, 그렇기 때문에 MappingJackson2HttpMessageConverter 객체를 만들어 집어넣는다.

반면,

Spring framework는 jackson2Present boolean 값이 false였기 때문에, 위 객체가 존재하지 않았던 것.

 

 

그래서 jackson2Present boolean 변수가 어디에서 설정되는지 확인해봤습니다.

 

RestTemplate class의 static에서 정의되는데, Bean 생성 시 초기 1번에 설정되어집니다.

  → ClassLoader를 통해 해당 클래스가 import되서 존재하는지 체크한 후, 있으면 boolean 값을 변경합니다.

  → 그럼, 돌고돌아 jackson.databind.ObjectMapper, jackson.core class가 import 되지 않은것인지부터 다시 확인.

 

 

ObjectMapper와 jackson 라이브러리를 maven을 통해 import해오고, 이를 실제 배포할 때 배포 파일 안에 반영이 되야 한다는 것인데, 여기서 문제가 발생한 것으로 추정했습니다.

 

 

이는 곧

 

→ ClassUtils.isPresent() 함수를 끝까지 타고 가니 위 함수에서 name.length() ≤ 7 때문에 null로 리턴하고,

→ 여기까지와서 Class.forName()으로 true를 리턴하는 것이었습니다.

위 284 Line에서 breakpoint를 찍은 다음, 

ClassLoader 객체를 확인했습니다.

 

그 결과

 

→ ClassLoader 안에 담겨있는 Class들 중 "com" 단어가 포함된 이름의 클래스는 위 스크린샷이 전부였습니다.

(com.fasterxml.jackson.databind 라이브러리가 있어야 하고, ObjectMapper와 JsonGenerator class가 있어야 함)

 

 

반면, Spring Boot에서는

Jackson 라이브러리를 포함하고 있었습니다. (사이즈가 7782개인데, boot 특성 상 기본적인 라이브러리를 내장하고 있어서 많습니다.)

 

 

4. 해결


결론은

 

"Maven dependency 설정으로 Maven에서 jackson 라이브러리를 정상적으로 땡겨왔고, 코드 상에서도 에러가 발생하지 않았지만, 실행 시 라이브러리가 존재하지 않아 MessageConverter가 json 형식으로 converting을 하지 못했다." 

 

 

그렇습니다.

 

배포할 때 라이브러리가 배포 파일(war 파일) 에 들어가지 않아서 런타임 시 라이브러리를 활용하지 못하는 것인가? 에 다다랐고,

 

File → Project Structure 에서

Project Settings → Artifacts 탭을 들어간 다음,

 

배포 시 담기는 라이브러리를 안에 집어넣었습니다..

 

오른쪽에서 왼쪽으로 옮겨놔야 배포할 때 실제로 라이브러리가 담기더라구요..

 

Intellij는 어려웠습니다...

 

 

 

긴 글 봐주셔서 감사합니다.

 

뜻깊은

집요한

트러블슈팅 경험이었어서 좋았습니다.

 

 

 

감사합니다.

 

 

 

Comments