스프링/3. 스프링 MVC

49. 컨버터가 실행되는 위치, ArgumentResolver

sdafdq 2023. 8. 13. 20:57

여기 선 안보인다.

 

우리가 지금까지 컨트롤러 만든 거 보면, 예를 들어

    @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
        HelloData helloData = objectMapper.readValue(messageBody,HelloData.class);

        log.info("name={}, age={}",helloData.getUsername(), helloData.getAge());
        return "ok";
    }

이런 식으로 우리가 파라미터 써놓기만 하면 자동으로 누군가가 그에 맞는 인자를 넣어서 쓸 수 있게 해준다.

그 외에도 @RequestBody String messageBody 이런 거 읽어서, 이런 애노테이션까지 다 인지하고 처리해준다. 그게 어디에서 일어날까.

 

그건 바로

이 부분에서 핸들러 어댑터가 handler를 호출할 때 일어난다.

 

우리가 옛적에 저거 하나하나 구현해서 봤을때

핸들러 mapping에서 얻어오고, 그에 맞는 adapter를 찾기 위해 그 mapping에서 찾아온 handler를 핸들러 어댑터 목록에서 맞는 핸들러 어댑터를 반환해주는 함수에 인자로 그 handler를 넣어주고 어댑터를 반환받았다.

 

여기서 보면, 먼저 2번처럼 Controller를 직접적으로 호출하기 전에, 

ArgumentResolver 라는 것을 호출한다.

Resolver 뭔가 자주 나오는 거 보니 무언가 패턴인 듯 싶다.

 

여튼, 컨트롤러의 인자를 보고, 이 ArgumentResolver한테 이 인자들 하나 하나 가져오라고 시키는 거다.

내 예상으론 Request같은 경우 요청의 데이터니까, 그런 요청같은 것이 들어올 경우 하나의 객체를 생성해서 거기다가 정보들을 모아놓는 것 같다. 그 다음 이게 ArgumentResolver한테 요청할 수 있는 인자의 종류가 30가지가 넘는데, 어떤 요청에 대한 정보들을 그렇게 하나로 다 모아놓은 곳에서 찾아서, get~~~ 이런 식으로 반환할 수 있게끔 하는 메소드가 저 요청에 대한 정보를 모아놓은 곳의 메소드로 있지 않을 까 싶다.

 

    @ResponseBody
    @PostMapping("/request-body-json-v1")
    public String requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        InputStream ip = request.getInputStream();
        String messageBody = StreamUtils.copyToString(ip, StandardCharsets.UTF_8);
        HelloData helloData = objectMapper.readValue(messageBody,HelloData.class);

        log.info("name={}, age={}",helloData.getUsername(), helloData.getAge());
        return "ok";
    }

어댑터가 너 HttpServletRequest에 관한 객체 생성해서 넘겨줘, 하면서 ArgumentResolver한테 시키는 거다.

그 다음 HttpServletResponse도 시키고. 

이렇게 모든 객체가 다 완성되고 나야, 컨트롤러에 그 객체를 담고 호출해준다.

 

 

이것의 실제 인터페이스 이름은 HandlerMethodArgumentResolver 인데, 직접 들어가서 보면

public interface HandlerMethodArgumentResolver {
	boolean supportsParameter(MethodParameter parameter);

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

}

이렇게 되어있다. 보면 supportsParamter로 먼저 되는 파라미터인지 검사하고,

그게 되는 파라미터면 resolveArgument해서 실제 Object를 넘겨준다.

 

 

이게 supportsParameter 로 먼저 되는지 확인하고, 되면 argument 넘겨주는게, 

 

    private MyHandlerAdapter getHandlerAdapter(Object handler){
        for( MyHandlerAdapter adapter : handlerAdapters){
            if(adapter.supports(handler)) return adapter;
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
    }

우리가 과거 구현해봤던 핸들러 어댑터 찾아보는 방식이랑 비슷한데, 이것도 어댑터 패턴인가보다.

 

 

 

여튼 이 어댑터패턴은 우리가 확장할 수 있다.

그래서 우리가 직접 이걸 확장해서 만들 수도 있다.

 

 

 

그리고 우리가 보면, return 형식도 다양하다. 

위의 사진도 보면 return하는데 무언갈 거치고 있다.

HandlerMethodReturnValueHandler, 줄여서 ReturnValueHandler 라고 하는데,

저거 들어가보면

public interface HandlerMethodReturnValueHandler {

	boolean supportsReturnType(MethodParameter returnType);

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

}

이렇게 구현되어 있다. 내가 줄 수 있는 타입이 맞는지 판단하는 supports와, 실제 그 returnType을 뭔가 다루는? 그런거다.

이것도 어댑터 패턴인 것 같다. 생각보다 어댑터 패턴이 굉장히 많이 구현되어 있다.

 

확장 가능하겠다.

 

 

이 메시지 바디 처리할 때도(@RequestBody, @ResponseBody, HttpEntity 등 이용할 때), ArgumentResolve를 이용한다. 저거 내부에 보면 

for (HttpMessageConverter<?> converter : this.messageConverters) {
    Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
    GenericHttpMessageConverter<?> genericConverter =
            (converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);
    if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
            (targetClass != null && converter.canRead(targetClass, contentType))) {
        if (message.hasBody()) {
            HttpInputMessage msgToUse =
                    getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
            body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                    ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
            body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
        }
        else {
            body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
        }
        break;
    }
}

ArgumentResolver를 상속받은 가상클래스 중 하나의 일부분이다. readWithMessageConverters() 라는 함수다.

저기 보면 HttpMessageConverter, 즉 메시지바디 컨버터 중 최상위 인터페이스를 받아서, 그게 맞는지 converter.canRead()해본다. 저번 강의에서 이야기 했던 HttpMessageConverter의 canRead가 나온다.

 

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;

}

실제로 이렇게 되어있다.

 

ArgumentResolver가 저걸 호출해보면서 처리를 하는거다.

 

@ResponseBody도 똑같다.

 

메시지컨버터 종류도 여러가지이다. xml, string 등등 굉장히 많은 메시지바디에 대한 컨버팅을 지원한다.

 

 

 

 

확장하는 법

저런 리졸브들, 뭐 어댑터들 아니면 그 외에 다양한 것들 확장하고 싶을 땐, WebMvcConfigurer를 상속받아 만들어 빈에 등록하면 된다.

다음은 실제 WebMvcConfigurer의 인터페이스이다.

 

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

}

 

Path에 대한거, 동기화에 대한거, 서블렛에 대한거, 포맷, 인터셉터에 대한거, 핸들러에 대한거, 여튼 정말 많은 종류를 추가할 수 있다.

어.. 이게 그런 것 같은데.. 그러니까 약간 우리가 mapping 구현했을 때 

    private final Map<String, Object> handlerMappingMap = new HashMap<>();
   
    private void initHandlerMappingMap(Map<String, Object> handlerMappingMap){
        handlerMappingMap.put("/front-controller/v5/v3/members/new-form",new MemberFormControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members/save",new MemberSaveControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members",new MemberListControllerV3());
    }
    
    
    private Object getHandler(HttpServletRequest request){
        return handlerMappingMap.get(request.getRequestURI());
    }

이렇게 우리가 구현한 저쪽 부근에, 저 initHandlerMappingMap에 추가하는거랑 비슷한 느낌 아닐 지?