본문 바로가기
Spring 이해하기

SpringMVC 에서 말하는 MessageConverter 코드로 이해하기

by simplify-len 2021. 8. 15.

SpringMVC는 어떻게 내가 보낸 요청 메세지를 찰떡같이 알아듣고 Json 등의 내가 원하는 값으로 반환해주는걸까?

HTTP 메시지 컨버터에 대해서 조금더 자세히 알아보려고 합니다.

총 2가지로 나눠서 살펴볼 예정입니다.

  1. POST 방식의 Request 보낼시, @RequestBody 가 어떻게 Converter 되는지.
  2. 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가지의 기준을 가진다는 사실을 알 수 있습니다.

  1. Class 타입
  2. MediaType 이 무엇인지?

2가지 기준을 갖고, 아래 구현체 중 하나를 선택해 클라이언트가 보낸 메세지를 Conveter 시켜줍니다.

HttpMessageConverter<T> 의 구현체들은 아래와 같습니다.

image-20210811231141456

대표적인 MessageConverter 구현체로는

ByteArrayHttpMessageConverterbyte[] 데이터를 처리합니다.

  • 클래스 타입은 byte[], 미디어 타입은 */* 입니다.
  • 요청을 @RequestBody byte[] data
  • 응답을 @ResponseBody return byte[] 미디어 타입은 application/octet-stream

StringHttpMessageConverterString 문자로 데이터를 처리합니다.

  • 클래스 타입은 String, 미디어 타입은 */*
  • 요청은 @RequestBody String data
  • 응답은 @ResponseBody return "OK" 쓰기 미디어 타입은 text/plain

MappingJackson2HttpMessageConverter 는 객체를 JSON을 변환시켜주는 컨버터입니다.

  • 클래스 타입은 객체 또는 HashMap, 미디어타입application/json 관련
  • 요청은 @RequestBody HellowData data
  • 응답은 @ResponseBody return helloData 쓰기 미디어타입은 application/json

MessageConverter 는 어떻게 동작되는가?

HTTP 요청 데이터 읽기

  1. HTTP 요청이 오고, Controller 에서 @RequestBody, HttpEntity 파라미터 사용
  2. 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead() 호출
    • 대상 클래스 타입을 지원하는가? - @RequestBody 대상 클래스
    • HTTP요청의 Content-Type 미디어 타입을 지원하는가? -text/plain , application/json
  3. ``canRead()조건을 만족하면read()` 를 호출해서 객체를 생성하고 반환합니다.

HTTP 응답 데이터 생성하기

  1. Controller 에서 @ResponseBody, HttpEntity 로 값이 반환됩니다.
  2. 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 를 호출합니다.
    • Return 해주는 값의 대상 클래스 타입을 지원하는가?
    • HTTP요청의 Accpet 미디어 타입을 지원하는가? (@RequestMappingproduces)
  3. 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 에서 호출됩니다.

image-20210812003016241

doDispatch 메소드 내에서 위 그림에서 1번에 해당하는 소스코드를 통해 HandlerAdapter 를 찾습니다. 그 다음 찾은 HandlerAdapter 의 handler 메소드를 실행시킵니다.

위에서 이미 언급한것과 같이 @RequestMapping 사용시 반환되는 HandlerAdapterRequestMappingHandlerAdapter 입니다.

1번에서 찾은 HandlerAdapter 를 2번에서 handler 동작시킵니다. 그럼 바로 여기서 RequestMappingHandlerAdapter로 가는 것은 아닙니다. AbstractHandlerMethodAdapter.javahandle 이동합니다.

// 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); 이 부분을 타고 들어가면,

image-20210812085004660

invokeForRequest() 를 만나게 되는데, 이 때 우리가 작성한 @Controller의 메소드 이름을 찾아 리플렉션합니다.

image-20210812085417175

이렇게 해서 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;

}

이 함수에 대한 구현체는 아래와 같습니다.

image-20210812090832536

이 중에 RequestResponseBodyMethodProcessor 클래스가 동작됩니다.

동작되는 위치는 위에서 언급한 RequestMappingHandlerAdapter.java 에서

image-20210812091642778

invokeAndHandle() 메서드를 들어가서

image-20210812091758675

invokeForRequest() 로 들어가면, 우리가 찾던 ArgumentResolver 를 찾는 getMethodArgumentValues() 발견할 수 있습니다.

조금 더 구체적으로 들어가면 this.resolvers.resolveArgument() 를 찾을 수 있고 여기서 getArgumentResolver() 호출하여 우리가 드디어 원하던 ArgumentResolver 를 찾아옵니다.

찾아온 ArgumentResolverInvocableHandlerMethod.invokeForRequest() 에서 doInvoke() 를 통해 실행됩니다.

ReturnValueHandler 동작시, 코드 흐름 이해하기

ReturnValueHandler 또한 ArgumentResolver 와 같이 인터페이스를 가지고, 이를 구현하는 객체들의 컬렉션에서 실행되는데요.

returnValueHandler() 가 실행되는 부분을 좀 더 살펴보면 아래와 같니다.

image-20210812092921016

위에서 언급한 RequestResponseBodyMethodProcessor 를 찾아옵니다.

image-20210812093145894

그 다음 handler.handleReturnValue(...) 를 타고 가면,

image-20210812093235403

여기서 반환할 값의 타입과 Value 을 찾아 적절하게 변환시켜 write 합니다.

writeWithMessageConverters() > List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); 를 통해서 적절한 값을 변환시키는데요.

아래는 MediaType 을 찾는 부분입니다.

image-20210812093532901

MediaType 에서 원하는 값을 찾게 되면, 다시 writeWithMessageConverters 로 돌아와

image-20210815145320221

이 부분에 Body 에 필요한 내용을 Write 하고 난 뒤 사용자에게 반환합니다.

반환될 때 DispatcherServlet 은 어떻게 될까요?

이 부분은 여기까지 따라왔다면, 분명 조금 더 찾아보실 수 있을 것입니다 : )

관련 테스트했던 코드는 https://github.com/LenKIM/code-in-action/tree/master/springmvc 여기서 찾아보실수 있습니다!

김영한-스프링웹MVC강의자료

댓글