Spring MVC - Message Converter

뚝딱이·2022년 8월 3일
1

스프링 MVC

목록 보기
9/23

위의 그림은 이전에 배웠던 ResponseBody의 동작원리를 보여준다.
@ResponseBody 를 사용하면 viewResolver를 사용하지 않고,HttpMessageConverter 가 동작하여 HTTP의 BODY에 문자 내용을 직접 반환한다.

  • 기본 문자처리: StringHttpMessageConverter
  • 기본 객체처리: MappingJackson2HttpMessageConverter
  • byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음

스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용한다.

  • HTTP 요청: @RequestBody , HttpEntity(RequestEntity)
  • HTTP 응답: @ResponseBody , HttpEntity(ResponseEntity)
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 a 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;

}

위의 인터페이스는 HttpMessageConverter이며 HTTP 요청, HTTP 응답 둘 다 사용된다.

  • canRead() , canWrite() : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크
  • read() , write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능

스프링 부트 기본 메세지 컨버터

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
  • MappingJackson2HttpMessageConverter : application/json
    • 클래스 타입: 객체 또는 HashMap , 미디어타입 application/json 관련
    • 요청 예) @RequestBody HelloData data
    • 응답 예) @ResponseBody return helloData 쓰기 미디어타입 application/json 관련

예제를 통해 알아보자.

content-type: application/json
@RequestMapping
void hello(@RequetsBody String data) {}

클래스 타입이 String이므로 StringHttpMessageConverter의 클래스 타입에 맞는데 이 컨버터는 미디어 타입에 아무거나 와도 상관이 없으므로 application/json도 지원한다. 따라서 StringHttpMessageConverter이 동작한다.

content-type: application/json
@RequestMapping
void hello(@RequetsBody HelloData data) {}

클래스 타입이 객체이므로 MappingJackson2HttpMessageConverter의 클래스 타입에 맞는다. 또한 미디어 타입이 application/json으로 부합해 위의 예제에선 MappingJackson2HttpMessageConverter가 동작한다.

content-type: text/html
@RequestMapping
void hello(@RequetsBody HelloData data) {}

클래스 타입이 객체인데 미디어 타입이 text/html이다. 따라서 기준에 부합하는 컨버터가 존재하지 않아 동작하지 않는다.

  • HTTP 요청 데이터 읽기

    • HTTP 요청이 오고, 컨트롤러에서 @RequestBody , HttpEntity 파라미터를 사용한다.
    • 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead() 를 호출한다.
      • 대상 클래스 타입을 지원하는가. : 예) @RequestBody 의 대상 클래스 ( byte[] , String , HelloData )
      • HTTP 요청의 Content-Type 미디어 타입을 지원하는가. : 예) text/plain , application/json , */*
    • canRead() 조건을 만족하면 read() 를 호출해서 객체 생성하고, 반환한다.
  • HTTP 응답 데이터 생성

    • 컨트롤러에서 @ResponseBody , HttpEntity 로 값이 반환된다.
    • 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 를 호출한다.
      • 대상 클래스 타입을 지원하는가. : 예) return의 대상 클래스 ( byte[] , String , HelloData )
      • HTTP 요청의 Accept 미디어 타입을 지원하는가.(더 정확히는 @RequestMapping 의 produces ) : 예) text/plain , application/json , */*
    • canWrite() 조건을 만족하면 write() 를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성한다.

Request Mapping Handler Adapter 구조

그렇다면 이 컨버터들은 언제 동작하는걸까

핸들러를 호출하는, @RequestMapping 을 처리하는 핸들러 어댑터인
RequestMappingHandlerAdapter 에 있다.

Argument Resolver

애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다.
HttpServletRequest , Model 은 물론이고, @RequestParam , @ModelAttribute 같은 애노테이션 그리고 @RequestBody , HttpEntity 같은 HTTP 메시지를 처리하는 부분까지 매우 큰 유연함을 보여주었다.
이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.
애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter 는 바로 이
ArgumentResolver 를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다. 그리고 이렇게 파리미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다.
스프링은 30개가 넘는 ArgumentResolver 를 기본으로 제공한다.

public interface HandlerMethodArgumentResolver {

	/**
	 * Whether the given {@linkplain MethodParameter method parameter} is
	 * supported by this resolver.
	 * @param parameter the method parameter to check
	 * @return {@code true} if this resolver supports the supplied parameter;
	 * {@code false} otherwise
	 */
	boolean supportsParameter(MethodParameter parameter);

