0+ 스프링/0+ 스프링 MVC

[스프링 MVC] 어떻게 컨트롤러는 다양한 종류의 파라미터를 받아서 처리할 수 있을까? 🤔

힘들면힘을내는쿼카 2023. 5. 27. 15:42
728x90
반응형

[스프링 MVC] 어떻게 컨트롤러는 다양한 종류의 파라미터를 받아서 처리할 수 있을까?

 

스프링을 이용해서 개발하다보면 컨트롤러에 알맞은 어노테이션면 설정하면
알아서 요청, 응답 파라미터가 변환되는 것을 알수 있습니다.

그런데 어떻게 스프링은 알아서 척척 요청, 응답 파라미터를 변환하는 걸까요? 🤔

 

미리 보는 결론

RequestMappingHandlerAdapter 동작 방식

 

요청의 경우
@RequestBody를 처리하는 RequestResponseBodyMethodProcessor(ArgumentResolver)가 존재하고,
HttpEntity를 처리하는 HttpEntityMethodProcessor(ArgumentResolver)가 있습니다.
이러한 ArgumentResolver들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성합니다.

 

응답의 경우
@ResponseBodyHttpEntity를 처리하는 ReturnValueHandler가 있습니다.
그리고 이 ReturnValueHandler에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 생성합니다.

HTTP 메시지 컨버터

바로 핸들러 어댑터와 컨트롤러(핸들러) 사이에서 HTTP 메시지 컨버터가 동작하기 때문입니다.

그림을 보면 다음과 같습니다.

 

스프링 부트는 다양한 메시지 컨버터를 제공합니다.
메시지 컨버터는 대상 클래스 타입과 미디어 타입을 확인해서 사용 여부를 결정합니다.
만약 만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어갑니다.

HTTP 메시지 컨버터 인터페이스

HTTP 메시지 컨버터 인터페이스는 6가지의 메소드(1개는 default 메소드)를 정의합니다.

  • canRead()
  • canWrite()
  • read()
  • write()
  • getSupportedMediaTypes()

 

HTTP 메시지 컨버터 인터페이스

public interface HttpMessageConverter<T> {
  // 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 확인
    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
  // 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 확인
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

  // 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능 
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException;
  // 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능 
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException;

    List<MediaType> getSupportedMediaTypes();

    default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
        return (canRead(clazz, null) || canWrite(clazz, null) ?
                getSupportedMediaTypes() : Collections.emptyList());
    }

}

 

주요한 메소드 4개에 대해서 알아보겠습니다.

canRead()

HTTP 요청을 처리할 때 사용합니다.
메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 확인하는 용도로 사용하는 메소드 입니다.

 

canWrite()

HTTP 응답을 처리할 때 사용합니다.
메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 확인하는 용도로 사용하는 메소드 입니다.

 

read()

canRead()ture인 경우 메시지 컨버터를 통해서 메시지를 읽습니다.

 

write()

canWrite()ture인 경우 메시지 컨버터를 통해서 메시지를 작성합니다.

HTTP 메시지 컨버터 동작 과정

컨트롤러에서 @RequsetBody, HttpEntity 파라미터 사용했다는 가정하에 진행하겠습니다.
(컨트롤러에서 @RequsetBody, HttpEntity 파라미터 사용해야 메시지 컨버터가 적용될 수 있습니다.)

 

HTTP 요청 데이터 읽기

1. HTTP 요청 발생
2. canRead() 동작(메시지 컨버터가 메시지를 읽을 수 있는지 확인)
 2-1. 대상 클래스 타입 지원 확인
      e.g) byte[], String, Object ...
 2-2. HTTP 요청의 Content-Type의 미디어 타입 지원 확인
      e.g) text/plain, application/json, */* ...
3. canRead() 조건을 만족하면 read()를 호출하고 객체를 생성하고 반환

 

HTTP 응답 데이터 생성

1. 컨트롤러 내부 로직 수행 완료
2. canWrite() 동작(메시지 컨버터가 결과를 쓸 수 있는지 확인)
 2-1. 반환 클래스 타입 지원 확인
        e.g) byte[], String, Object ...
 2-2. HTTP 요청의 Accept 미디어 타입을 지원 확인(@RequestMapping의 produces)
        e.g) text/plain, application/json, */* ...
