스프링에는 컨트롤러 이전에 처리해줘야 하는 로직이 들어가는 인터페이스인HandlerInterceptor
, HTTP 요청에서 필요한 값을 꺼내어 컨트롤러 메서드의 파라미터로 바인딩하는 HandlerMethodArgumentResolver
인터페이스가 있습니다. 기본적으로 스프링에 구현되어 있는 리졸버 구현체들이 있는데요, 이 구현체들은 스프링 시작 시 RequestMappingHandlerAdapter
가 초기화되면서 afterPropertiesSet
메서드의 호출 시 등록됩니다. getDefaultArgumentResolvers
메서드로 기본적으로 구현된 리졸버들을 가져오기 때문이죠.
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
...
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);
// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
...
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
...
// 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;
}
}
하지만 스프링에서 제공되는 인터셉터나 리졸버 외에 추가적인 인터셉터나 리졸버가 필요한 경우가 있습니다. 예를 들어 인증/인가 등을 요청이 컨트롤러로 들어오기 전에 체크하거나, 요청을 보낸 회원 정보를 컨트롤러에 파라미터로 바인딩 해야 하는 경우가 있겠네요. 그리고 이럴 때 구현한 인터셉터나 리졸버는 보통 다음과 같이 WebMvcConfigurer
를 구현한 객체에서 등록하게 됩니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(final InterceptorRegistry registry) {
SampleInterceptor sampleInterceptor = new SampleInterceptor();
registry.addInterceptor(sampleInterceptor);
}
@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
SampleArgumentResolver sampleArgumentResolver = new SampleArgumentResolver();
resolvers.add(sampleArgumentResolver);
}
}
그런데, 만약 인터셉터나 리졸버를 여러 종류를 만들어야 한다면 어떻게 될까요? AnotherInterceptor
와 AnotherResolver
를 추가해보도록 하겠습니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
SampleInterceptor sampleInterceptor = new SampleInterceptor();
AnotherInterceptor anotherInterceptor = new AnotherInterceptor();
registry.addInterceptor(sampleInterceptor);
registry.addInterceptor(anotherInterceptor);
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
SampleArgumentResolver sampleArgumentResolver = new SampleArgumentResolver();
AnotherArgumentResolver anotherArgumentResolver = new AnotherArgumentResolver();
resolvers.add(sampleArgumentResolver);
resolvers.add(anotherArgumentResolver);
}
}
보시는 것 처럼 인터셉터 하나, 리졸버 하나가 추가될 때마다 계속해서 코드를 추가해나가야 합니다. 등록된 인터셉터와 리졸버가 많다면 귀찮으면서도 관리가 어려운 작업이죠. 그렇다면 이들을 빈으로 등록하면 자동으로 주입되지 않을까요?
인터셉터나 리졸버를 등록하는 과정은 등록하려는 인터셉터나 리졸버가 스프링 빈으로 등록되어 있는지 여부는 중요하지 않습니다. 위에서 만든 모든 인터셉터와 리졸버에 @Component
를 붙여서 스프링 빈으로 만든다고 가정해보겠습니다. 스프링 빈으로 등록하면 자동으로 인터셉터와 리졸버로서의 기능을 할 것 같지만 그렇지 않습니다. 스프링 빈으로 등록되어 있더라 하더라도 위에서 설명한 RequestMappingHandlerAdapter
는 저희가 구현한 인터셉터와 리졸버 구현체들을 바로 인식할 수가 없기 때문이죠. 결국 다시 WebMvcConfigurer
에서 의존성을 주입받아서 직접 등록해줘야 합니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final SampleInterceptor sampleInterceptor;
private final SampleArgumentResolver sampleArgumentResolver;
private final AnotherArgumentResolver anotherArgumentResolver;
private final AnotherInterceptor anotherInterceptor;
public WebConfig(final SampleInterceptor sampleInterceptor, final SampleArgumentResolver sampleArgumentResolver,
final AnotherArgumentResolver anotherArgumentResolver,
final AnotherInterceptor anotherInterceptor) {
this.sampleInterceptor = sampleInterceptor;
this.sampleArgumentResolver = sampleArgumentResolver;
this.anotherArgumentResolver = anotherArgumentResolver;
this.anotherInterceptor = anotherInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(sampleInterceptor);
registry.addInterceptor(anotherInterceptor);
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(sampleArgumentResolver);
resolvers.add(anotherArgumentResolver);
}
}
이 과정 역시 new 키워드로 객체를 직접 생성해서 등록해주는 것과 그닥 차이가 없어보입니다. 어떻게 하면 많은 인터셉터와 리졸버를 한번에 등록해줄 수 있을까요?
우리는 여기서 인터페이스 타입의 DI에 대해 한번 다시 생각하고 넘어갈 필요가 있습니다. 어떤 인터페이스 타입의 구현체 여러 개가 스프링 빈으로 등록될 경우, 하나의 빈을 주입하려면 필드 이름의 구분이나 @Primary
, @Qualifier
같은 어노테이션의 힘을 빌리지 않고는 주입할 수 없습니다. 하지만 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);
}
}
보시는 것 처럼 List<HandlerInterceptor>
, List<HandlerMethodArgumentResolver>
타입으로 모든 커스텀 인터셉터와 리졸버를 주입받을 수 있습니다. 이렇게 주입받은 리스트를 등록하는 코드 한 줄씩만 작성하면 손쉽게 커스텀 인터셉터와 커스텀 리졸버를 등록할 수 있습니다. 이런 식으로 등록하게 되면, 앞으로 커스텀 인터셉터와 커스텀 리졸버가 추가되더라도 WebConfig
는 건드릴 필요 없이 새로 만드는 커스텀 객체에 @Component
만 붙이면 자동으로 등록됩니다.
참고로, 스프링에 기본으로 구현되어 있는 리졸버들은 빈으로 등록되어 있지 않고 RequestMappingHandlerAdaptor
에서 직접 리졸버로 등록하기 때문에 스프링의 의존성 주입 기능으로는 주입되지 않습니다.