SpringMVC는 어떻게 내가 보낸 요청 메세지를 찰떡같이 알아듣고 Json 등의 내가 원하는 값으로 반환해주는걸까?
HTTP 메시지 컨버터에 대해서 조금더 자세히 알아보려고 합니다.
총 2가지로 나눠서 살펴볼 예정입니다.
- POST 방식의 Request 보낼시,
@RequestBody
가 어떻게 Converter 되는지. - GET 방식으로 Request 조회시,
@ResponseBody
가 어떻게 Converter 되는지 확인해볼 예정입니다.
MessageConverter 종류
MessageConverter 의 종류를 이해하기 위해서는 먼저 HttpMessageConverter
가 무엇인지 알아봅니다.
SpringMVC 에서는 인터페이스로 존재하는데, 아래와 같습니다.
package org.springframework.http.converter;
/**
* Strategy interface for converting from and to HTTP requests and responses.
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @author Rossen Stoyanchev
* @since 3.0
* @param <T> the converted object type
*/
public interface HttpMessageConverter<T> {
/**
* Indicates whether the given class can be read by this converter.
* @param clazz the class to test for readability
* @param mediaType the media type to read (can be {@code null} if not specified);
* typically the value of a {@code Content-Type} header.
* @return {@code true} if readable; {@code false} otherwise
*/
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
/**
* Indicates whether the given class can be written by this converter.
* @param clazz the class to test for writability
* @param mediaType the media type to write (can be {@code null} if not specified);
* typically the value of an {@code Accept} header.
* @return {@code true} if writable; {@code false} otherwise
*/
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
/**
* Return the list of media types supported by this converter. The list may
* not apply to every possible target element type and calls to this method
* should typically be guarded via {@link #canWrite(Class, MediaType)
* canWrite(clazz, null}. The list may also exclude MIME types supported
* only for a specific class. Alternatively, use
* {@link #getSupportedMediaTypes(Class)} for a more precise list.
* @return the list of supported media types
*/
List<MediaType> getSupportedMediaTypes();
/**
* Return the list of media types supported by this converter for the given
* class. The list may differ from {@link #getSupportedMediaTypes()} if the
* converter does not support the given Class or if it supports it only for
* a subset of media types.
* @param clazz the type of class to check
* @return the list of media types supported for the given class
* @since 5.3.4
*/
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return (canRead(clazz, null) || canWrite(clazz, null) ?
getSupportedMediaTypes() : Collections.emptyList());
}
/**
* Read an object of the given type from the given input message, and returns it.
* @param clazz the type of object to return. This type must have previously been passed to the
* {@link #canRead canRead} method of this interface, which must have returned {@code true}.
* @param inputMessage the HTTP input message to read from
* @return the converted object
* @throws IOException in case of I/O errors
* @throws HttpMessageNotReadableException in case of conversion errors
*/
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
/**
* Write an given object to the given output message.
* @param t the object to write to the output message. The type of this object must have previously been
* passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}.
* @param contentType the content type to use when writing. May be {@code null} to indicate that the
* default content type of the converter must be used. If not {@code null}, this media type must have
* previously been passed to the {@link #canWrite canWrite} method of this interface, which must have
* returned {@code true}.
* @param outputMessage the message to write to
* @throws IOException in case of I/O errors
* @throws HttpMessageNotWritableException in case of conversion errors
*/
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
메소드를 보면, 짐작할 수 있는 부분이 canRead
canWrite
을 통해서 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크합니다.
만약 체크가 되었다면, 이는 read()
, write()
메세지 컨버터를 통해서 메시지를 읽고 쓸 수 있습니다.
무슨 기준으로 MessageConverter 를 선정하는가?
또한, canRead()
의 메소드 파라미터를 살펴보면 2가지의 기준을 가진다는 사실을 알 수 있습니다.
- Class 타입
- MediaType 이 무엇인지?
2가지 기준을 갖고, 아래 구현체 중 하나를 선택해 클라이언트가 보낸 메세지를 Conveter 시켜줍니다.
HttpMessageConverter<T>
의 구현체들은 아래와 같습니다.
대표적인 MessageConverter 구현체로는
ByteArrayHttpMessageConverter
는 byte[]
데이터를 처리합니다.
- 클래스 타입은
byte[]
, 미디어 타입은*/*
입니다. - 요청을
@RequestBody byte[] data
- 응답을
@ResponseBody return byte[]
미디어 타입은application/octet-stream
StringHttpMessageConverter
는 String
문자로 데이터를 처리합니다.
- 클래스 타입은
String
, 미디어 타입은*/*
- 요청은
@RequestBody String data
- 응답은
@ResponseBody return "OK"
쓰기 미디어 타입은text/plain
MappingJackson2HttpMessageConverter
는 객체를 JSON을 변환시켜주는 컨버터입니다.
- 클래스 타입은 객체 또는
HashMap
, 미디어타입은application/json
관련 - 요청은
@RequestBody HellowData data
- 응답은
@ResponseBody return helloData
쓰기 미디어타입은application/json
MessageConverter 는 어떻게 동작되는가?
HTTP 요청 데이터 읽기
- HTTP 요청이 오고, Controller 에서
@RequestBody
,HttpEntity
파라미터 사용 - 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해
canRead()
호출- 대상 클래스 타입을 지원하는가? - @RequestBody 대상 클래스
- HTTP요청의 Content-Type 미디어 타입을 지원하는가? -
text/plain
,application/json
- ``canRead()
조건을 만족하면
read()` 를 호출해서 객체를 생성하고 반환합니다.
HTTP 응답 데이터 생성하기
- Controller 에서
@ResponseBody
,HttpEntity
로 값이 반환됩니다. - 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해
canWrite()
를 호출합니다.- Return 해주는 값의 대상 클래스 타입을 지원하는가?
- HTTP요청의 Accpet 미디어 타입을 지원하는가? (
@RequestMapping
의produces
)
canWriter()
조건을 만족하면write()
를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성합니다.
그럼 MessageConverter는 SpringMVC 어디에서 사용되는가?
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
HelloData data = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", data.getUsername(), data.getAge());
response.getWriter().write("ok");
}
위와 같은 코드에서 @PostMapping
에서 @RequestMapping(method = RequestMethod.POST)
이 애노테이션을 만나, 처리하는 RequestMappingHandlerAdapter(요청 매핑 핸들러 어탭터)
에서 사용됩니다.
간단하게, RequestMappingHandlerAdapter
를 분석해보면 아래와 같습니다.
package org.springframework.web.servlet.mvc.method.annotation;
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
...
public RequestMappingHandlerAdapter() {
this.messageConverters = new ArrayList<>(4);
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
if (!shouldIgnoreXml) {
try {
this.messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
}
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
...
생성자 부분에서 기본적인 MessageConverter 4가지를 ADD 합니다.
이후에 getDefaultArgumentResolvers()
, getDefaultReturnValueHandlers()
라는 메소드를 볼 수 있습니다.
// RequestMappingHandlerAdapter.java
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);
// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
resolvers.add(new ErrorsMethodArgumentResolver());
resolvers.add(new SessionStatusMethodArgumentResolver());
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
if (KotlinDetector.isKotlinPresent()) {
resolvers.add(new ContinuationHandlerMethodArgumentResolver());
}
// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all
resolvers.add(new PrincipalMethodArgumentResolver());
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));
return resolvers;
}
// RequestMappingHandlerAdapter.java
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);
// Single-purpose return value types
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
handlers.add(new StreamingResponseBodyReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
handlers.add(new HttpHeadersReturnValueHandler());
handlers.add(new CallableMethodReturnValueHandler());
handlers.add(new DeferredResultMethodReturnValueHandler());
handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
// Annotation-based return value types
handlers.add(new ServletModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
// Multi-purpose return value types
handlers.add(new ViewNameMethodReturnValueHandler());
handlers.add(new MapMethodProcessor());
// Custom return value types
if (getCustomReturnValueHandlers() != null) {
handlers.addAll(getCustomReturnValueHandlers());
}
// Catch-all
if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
}
else {
handlers.add(new ServletModelAttributeMethodProcessor(true));
}
return handlers;
}
이 둘의 정체는 무엇일까요?
ArgumentResolvers
중에서 하나를 보면RequestResponseBodyMethodProcessor
의 경우는@RequestBody
,@ResponseBody
를 Controller 에서 선언할 경우 사용되는 MethodProcessor 입니다.
다시 흐름을 이어가기 위해서 이 둘의 정체는 무엇일까요? 정체를 알아보기 위해서 다시 RequestMappingHandlerAdapter.java
를 살펴봅시다.
해당 클래스에서 handleInternal()
를 살펴보면 되는데요. 이 메소드는 DispatcherServlet
에서 호출됩니다.
doDispatch
메소드 내에서 위 그림에서 1번에 해당하는 소스코드를 통해 HandlerAdapter 를 찾습니다. 그 다음 찾은 HandlerAdapter 의 handler 메소드를 실행시킵니다.
위에서 이미 언급한것과 같이
@RequestMapping
사용시 반환되는HandlerAdapter
는RequestMappingHandlerAdapter
입니다.
1번에서 찾은 HandlerAdapter 를 2번에서 handler 동작시킵니다. 그럼 바로 여기서 RequestMappingHandlerAdapter
로 가는 것은 아닙니다. AbstractHandlerMethodAdapter.java
의 handle
이동합니다.
// AbstractHandlerMethodAdapter.java
/**
* This implementation expects the handler to be an {@link HandlerMethod}.
*/
@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return handleInternal(request, response, (HandlerMethod) handler);
}
/**
* Use the given handler method to handle the request.
* @param request current HTTP request
* @param response current HTTP response
* @param handlerMethod handler method to use. This object must have previously been passed to the
* {@link #supportsInternal(HandlerMethod)} this interface, which must have returned {@code true}.
* @return a ModelAndView object with the name of the view and the required model data,
* or {@code null} if the request has been handled directly
* @throws Exception in case of errors
*/
@Nullable
protected abstract ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception;
...
템플릿 메서드패턴으로 handleInternal()
의 구현을 맡기는데, 이때 위에서 언급된 RequestMappingHandlerAdapter
가 실행됩니다.
// RequestMappingHandlerAdapter.java
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
checkRequest(request);
// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod);
}
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}
return mav;
}
invokeHandlerMethod()
를 조금더 자세히 살펴보면 아래와 같습니다.
...
/**
* Invoke the {@link RequestMapping} handler method preparing a {@link ModelAndView}
* if view resolution is required.
* @since 4.2
* @see #createInvocableHandlerMethod(HandlerMethod)
*/
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
위 코드 중에서
...
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
...
이 부분에서 ArgumentResolvers
, ReturnValueHandlers
에 대한 구현체를 Set 합니다.
...
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
**invocableMethod.invokeAndHandle(webRequest, mavContainer);**
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);
}
**invocableMethod.invokeAndHandle(webRequest, mavContainer);**
13번 라인의 invokeAndHandle 을 통해서 set 한 ArgumentResolvers
, ReturnValueHandlesr
객체들 중 하나를 찾아 실행시킵니다.
invocableMethod.invokeAndHandle(webRequest, mavContainer);
이 부분을 타고 들어가면,
invokeForRequest()
를 만나게 되는데, 이 때 우리가 작성한 @Controller의 메소드 이름을 찾아 리플렉션합니다.
이렇게 해서 DispatcherServlet 에서 Controller까지의 코드를 알아보았는데요.
이번에는 관점을 바꿔서, ArgumentResolver
, ReturnValueHandler
가 실행되는 시점을 주의해서 살펴보겠습니다.
ArgumentResolver 동작시, 코드 흐름 이해하기
@RequestMapping
호출시 RequestMappingHandlerAdaptor
가 동작된다고 말씀드렸습니다.
당연히 ArgumentResolver
도 이때 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성합니다.
이 부분에 대해서 코드로 살펴봅시다.
ArgumentResolver
는 인터페이스 HandlerMethodArgumentResolver
를 줄여서 부르는데, 코드는 아래와 같습니다.
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
이 함수에 대한 구현체는 아래와 같습니다.
이 중에 RequestResponseBodyMethodProcessor
클래스가 동작됩니다.
동작되는 위치는 위에서 언급한 RequestMappingHandlerAdapter.java
에서
저 invokeAndHandle()
메서드를 들어가서
invokeForRequest()
로 들어가면, 우리가 찾던 ArgumentResolver
를 찾는 getMethodArgumentValues()
발견할 수 있습니다.
조금 더 구체적으로 들어가면 this.resolvers.resolveArgument()
를 찾을 수 있고 여기서 getArgumentResolver()
호출하여 우리가 드디어 원하던 ArgumentResolver
를 찾아옵니다.
찾아온 ArgumentResolver
는 InvocableHandlerMethod.invokeForRequest()
에서 doInvoke()
를 통해 실행됩니다.
ReturnValueHandler 동작시, 코드 흐름 이해하기
ReturnValueHandler
또한 ArgumentResolver
와 같이 인터페이스를 가지고, 이를 구현하는 객체들의 컬렉션에서 실행되는데요.
returnValueHandler()
가 실행되는 부분을 좀 더 살펴보면 아래와 같니다.
위에서 언급한 RequestResponseBodyMethodProcessor
를 찾아옵니다.
그 다음 handler.handleReturnValue(...)
를 타고 가면,
여기서 반환할 값의 타입과 Value 을 찾아 적절하게 변환시켜 write 합니다.
writeWithMessageConverters()
> List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
를 통해서 적절한 값을 변환시키는데요.
아래는 MediaType 을 찾는 부분입니다.
MediaType
에서 원하는 값을 찾게 되면, 다시 writeWithMessageConverters
로 돌아와
이 부분에 Body 에 필요한 내용을 Write 하고 난 뒤 사용자에게 반환합니다.
반환될 때 DispatcherServlet
은 어떻게 될까요?
이 부분은 여기까지 따라왔다면, 분명 조금 더 찾아보실 수 있을 것입니다 : )
관련 테스트했던 코드는 https://github.com/LenKIM/code-in-action/tree/master/springmvc 여기서 찾아보실수 있습니다!
'Spring 이해하기' 카테고리의 다른 글
Thymeleaf CheatSheet - 2 (template layout) (0) | 2021.09.06 |
---|---|
Thymeleaf CheatSheet - 1 (basic) (0) | 2021.09.06 |
[JPA] JoinColumn vs mappedBy (0) | 2021.06.13 |
인터페이스 빈 주입을 사용해야 하는 이유 (1) | 2021.06.12 |
[JPA] Collection 을 업데이트 할때, add()와 addAll() 을 조심하자. (0) | 2021.06.05 |
댓글