개발 이론/Spring

[Spring] HandlerMethodArgumentResolver와 WebMvcConfigurer

dal_been 2023. 11. 16. 21:10
728x90

아아 저거 어디서 많이 봤는데.. 분명 인강들으면서 리졸버라는 말 썻던거같은데... 찾아보니 역시나 배웠던거...나의 기억련이란 정말 하루살이 같구나..

 

 

HandlerMethodArgumentResolver

 

컨트롤러 메서드에서 특정 조건에 맞는 파라미터가 있을때 원하는 값을 바인딩해주는 인터페이스이다.

 

엥??그게 뭔소리여...

그니까 컨트롤러 메서드를 만들때 @RequestBody라든지 @PathVariable를 사용한적이 있을것이다. 이때 HandlerMethodArgumentResolver가 HTTP메세지를 처리해주는 것이다.

 

 

public interface HandlerMethodArgumentResolver {

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

 

 

supportsParameter()를 호출해서 해당 파라미터를 지원하는지 체크하고 resolveArgument를 호출해서 실제 객체를 생성한다.

이렇게 생성된 객체가 컨트롤러 호출시 넘어가는 것이다!

 

 

그래서 이부분을 활용해서 @RequestBody처럼 어노테이션으로 만들어 우리가 원하는 객체로 컨트롤러에서 반환할 수 있다

이부분은 좀있다가밑에서 이어가겠다.

 

근데 여기서 의문 근데 누가 메세지 바디의 내용을 String또는 객체로 만들어??

바로 HttpMessageConverter이다

 

HttpMessageConverter

 

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;

}

 

 

 

여기서 canRead,canWrite메서드를 통해 메세지 컨버터가 해당 클래스 ,미디어타입을 지원하는지 체크한다

read,write통해서는 메세지 컨버터를 통해 메세지를 읽고 쓰는 기능을 지원한다

 

그래서 스프링 부트에는 이를 구현한 다양한 메세지 컨버터들이 잇지만 대표적인 컨버터를 보자면

 

- ByteArrayHttpMessageConverter

  • byte[]타입의 데이터를 지원한다
  • 요청 : @RequestBody byte[] data
  • 응답 : @ResponseBody return byte[], 쓰기 미디어 타입 : application/octet-stream

 

- StringHttpMessageConverter

  • String 문자로 데이터를 처리한다
  • 요청 : @RequestBody String data
  • 응답 : @ResponseBody return "ok" 쓰기 미디어타입 text/plain

     

     

- MappingJackson2HttpMessageConverter

  • 클래스 타입 객체또는 HashMap
  • 요청 : @RequestBody HelloData data

  • 응답 : @ResponseBody return helloData 쓰기 미디어타입 application/json 관련

그래서 언제수행되는 데??

 

 

일단 ArgumentResolver를 통해 컨트롤러가 필요로 하는 다양한 파라미터 값을 생성한다

이때 Http메세지 컨버터를 사용해서 필요한 파라미터 값, 객체를 생성하는 것이다.

 

 

HandlerMethodArgumetResolver 예제를 만들어보자

 

나는 이 어노테이션을 통해 로그인을 했는지 확인하고 객체를 생성할 것이다

 

일단 어노테이션

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {


}

 

 

간단하게 세션에 로그인 여부를 저장했다고 하자

 

@RequiredArgsConstructor
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {

    private final HttpSession httpSession;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {

        return parameter.hasParameterAnnotataion(Login.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return httpSession.getAttribute("user");
    }
}

 

resolveArguemen에서 내가 원하는 객체로 반환가능하다 메서드 반환이 Object이기 때문

 

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final LoginUserArgumentResolver loginUserArgumentResolver;


    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(loginUserArgumentResolver);
    }
}

 

그래서 이렇게 등록해주면 끝!!

 

 

근데 여기서 궁금한점... 만약 HandlerMethodArgumentResolver가 많다면..?이렇게 다 일일히 등록해줘야행??

이거는 인테셉터에도 적용되는 거다. 인터셉터도 만약 여러개가 있다면 다 일일히 등록해줘야하는가??

 

 

아그러면 위 코드처럼 스프링빈으로 등록했으니 되겠지?? 아니다. RequsetMapppingHandlerAdapter가 우리가 구현한 인터셉터나 리졸버를 인식할 수가 없다. 즉 어째든 WebMvcCongifurer의 의존성 주입을 받야한다. 

 

여기서 잠깐 RequsetMapppingHandlerAdapter란 스프링에 구현된 기본적인 리졸버 구현체들이 있는데 해당 클래스 안에 afterPropertiesSet메서를 통해서 등록해준다

 

그러면 어찌하지...

찾아보니 List타입으로 해당 인테페이스를 구현한 모든 구현체 빈을 주입받을 수 있다

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final List<HandlerInterceptor> interceptors;
    private final List<HandlerMethodArgumentResolver> argumentResolvers;

    public WebConfig(final List<HandlerInterceptor> interceptors,
                     final List<HandlerMethodArgumentResolver> argumentResolvers) {
        this.interceptors = interceptors;
        this.argumentResolvers = argumentResolvers;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        interceptors.forEach(registry::addInterceptor);
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.addAll(argumentResolvers);
    }
}

 

 

이렇게 하면 인터셉터나 리졸버에 @Component만 붙여주면 자동으로 스프링빈으로 등록된다

근데 이렇게 하면 기본으로 구현된 리졸버들도 빈ㅇ로 등록되나?? 아니다 빈으로 등록되지 않고 앞서 말한RequsetMapppingHandlerAdapter가 직접 리졸버로 등록해준다

 

 

 

 

인프런_김영한님의 스프링 MVC2 자료를 참고하였습니다