문제발생
//컨트롤러
@PostMapping("/apply")
public void apply(@RequestBody Long postId, HttpServletRequest request) {
...
}
//보낸 json 요청
{
"postId": 1
}
다음과 같은 환경에서 아래와 같은 오류가 발생했다.
resolved [org.springframework.http.converter.httpmessagenotreadableexception: json parse error: cannot deserialize value of type `java.lang.long` from object value (token `jsontoken.start_object`); nested exception is com.fasterxml.jackson.databind.exc.mismatchedinputexception
요청 HTTP body에 json 데이터를 Long 타입으로 역직렬화
하지 못했다는 오류이다.
직렬화:
디스크 저장이나 네트워크 전송을 위해 바이트 스트림으로 만든다.
역직렬화:
직렬화 된 데이터를 다시 객체의 형태로 만드는것
HTTP 메시지 컨버터가 처리를 하지 못했다는 건데, 위 오류의 정확한 원인을 찾기위해 HTTP 메시지 컨버터의 동작 원리를 다시 정리하려고 한다.
들어가기 전에 결론적으로 말하자면 HTTP 메시지 컨버터의 동작방식에 이해가 부족했다.
요청 HTTP 의 데이터를 역직렬화
할때 사용되는게 HTTP 메시지 컨버터이다. 어디서 어떻게 동작하는지 알아보자.
메시지 컨버터 동작 위치
Spring MVC 동작 구조에서 @RequestMapping을 처리하는 핸들러 어댑터(RequestMappingHandlerAdaptor) 부분을 자세히 살펴보자
RequestMappingHandlerAdaptor은 @RequestMapping 즉, 에너테이션 기반 컨트롤러를 처리할때, ArgumentResolver를 호출해서 처리한다.
이 ArgumentResolver는 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다.
스프링은 여러종류의 ArgumentResolver를 제공한다.
스프링 MVC는 @RequestBody 또는 @ResponseBody 가 있으면 RequestResponseBodyMethodProcessor (ArgumentResolver),
HttpEntity 가 있으면 HttpEntityMethodProcessor (ArgumentResolver)를 사용한다.
ReturnValueHandle 는 ArgumentResolver 와 비슷한데, 이것은 응답 값을 변환하고 처리한다.
스프링은 여러종류의 ReturnValueHandle를 제공한다.
HTTP 메시지 컨버터는 어디에 있을까?
이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.
ReturnValueHandle도 마찮가지
*이모저모*
스프링은 다음을 모두 인터페이스로 제공한다. 따라서 필요하면 언제든지 기능을 확장할 수 있다.
HandlerMethodArgumentResolver
HandlerMethodReturnValueHandler
HttpMessageConverter
메시지 컨버터 동작 원리
어디서 동작하는지 알아봤으니 어떻게 동작하는지도 알아보자.
[전체 플로우]
요청의 경우 @RequestBody 를 처리하는 ArgumentResolver 가 있고, HttpEntity 를 처리하는 ArgumentResolver 가 있다.
이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.
응답의 경우 @ResponseBody 와 HttpEntity 를 처리하는 ReturnValueHandler 가 있다.
이 ArgumentResolver 들이 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.
[디테일]
*HTTP 메시지 컨버터 인터페이스
//org.springframework.http.converter.HttpMessageConverter
package org.springframework.http.converter;
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) hrows IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage utputMessage) throws IOException, HttpMessageNotWritableException;
}
HTTP 메시지 컨버터 인터페이스를 살펴보면 이렇게 정의 되어있다
canRead() , canWrite() : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크
read() , write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능
중요한건 canRead() , canWrite() 에서 파라미터로 해당 클래스 데이터와 미디어 타입을 받아서 변환이 가능한지 검사한다는 것
*스프링이 구현해서 제공하는 HTTP 메시지 컨버터
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
스프링 부트는 다양한 메시지 컨버터를 제공하는데, 대상 클래스 타입과 미디어 타입 둘을 체크해서 사용여부를 결정한다. 만약 만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어간다.
ByteArrayHttpMessageConverter : byte[] 데이터를 처리한다.
클래스 타입: byte[] , 미디어타입: */* ,
요청 예) @RequestBody byte[] data
응답 예) @ResponseBody return byte[] 쓰기 미디어타입 application/octet-stream
StringHttpMessageConverter : String 문자로 데이터를 처리한다.
클래스 타입: String , 미디어타입: */*
요청 예) @RequestBody String data
응답 예) @ResponseBody return "ok" 쓰기 미디어타입 text/plain
//StringHttpMessageConverter
content-type: application/json
@RequestMapping
void hello(@RequetsBody String data) {} //ok!
MappingJackson2HttpMessageConverter : application/json 데이터를 처리한다.
클래스 타입: 객체 또는 HashMap , 미디어타입 application/json 관련
요청 예) @RequestBody HelloData data
응답 예) @ResponseBody return helloData 쓰기 미디어타입 application/json 관련
//MappingJackson2HttpMessageConverter
content-type: application/json
@RequestMapping
void hello(@RequetsBody HelloData data) {} //ok!
미디어 타입과 클래스 타입에 따라 알맞는 메시지 컨버터가 호출되어 변환된다.
문제해결
개요에서 다뤘던 문제의 원인을 이제 알수있다.
알맞은 메시지 컨버터가 없다!
//컨트롤러
@PostMapping("/apply")
public void apply(@RequestBody Long postId, HttpServletRequest request) {
...
}
//보낸 json 요청
{
"postId": 1
}
클래스 타입은 Long 미디어 타입은 application/json이다.
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
위 3개의 컨버터 중 어디에서도 부합하지 않는다. canRead()에서 false
그럼 해결은?
/**
현재 내 서버는 json 통신을 기본으로 하고있다.
아래 두가지 방법중 2번째 방법으로 해결
- MappingJackson2HttpMessageConverter를 사용하기 위해 postId 필드를 같는 클래스를 따로 정의한다. -> 비효율
- @RequestParam을 통해 url 쿼리 파라미터로 전달된 postId를 받는다. -> 효율
*/
//컨트롤러
@PostMapping("/apply")
public void apply(@RequestParam("postId") Long postId, HttpServletRequest request) {
...
}
//보낸 요청
localhst:8080/apply?postId=1 //쿼리 파라미터로 전달
단순히 입력 1개를 받는 작업에 굳이 Json을 고집해서 HTTP 메시지 컨버터를 사용하려고 할 필요 없다고 생각했다.
따라서 url 쿼리 파라미터로 해당 값을 받아오도록 처리했다.
'개발자 준비 > Spring' 카테고리의 다른 글
HTTP 메시지 컨버터 (0) | 2022.08.22 |
---|---|
스프링 AOP StackOverflowError (feat.스프링 AOP 동작원리) (0) | 2022.02.10 |
스프링 부트가 기본으로 제공하는 HandlerExceptionResolver (0) | 2022.01.24 |
ExceptionResolver를 이용한 스프링 부트 API 오류 처리 (0) | 2022.01.24 |
Spring에서 json 통신을 다루기 ResponseEntity, @ResponseBody (0) | 2022.01.17 |