JSON 데이터를 JAVA 환경에서 사용하는 방법이다.
HTTP Body 영역의 데이터를 InputStream 객체로 이진데이터를 가지고 온 후, 인코딩하고 문자열로 변환하면 JAVA 환경에서 사용가능한 데이터 타입이 된다. 이제 ObjectMapper 객체로 문자열로 표현된 JSON 데이터를 JAVA 객체로 생성하면 된다. 이처럼 HTTP Body 영역의 있는 데이터를 가져오려면 복잡한 과정을 거친다. 반면, GET이나 POST 같은 방식은 Request 객체에서 getParameter 메소드를 제공하기 때문에 JAVA환경에서 바로 사용 가능하지만 그 외 데이터는 HTTP Body 영역에서 직접 가져와야 한다. 응답 또한 마찬가지이다. 응답에 필요한 데이터를 Body 영역에 직접 넣어주어야 한다.
이런 작업은 모든 요청과 응답에서 공통으로 일어나는 과정으로 Spring이 담당하여 처리한다. 개발자는 그저 Controller에서 Body 영역의 데이터를 요청하거나 Body 영역으로 데이터를 반환하면 된다.
지난 포스팅에서 Controller에서 Request Body 영역의 데이터를 요청하거나 응답하는 방법을 다루어 보았다.
@RequestBody를 선언하거나 RequestEntity(HttpEntity) 타입으로 메소드의 파라미터를 정의하면, Spring은 HTTP Body 영역의 데이터를 읽어와 변환 후 파라미터로 넘긴다.
응답도 마찬가지이다.
@ResponseBody를 선언하거나 ResponseEntity(HttpEntity) 타입으로 데이터가 반환되면, Spring은 해당 타입의 데이터를 적절히 변환하여 HTTP Body 영역으로 전달한다. Spring에서 위 과정을 담당하는 모듈이 HTTP 메시지 컨버터이다.
이번 포스팅에서는 HTTP 메시지 컨버터의 동작원리를 다루어 보겠다.
HTTP 메시지 컨버터 동작원리
1. RequestHandlerMappingAdapter
요청이 들어오면 DispatcherServlet은 요청과 매핑되는 Controller(Handler)를 가져온다. ( Controller는 Spring Bean으로 스프링 컨테이너에 저장되어 있다. ) 그러나 DispatcherServlet은 Controller의 메소드를 직접 실행하지 않는다. Controller는 다양한 유형이 존재하므로 직접 의존하면 다형성을 보장할 수 없기 때문이다. 그래서 중간에 HandlerAdapter를 두어 Controller 메소드 실행 권한을 위임한다.
HandlerAdapter에는 대표적으로 RequestHandlerMappingAdapter가 있다. 한글로 요청 매핑 핸들러 어댑터라고 부른다. 이 핸들러어댑터는 @RequestMapping, @Controller로 선언된 Controller 메소드 실행권한을 가지고 있다. @RequestMapping로 선언된 Controller의 특징은 다양한 메소드를 가진다는 점이다. Controller는 비즈니스 로직을 처리하는 책임을 갖는다. 비즈니스 로직 처리에 필요한 데이터를 HandlerAdapter에게 요구하면 HandlerApdater는 요청한 데이터를 Controller에게 넘겨준다.
Controller는 필요한 데이터를 파라미터로 요구한다. @RequestBody, @ResponseBody, @ModelAttribute, @RequestParam 같은 어노테이션을 포함하여, HttpEntity 같은 클래스 타입도 요구에 포함된다. HandlerAdapter는 메소드의 파라미터를 읽고 이에 맞는 Request 객체에서 추출하고 변환하여 파라미터로 넘겨준다. 응답 또한 마찬가지이다. 메소드의 반환타입을 읽고 이에 맞는 변환로직을 거친다.
2. ArgumentResolver
Controller의 다양한 파라미터를 모두 지원할 수 있는 이유는 ArgumentResolver가 있기 때문이다.
- HandlerMethodArgumentReslover
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
ArgumentResolver의 대표적인 인터페이스가 HandlerMethodArgumentReslover이다. 인터페이스는 2개의 메소드를 가진다.
1. supportParameter 메소드
2. resolveArgument 메소드
supportParameter 메소드는 반환타입이 boolean이다. 해당 파라미터의 데이터를 생성해서 넘겨줄수 있는지 여부를 점검하는 메소드이다. supportParameter 메소드에서 true가 떨어져야 resolveArgument 메소드가 동작한다. resloveArgument 메소드는 파라미터로 넘겨질 데이터를 생성하여 Object 타입으로 반환한다.
- RequestParamMapMethodArgumentResolver
public class RequestParamMapMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); // @RequestParam 파라미터에서 어노테이션 가져오기
return (requestParam != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
!StringUtils.hasText(requestParam.name())); // @RequestParam 어노테이션인지 확인하기
}
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// [ 중략 ... ]
}
}
RequestParamMapMethodArgumentResolver는 @RequestParam 어노테이션이 붙은 파라미터를 지원하는 ArgumentResolver이다. supportsParameter 메소드 로직을 보면 파라미터에서 RequestParam 어노테이션을 가져 온 다음 진위여부를 검사하는 것을 확인할 수 있다. @RequestParam이 확인되어 파라미터로 넘길 데이터 생성이 가능하다고 여겨지면 resolveArgument 메소드가 실행된다.
위 사진은 ArgumentResolver 인터페이스의 구현체 목록이다. 이처럼 ArgumentResolver 인터페이스를 지원하는 다양한 구현체가 존재하기에 ,Controller의 다양한 파라미터를 지원할 수 있게 된다.
3. HTTP 메시지 컨버터
ArgumentResolver에서 파라미터에 명시된 데이터를 반환한다는 사실을 알게 되었다. 파라미터가 단순하게 Request 객체에 있는 데이터를 요구하면 Request에서 추출하여 전달하면 된다. 그러나 @RequestBody, HttpEntity 혹은 특정한 객체와 같이, HTTP Body 영역에 있는 데이터를 파라미터에 명시된 타입으로 변환하여 전달할 것을 요구한다면 컨버팅 과정이 필요하다.
이 작업을 담당하는 클래스가 HttpMessageConverter이다. ArgumentResolver는 HttpMessageConverter에게 HTTP Body 영역의 데이터(Message)를 파라미터에 맞게 변환하여 넘겨주기를 요청한다.
- HTTPMessageConverter
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return (canRead(clazz, null) || canWrite(clazz, null) ?
getSupportedMediaTypes() : Collections.emptyList());
}
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}
HttpMessageConverter 인터페이스는 canRead, canWrite가 있다. canRead는 HTTP Body 영역의 데이터를 읽을 수 있느냐를 검사하는 메소드이고, canWrite는 HTTP Body 영역에 데이터를 쓸수 있느냐를 검사하는 메소드이다. 즉, 요청시, HTTP Body 영역에 접근하여 데이터를 가져와 변환하거나, 응답 시, 변환된 데이터를 전달하는 역할을 하는 것이다.
canRead는 파라미터로 Class<?> 타입과 MediaType을 받는다. Class 타입은 Controller 메소드의 파라미터 타입이고 MediaType은 HTTP Content-Type이다. 정리하면 HTTP Body 영역에 Content-Type 형식의 데이터가 있는데, 이를 Class 타입으로 변환가능하면 true, 불가능하면 false를 반환하는 것이다.
canwrite도 파라미터로 Class 타입과 MediaType을 받는다. Class 타입은 Controller 메소드의 반환 타입이고 MediaType은 HTTP Accept 형식이다. 정리하면 HTTP Body 영역에 Accept 형식의 데이터만 받을 수 있는데, Class 타입의 데이터를 Accept 형식의 데이터로 변환 가능하면 true, 불가능하면 false를 반환하는 것이다.
Content-Type은 클라이언트에서 서버로 전달된 HTTP Body 영역의 데이터의 형식을 의미하고 Accept 헤더는 클라이언트가 서버에게 데이터 응답시, Accept 헤더에 명시된 형식만 데이터로 받을 수 있음을 알려주는 것이다.
그러면 간단한 예를 보자.
클라이언트에서 SpringMVC 기반 서버로 HTTP Request를 보냈다. HTTP Body에는 JSON데이터가 담겨있고 Header에는 Content-Type은 application/json, Accept 헤더는 application/json 정보가 담겨있다. 즉, application/json 타입의 데이터를 보낼테니, 응답도 application/json 형태로 해달라는 의미이다.
DispatcherServlet은 url과 매핑되는 Controller를 Spring 컨테이너에서 가져온다. 그리고 Controller 실행권한을 HandlerAdapter에게 위임한다. @RestMapping 어노테이션으로 선언된 Controller는 RequestHandlerMappingAdapter에 의해 메소드가 실행된다. 메소드의 파라미터가 @RequestBody HelloData helloData라고 가정해보자. 이는 HTTP Body 영역의 데이터를 HelloData로 변환하여 전달해달라는 요구이다. 파리미터에 적합한 데이터를 전달하는 ArgumentResolver는 HTTP Body영역의 데이터를 변환해야 하므로, HttpMessageConverter의 구현체 중에 적합한 컨버터를 탐색한다. 탐색은 컨버터의 canRead와 canWrite를 호출하여 변환가능 여부를 판단하는 방식으로 진행된다.
MappingJackson2HttpMessageConverter는 클래스타입이 객체타입이거나 HashMap 타입이고, 미디어타입이 application/json인 경우, HTTP Body 영역의 JSON데이터를 클래스 타입으로 변환시킬 수 있고, 클래스 타입의 데이터를 JSON 데이터로 변환하여 HTTP Body에 write 할 수 있다.
canRead와 canWrite로 MappingJackson2HttpMessageConverter를 찾으면, JSON 데이터를 읽어 HelloData로 변환하여 HandlerAdapter에게 넘기면 HandlerAdpater는 Controller의 파라미터에 생성된 HelloData를 넘겨준다. 그 후, Controller가 HelloData 타입으로 Return하면 MappingJackson2HttpMessageConverter가 HelloData를 JSON 데이터로 변환하여 HttpBody 영역에 write 한다.
이 과정을 실제 코드로 살펴보자.
- HttpEntityMethodProcessor
public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
// 중략...
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
throws IOException, HttpMediaTypeNotSupportedException {
// 중략...
Object body = readWithMessageConverters(webRequest, parameter, paramType); // HTTP메시지컨버터로 Body영역의 데이터 Object로 변환하기
if (RequestEntity.class == parameter.getParameterType()) {
return new RequestEntity<>(body, inputMessage.getHeaders(), //RequestEntity로 반환
inputMessage.getMethod(), inputMessage.getURI());
}
else {
return new HttpEntity<>(body, inputMessage.getHeaders()); // HttpEntity로 반환
}
}
// 중략...
}
HttpEntityMethodProcessor는 ArgumentResolver 인터페이스의 구현체이다. HTTP Body 영역에 JSON 데이터가 들어왔고 Controller가 HttpEntity 타입으로 요구한다면 HttpEntityMethodProcessor가 동작한다. HttpEntityMethodProcessor는 HTTP Body 영역의 JSON데이터를 변환시켜야 하므로 HTTP 메시지 컨버터 구현체 중 적합한 구현체를 탐색하여 클래스 타입에 맞게 변환한다.
- readWithMessageConverters 메소드
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
// 중략...
for (HttpMessageConverter<?> converter : this.messageConverters) { // HTTP 메시지 컨버터 구현체 탐색
// 중략 ...
// HTTP 메시지 컨버터 canRead 메소드 수행
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) { // canRead로 탐색
if (message.hasBody()) {
// 중략 ...
//HTTP 메시지 read 메소드 수행
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
}
}
}
// 중략...
return body;
}
readWithMessageConverters 메소드를 보면 for문으로 HTTP 메시지 컨버터 구현체를 탐색하는 로직을 볼 수 있다. 탐색은 컨버터의 canRead 메소드로 이루어진다. MappingJackson2HttpMessageConverter 구현체가 탐색이 되면, HTTP Body 영역의 데이터를 read하여 가져온 뒤, JAVA환경에 사용가능한 객체로 변환한다. 그리고 이를 ArgumentResolver인 HttpEntityMethodProcessor에게 넘기면 이를 HttpEntity 객체로 변환하여 HandlerAdapter에게 반환한다. 이렇게 생성된 HttpEntity객체는 JSON데이터를 품고 Controller의 파라미터로 넘어간다.
정리하면, 컨트롤러가 HTTP Body영역의 데이터를 특정 클래스타입으로 요구하는 경우, HandlerAdapter는 ArgumentResolver에게 요청하고 ArgumentResolver는 HTTP Body 영역의 미디어타입의 데이터를 파라미터의 클래스 타입으로 변환할 수 있는 HTTP Message Converter의 구현체를 탐색하여 변환을 요청한다. 변환된 데이터는 다시 전달되어 Controller의 파라미터로 넘어간다.
미디어타입과 클래스 타입에 따라 다양한 경우가 존재하므로 HTTP Message Converter 구현체는 정말 다양하게 존재한다. 거의 대부분의 경우는 SpringMVC가 이미 구현해 놓았으나 만약 지원하지 않는 경우가 있다면 HTTP Message Converter 구현체를 직접 만들어서 등록하면 된다. 이처럼 SpringMVC는 다양한 인터페이스를 제공하기에, 필요에 따라 구현체를 만들어 사용하면 된다.
참고자료
'SPRING > Spring MVC' 카테고리의 다른 글
[SpringMVC] PRG ( Post/Redirect/Get ) (0) | 2023.08.18 |
---|---|
[SpringMVC] @ModelAttribute 와 Model (0) | 2023.08.17 |
[SpringMVC] HTTP 응답 - Controller 반환타입 (0) | 2023.08.14 |
[SpringMVC] HTTP 요청 메시지 - TEXT, JSON (0) | 2023.08.12 |
[SpringMVC] HTTP 요청 파라미터 - @ModelAttribute (0) | 2023.08.11 |