	/**
	 * Resolves a method parameter into an argument value from a given request.
	 * A {@link ModelAndViewContainer} provides access to the model for the
	 * request. A {@link WebDataBinderFactory} provides a way to create
	 * a {@link WebDataBinder} instance when needed for data binding and
	 * type conversion purposes.
	 * @param parameter the method parameter to resolve. This parameter must
	 * have previously been passed to {@link #supportsParameter} which must
	 * have returned {@code true}.
	 * @param mavContainer the ModelAndViewContainer for the current request
	 * @param webRequest the current request
	 * @param binderFactory a factory for creating {@link WebDataBinder} instances
	 * @return the resolved argument value, or {@code null} if not resolvable
	 * @throws Exception in case of errors with the preparation of argument values
	 */
	@Nullable
	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

위의 인터페이스는 ArgumentResolver 인터페이스이다. supportsParameter를 통해 해당 파라미터를 지원해줄 수 있는지 확인하고, resolveArgument를 통해 파라미터를 받는다. 여러 종류의 파라미터를 지원하기 때문에 반환형이 Object 인것을 확인할 수 있다.

ArgumentResolver는 argument를 찾는 것이고, 그 ArgumentResolver들 중에서 HTTP 메세지 바디에 있는 걸 바로 뭔가 처리해야하면 message converter를 호출한다.
단순하게 처리할 수 있는 건 ArgumentResolver에서 바로 처리하고, HTTP 바디 메세지를 처리해야되는건 컨버터를 통해서 처리하면된다.

그리고 원한다면 직접 이 인터페이스를 확장해서 원하는 ArgumentResolver 를 만들 수도 있다.

ReturnValueHandler

HandlerMethodReturnValueHandler 를 줄여서 ReturnValueHandler 라 부른다.
ArgumentResolver 와 비슷한데, 이것은 응답 값을 변환하고 처리한다.
컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 바로 ReturnValueHandler 덕분이다.
스프링은 10여개가 넘는 ReturnValueHandler 를 지원한다.
예) ModelAndView , @ResponseBody , HttpEntity , String

public interface HandlerMethodReturnValueHandler {

	/**
	 * Whether the given {@linkplain MethodParameter method return type} is
	 * supported by this handler.
	 * @param returnType the method return type to check
	 * @return {@code true} if this handler supports the supplied return type;
	 * {@code false} otherwise
	 */
	boolean supportsReturnType(MethodParameter returnType);

	/**
	 * Handle the given return value by adding attributes to the model and
	 * setting a view or setting the
	 * {@link ModelAndViewContainer#setRequestHandled} flag to {@code true}
	 * to indicate the response has been handled directly.
	 * @param returnValue the value returned from the handler method
	 * @param returnType the type of the return value. This type must have
	 * previously been passed to {@link #supportsReturnType} which must
	 * have returned {@code true}.
	 * @param mavContainer the ModelAndViewContainer for the current request
	 * @param webRequest the current request
	 * @throws Exception if the return value handling results in an error
	 */
	void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;

}

HTTP Message Converter

위치

HTTP 메시지 컨버터를 사용하는 @RequestBody 도 컨트롤러가 필요로 하는 파라미터의 값에 사용된다.
@ResponseBody 의 경우도 컨트롤러의 반환 값을 이용한다.
요청의 경우 @RequestBody 를 처리하는 ArgumentResolver 가 있고, HttpEntity 를 처리하는 ArgumentResolver 가 있다. 이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.
응답의 경우 @ResponseBody 와 HttpEntity 를 처리하는 ReturnValueHandler 가 있다. 그리고여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.
스프링 MVC는 @RequestBody @ResponseBody 가 있으면 RequestResponseBodyMethodProcessor (ArgumentResolver), HttpEntity 가 있으면 HttpEntityMethodProcessor (ArgumentResolver)를 사용한다.

확장

스프링은 다음을 모두 인터페이스로 제공한다. 따라서 필요하면 언제든지 기능을 확장할 수 있다.

  • HandlerMethodArgumentResolver
  • HandlerMethodReturnValueHandler
  • HttpMessageConverter

스프링이 필요한 대부분의 기능을 제공하기 때문에 실제 기능을 확장할 일이 많지는 않다. 기능 확장은 WebMvcConfigurer 를 상속 받아서 스프링 빈으로 등록하면 된다. 실제 자주 사용하지는 않으니 실제 기능 확장이 필요할 때 WebMvcConfigurer 를 검색해보자.

default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
	}

출처 : 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

profile
백엔드 개발자 지망생

0개의 댓글