3. canWrite() 조건을 만족하면 wirte()를 호출하고 객체를 생성하고 반환

 

예시

HTTP 요청
content-type: application/json
Http request:
{
    "name": "홍길동",
    "age": 100
}


@RequestMapping(produces = MediaType.ALL_VALUE)
public String hello(@RequestBody MyData myData) {
    return "ok";
}

 

HTTP 요청일 때는 MappingJackson2HttpMessageConverter가 적용되고
HTTP 응답일 때는 StringHttpMessageConverter가 적용 됩니다.

컨트롤러에 @RequsetBody, @ResponseBody만 사용되는건 아니잖아요!

맞습니다....!!!!!!!!!!!!!!!!!

 

애노테이션 컨트롤러에는 매우 다양한 파라미터를 사용할 수 있습니다.
@RequestBody, HttpEntity, HttpServletRequest, Model, @RequestParam, @ModelAttribute, @PathVariable 도 사용할 수 있습니다!

 

이것 역시 전부 HTTP 메시지 컨버터가 처리하는 것 일까요? 🤔

HandlerMethodArgumentResolver 두두등장

다양한 애노테션 기반의 파라미터를 처리할 수 있는 이유가 바로 HandlerMethodArgumentResolver 덕분 입니다. ^^

애노테이션 기반의 컨트롤러(핸들러)를 처리하는 핸들러 어댑터(RequestMappingHandlerAdapter)는 바로 HandlerMethodArgumentResolver를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)를 생성합니다.
객체 생성이 완료 되면 컨트롤러(핸들러)를 호출 합니다.

 

흐름

디스패처 서블릿 -> 핸들러 어댑터 -> HandlerMethodArgumentResolver -> Controller

HandlerMethodArgumentResolver 인터페이스

HandlerMethodArgumentResolver 인터페이스는 2가지 메소드를 정의 합니다.

  • supportsParameter()
  • resolveArgument()

 

HandlerMethodArgumentResolver

public interface HandlerMethodArgumentResolver {

    boolean supportsParameter(MethodParameter parameter);

    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

 

supportsParameter()

먼저, 반환 타입이 boolean인 것을 확인할 수 있습니다.
HandlerMethodArgumentResolver가 지원하는 파라미터인지 확인하는 메소드 입니다.

 

resolveArgument()

먼저, 반환 타입이 Object인 것을 확인할 수 있습니다.
supportsParameter()true이면 컨트롤러(핸들러)가 필요로 하는 값(Object)를 생성합니다.

HandlerMethodArgumentResolver 44개의 구현체가 있는것을 확인할 수 있습니다….! 😃

아래 링크에 가능한 파라미터 목록을 볼 수 있습니다.

HandlerMethodArgumentResolver 동작 방식

1. HandlerMethodArgumentResolver의 supportsParameter()를 호출해서 해당 파라미터를 지원하는지 확인.
2. 지원하면 resolveArgument()을 호출하여 실제 객체를 생성.
3. resolveArgument()에서 생성된 객체가 컨트롤러(핸들러)로 넘어감.

응답은 누가 처리하는거지?

ArgumentResolver 와 비슷한데, HandlerMethodReturnValueHandler응답 값을 변환하고 처리합니다.

우리가 컨트롤러에서 반환값을 String으로 뷰이름을 반환해도 동작하는 이유가 바로 이녀석 덕분입니다.^^

@Controller
public class HomeController {

    @GetMapping(value = "/")
    public String home(@PathVariable) {
        // String으로 뷰이름을 반환
        return "home";
    }
}

 

아래 링크에 가능한 응답 값 목록을 확인할 수 있습니다.

HandlerMethodReturnValueHandler 인터페이스

HandlerMethodReturnValueHandler는 2가지 메소드를 정의 합니다.

  • supportsReturnType()
  • handleReturnValue()

 

HandlerMethodReturnValueHandler

public interface HandlerMethodReturnValueHandler {

    boolean supportsReturnType(MethodParameter returnType);

