문제 상황 )
spring boot를 통해서 간단한 CRUD기능을 하는 API를 구축하고, 이를 테스트해보기 위해서
Postman을 통해 간단한 POST 요청을 보냈는데 위와 같이 에러가 떴다.
검색을 해보면 문제의 원인이 되는 부분이 다양하게 나와있기에
일단 먼저 내 코드의 어떤 부분이 문제가 되는지 로그의 stackTrace를 통해서 파악해 보았다.
문제점의 원인 파악)
1. StackTrace를 통한 메소드 흐름 확인
일단 첫 번째 줄에서 AbstractMessageConverterMethodProcessor 클래스의
writeWithMessageConverters 메서드를 보자.
공식 document를 찾아보니 해당 클래스는 다음과 같이 요약되어 있었다.
Extends AbstractMessageConverterMethodArgumentResolver with the ability to handle method return values by writing to the response with HttpMessageConverters.
" HttpMessageConverters로 응답하기 위한 return 값을 작성하는 method를 다루는 AbstractMessageConverterMethodArgumentResolver를 확장한 클래스"
해당하는 문제점이 HttpMessageConverter를 사용해 "작성"할 때 생기는 문제로 볼 수 있겠다.
즉, 직렬화 or 역직렬화 과정에서 문제가 생기는 것이다.
그 밑의 두 줄에 이를 확실히 하고 있다.
HttpEntityMethodProcessor.handleReturnValue
HandlerMethodReturnValueHandlerComposite.handleReturnValue
아래의 HandlerMethodReturnValueHandlerComposite 는
handleReturnValue와 함께 composite한 클래스일 것이다.
다시 말해 handleReturnValue가 직접적인 원인이 될 것임을 확인 할 수 있다.
그 밑의 줄에
ServletInvocableHandlerMethod.invokeAndHandle
RequestMappingHandlerAdapter.invokeHandlerMethod
RequestMappingHandlerAdapter.handleInternal
이 있는 것을 보아 RequestMapping에서 invoke, handle과정에서 일어난 문제점으로 추정된다.
DispatcherServlet은 HandlerMapping을 통해 요청에 대한 응답을 처리할
메서드들을 선택하고, 이를 HandlerAdapter를 통해서 servlet container의 스레드풀에
해당 스레드에게 mapping한 메서드를 사용할 수 있도록 위임(delegate)하는데,
이 과정에서 해당 메서드를 단순히 부른다(call)라는 개념보다는
invoke라는 단어를 사용하는 것 같다.
요약하자면, HttpMessageConverter를 통한 직렬화, 역직렬화 과정에서 문제가 생겼음을 알 수 있다.
2. 세부 문제점 파악
AbstractMessageConverterMethodProcessor.writeWithMessageConverters 를 뜯어보자.
해당하는 구문은 determineCompatibleMediaTypes 이라는 메서드에 정의되어 있고,
이 메서드를 호출하면서 HttpMediaTypeNotAcceptableException을 throw하면서
그와 동시에 어떤 메시지도 없게끔 호출하는 구문은
List<MediaType> producibleTypes = this.getProducibleMediaTypes(request, valueType, (Type)targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
}
List<MediaType> compatibleMediaTypes = new ArrayList();
this.determineCompatibleMediaTypes(acceptableTypes, producibleTypes, compatibleMediaTypes);
if (compatibleMediaTypes.isEmpty() && ProblemDetail.class.isAssignableFrom(valueType)) {
this.determineCompatibleMediaTypes(this.problemMediaTypes, producibleTypes, compatibleMediaTypes);
}
if (compatibleMediaTypes.isEmpty()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
return;
}
뿐이다.
여기서 body가 null이 아닌데 producibleTypes는 null혹은 아무것도 반환을 하지 않는 상태라는 것이다.
즉, producibleTypes에 문제가 있다는 추론을 할 수 있다.
producibleTypes를 정의하는 메서드는 getProducibleMediaTypes이고,
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
Set<MediaType> mediaTypes = (Set)request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList(mediaTypes);
} else {
Set<MediaType> result = new LinkedHashSet();
Iterator var6 = this.messageConverters.iterator();
while(true) {
while(var6.hasNext()) {
HttpMessageConverter<?> converter = (HttpMessageConverter)var6.next();
if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> ghmc = (GenericHttpMessageConverter)converter;
if (targetType != null) {
if (ghmc.canWrite(targetType, valueClass, (MediaType)null)) {
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
continue;
}
}
if (converter.canWrite(valueClass, (MediaType)null)) {
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}
return (List)(result.isEmpty() ? Collections.singletonList(MediaType.ALL) : new ArrayList(result));
}
}
}
이 메서드의 반환 후보들을 보면 null은 절대로 없다.
가능한 경우는 result가 비어있을 경우만이 존재한다.
빈 List를 반환했기에 exception 메세지의 뒤에 아무런 값도 나오지 않았음이 자명하다.
지금 request는 postman을 통해 제대로 Accept: "*/*"으로 모든 mediaType을 받을 수 있도록 보내지고 있고,
stacktrace를 통해서 handlermapping이 정상 작동하는 것을 볼 수 있으므로
문제점은 valueClass와 targetType이 될 것 같다.
그리고 응답 자체는 디버깅을 통해 Dto 객체가 정상적으로 생성은 되고 있음을 확인했다.
충분히 내가 원하는 객체를 생성하고, 해당 필드들을 저장하고 있었다.
그렇다면 messageConverter 가 valueClass를 제대로 처리하지 못했을 가능성이 높아보인다.
디버깅 결과 result가 0이 나왔으므로 producibleType은 "*/*"이 나왔다.
즉, mediaType과는 아무런 관련이 없다. 순수하게 valueType, targetType, Converter의 문제이다.
직접적인 원인이 되는 부분에서 converter.canwrite를 계속해서 실행하게 된다.
1. GenericHttpMessageConverter 의 모든 instance converter에 대해서 canWrite 를 통해
내가 응답, 반환에 사용할 class의 타입(valueType), targetType과 선택된 MediaType이
converter에서 올바르게 변환해서 write할 수 있는지를 확인하는 과정과
2. converter의 canWrite에서도 위와 동일한지를 확인하는 과정을 거친다.
그런데 어떠한 converter와도 맞지 않는다는 것은 직렬화 작업이 잘못되었다는 것을 의미한다.
그렇다면 Spring boot에서 직렬화 작업이 어떻게 진행되는지를 알아야 한다.
3. Jackson의 직렬화, 역직렬화 원리
... 는 다음에 설명하도록 하겠다.
핵심은 Jackson은 직렬화를 할 때, private 필드에 직접 접근하는 것이 아니라
getter메서드를 통해서 필드 값을 얻어 JSON으로 직렬화를 한다.
그렇기에 getter 메서드가 없다면 필드에 접근 할 수 없다.
그리고 이에 따라 JSON으로 변환할 수 없다.
확인해보니 responseDto 클래스에 getter가 정의 되어 있지 않았다.
lombok을 사용하고 있으므로 @Getter annotation을 달아주니
아래와 같이 정상적으로 작동하는 것 또한 확인했다.
참고로 ErrorDetail 등 Jackson을 통한 직렬화, 역직렬화를 하는
responsebody에 들어가는 모든 것들은 다 getter가 필요하다.
https://www.baeldung.com/jackson-object-mapper-tutorial
'Trouble Shooting > episode 3. Java, Spring' 카테고리의 다른 글
AOP : Dummy User 생성 + Id 기반 Entity 설정 (1) | 2024.12.01 |
---|---|
Tomcat의 일부 Thread가 놀고 있다?! (0) | 2024.11.25 |