    void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;

}

 

supportsReturnType()

먼저, 반환 타입이 boolean 입니다.
HandlerMethodReturnValueHandler가 지원하는 타입인지 확인하는 메소드 입니다.

 

handleReturnValue()

먼저, 반환타입이 void 입니다.
supportsReturnType()에서 true를 호출하면 반환 값을 처리하여 모델에 속성을 추가하고 뷰를 설정하거나
ModelAndViewContainer.setRequestHandled 플래그를 true로 설정하여 응답이 직접 처리되었음을 나타냅니다.

그러면 ArgumentResolver와 HTTP 메시지 컨버터 둘 다 사용하는 건가요?

네 맞습니다.!

 

정확히는 ArgumentResolver가 HTTP 메시지 컨버터를 사용합니다.

 

스프링 MVC에서는
@RequestBody, @ResponseBody가 있으면RequestResponseBodyMethodProcessor(ArgumentResolver)를 사용하고,
HttpEntity가 있으면 HttpEntityMethodProcessor(ArgumentResolver)를 사용합니다.

RequestMappingHandlerAdapter 흐름

 

요청의 경우

@RequestBody를 처리하는 RequestResponseBodyMethodProcessor(ArgumentResolver)가 존재하고,
HttpEntity를 처리하는 HttpEntityMethodProcessor(ArgumentResolver)가 있습니다.
이러한 ArgumentResolver들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성합니다.

 

응답의 경우

@ResponseBodyHttpEntity를 처리하는 ReturnValueHandler가 있습니다.
그리고 이 ReturnValueHandler에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만듭니다.

 

확장

스프링은 이러한 기능을 인터페이스로 제공합니다.
그말은 필요하면 언제든지 확장해서 사용할 수 있다는 의미 입니다.

  • HandlerMethodArgumentResolver
  • HandlerMethodReturnValueHandler
  • HttpMessageConverter

 

기능 확장은 WebMvcConfiguter를 상속받아 스프링 빈으로 등록하면 됩니다.
(org.springframework.web.servlet.config.annotation)

public interface WebMvcConfigurer {

    default void configurePathMatch(PathMatchConfigurer configurer) {
    }

    default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    }

    default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
    }

    default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    }

    default void addFormatters(FormatterRegistry registry) {
    }

    default void addInterceptors(InterceptorRegistry registry) {
    }

    default void addResourceHandlers(ResourceHandlerRegistry registry) {
    }

    default void addCorsMappings(CorsRegistry registry) {
    }

    default void addViewControllers(ViewControllerRegistry registry) {
    }

    default void configureViewResolvers(ViewResolverRegistry registry) {
    }

    default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    }

    default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
    }

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

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

    default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    }

    @Nullable
    default Validator getValidator() {
        return null;
    }

    @Nullable
    default MessageCodesResolver getMessageCodesResolver() {
        return null;
    }
}

핸들러 매핑 요청 어댑터에서 ArgumentResolver를 초기화 한다!

핸들러 요청 매핑 클래스(RequestMappingHandlerAdapter)를 보면 엄청나게 많은 HandlerMethodArgumentResolver를 초기화하는 것을 볼 수 있습니다.

 

RequestMappingHandlerAdapter
(org.springframework.web.servlet.mvc.method.annotation)

public class RequestMappingHandlerAdapter extends AbatractHandlerMethodAdapter
        implements BeanFActoryAware, InitializingBean {

    // ... //

    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;
    }

    private List<HandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolvers() {
        List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(20);

        // 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 ExpressionValueMethodArgumentResolver(getBeanFactory()));
        resolvers.add(new SessionAttributeMethodArgumentResolver());
        resolvers.add(new RequestAttributeMethodArgumentResolver());

        // Type-based argument resolution
        resolvers.add(new ServletRequestMethodArgumentResolver());
        resolvers.add(new ServletResponseMethodArgumentResolver());

        // Custom arguments
        if (getCustomArgumentResolvers() != null) {
            resolvers.addAll(getCustomArgumentResolvers());
        }

        // Catch-all
        resolvers.add(new PrincipalMethodArgumentResolver());
        resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));

        return resolvers;
    }

    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;
    }

   // ... //
}

참고

 

 

 

 

728x90
반응형