
지난 포스팅에서는 Spring이 가지는 MVC + Service 패턴과
그 내부 구조를 자세히 들여다봤다.
이번 시간에는 이 구조에서 조금 더 자세히 들어가
추가적인 기능을 하는 Filter와 Interceptor에 대해 알아보자.
서블릿 컨테이너와 스프링 컨테이너는 기능에 있어서 유사한 측면이 있다.
Spring Bean에서는 Bean 적용 사항을 BeanProcessor가 담당하는 것처럼,
웹에서도 유사한 기능을 하는 역할이 있는데 그게 바로 Filter와 Interceptor이다.

Client Request → WAS(Tomcat) → Filter → Servlet → Controller
웹 사용자 요청은 서버에서 Servlet에 처리를 요청하기 전 Filter 단계를 거친다.
공통 기능을 처리한다는 점에서 Dispatcher Servlet과 유사하지만,
두 개념은 다른 목적으로 설계되어 독립적으로 동작한다.
Filter를 사용하면 로그와 같은 기능을 추가할 수도 있고,
인증과 같은 접근 제어의 목적으로도 사용할 수 있다.
Filter에 대해 자세히 알아보자.
// default 키워드는 interface에서 설계 가능
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
public default void destroy() {}
}
Filter 인터페이스 구현의 핵심은 Client Request마다 수행될 doFilter()이다.
doFilter() 메서드에 구현하고자 하는 로직을 설계하면 된다.
doFilter() 메서드는 Http Request가 아닌 것에 대비하여
ServletRequest, ServletResponse를 파라미터로 전달받는다.
HttpServletRequest, HttpServletResponse의 상위 타입으로,
Http Request, Http Response인 경우 다운캐스팅하여 사용하면 된다.
FilterChain은 Filter의 연쇄 호출과 Servlet 호출을 위해 전달받는 객체이다.
로직 내부에 반드시 chain.doFilter(request, response)를 호출해야 한다.
해당 메서드를 호출해야 다음 필터가 호출되고, 호출 필터가 없으면 Servlet을 호출한다.
chain.doFilter() 메서드를 호출하지 않으면 다음 단계로 진행되지 않는다.

@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean filterRegister() {
FilterRegistrationBean<Filter> filterRegistrationBean =
FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new Filter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/url");
return filterRegistrationBean;
}
}
Filter를 사용하기 위해서는 Bean으로 등록해야 하는데,
FilterRegstrationBean<Filter> 객체를 생성하여 등록할 수 있다.
Filter를 추가할 때마다 생성하여 등록해야 한다.
setFilter() 메서드를 호출하여 Filter를 등록하고
setOrder(n) 메서드를 호출하여 필터 순서를 설정할 수 있다.
순서가 설정된 필터는 chain 형식으로 연쇄적으로 호출된다.
addUrlPatterns() 메서드를 호출하여 Filter를 적용할 url을 설정할 수 있다.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// ** 다운캐스팅해서 사용 **
HttpServletRequest httpRequest = (HttpServletRequest) request;
// session이 없으면 즉시 종료
HttpSession session = httpRequest.getSession(false);
if (session = null) {
httpResponse.sendRedirect("/home?key=value"); // ** query parameter 전달 가능 **
return;
}
chain.doFilter(request, response);
}
지금까지는 Client가 Request를 하면 url Controller를 호출하는 것이 당연했다.
하지만 Filter를 사용하면 특정 조건에 따라 접근을 제한할 수도 있다.
chain.doFilter() 메서드를 호출하기 전 로그를 출력해둘 수도 있다.
Session의 기능을 잘 모르겠다면 아래를 읽어보자.

Client Request → WAS → Filter → Servlet → Interceptor → Controller
Filter를 추가함으로써 Request를 효율적으로 제어할 수 있게 됐지만
Servlet 호출 전에만 로직이 동작하여 제한적이라는 단점이 있다.
Interceptor는 Filter의 기능을 보완하여 더욱 세밀한 제어를 가능하게 해준다.
Filter보다 Interceptor 사용을 더 권장한다.
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {}
default void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, @Nullable ModelAndView modelAndView) throws Exception {}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, @Nullable Exception ex) throws Exception {}
}
Filter와 달리 Interceptor는 적용할 시점을 더욱 다양하게 지원한다.
Filter의 chain.doFilter() 메서드처럼 다음 단계 진행을 위해
필수적으로 호출해야할 메서드도 없어 훨씬 간결하게 작성할 수 있다.

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor())
.order(1)
.addPathPatterns("/url")
.excludePathPatterns("/url/exclude");
}
}
Interceptor를 등록하기 위해서는
WebMvcConfigurer를 구현하여 addInterceptors() 메서드를 오버라이딩해야 한다.
addInterceptor() 메서드를 통해 Interceptor를 등록하고,
order() 메서드로 Interceptor 순서를 설정한다.
Interceptor는 chain 형식으로 연쇄적으로 호출된다.
addPathPatterns() 메서드를 통해 Interceptor를 적용할 url을 지정할 수 있고
excludePathPatterns() 메서드를 통해 제외할 url도 지정할 수 있다.

이전 Spring 구조와 동작을 학습하면서 ArgumentResolver에 대해 언급했다.
Controller 호출 전에 로직을 수행한다는 점에서 ArgumentResolver는
Filter, Interceptor와 유사하다.
@GetMapping("/")
public String method(HttpServletRequest request, Model model) {
// 코드
}
ArgumentResolver는 Controller 호출 전 동작하여 파라미터 정보를 생성해준다.
위 메서드에서는 ArgumentResolver가
HttpServletRequest 객체와 Model 객체를 실제로 전달해주는 역할을 한다.

public interface HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter);
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception;
}
supportsParameter() 메서드에서는 MethodParameter 객체를 통해
파라미터에 대한 정보를 검증하여 resolveArgument() 메서드 실행 여부를 결정한다.
애노테이션을 설계하여 애노테이션이 선언되었는지,
파라미터의 타입은 기대한 타입인지 등을 확인할 수 있다.
resolveArgument() 메서드는 Controller 호출 직전에 호출되어
supportsParameter() 메서드를 통과한 파라미터에 실제 정보를 생성해준다.
예를 들어 로그인의 경우 로그인 전용 애노테이션을 설계하고
supportsParameter() 메서드를 통해 로그인 애노테이션 여부와
로그인 정보를 담고 있는 객체 타입을 확인한 뒤,
resolveArgument() 메서드를 통해 webRequest로부터 session 정보를 조회하여
실제 객체를 전달해줄 수 있다.
이 과정에서 만약 supportsParameter() 메서드에서 true를 반환했는데도
resolveArgument() 내부에서 검증에 실패했다면 Controller를 호출하지 않을 수 있다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new HandlerMethodArgumentResolver());
}
}
구현한 ArgumentResolver를 적용하기 위해서는 등록 과정을 거쳐야 하는데
WebMvcConfigurer 인터페이스를 구현하여
addArgumentResolvers() 메서드를 오버라이딩해야 한다.
resolvers 파라미터의 add() 메서드를 통하여 ArgumentResolver를 등록할 수 있다.

웹 요청에 따라 기능을 추가하거나 접근 제어가 가능한
Filter와 Interceptor에 대해 알아봤다.
Filter 보다는 세밀한 제어가 가능한 Interceptor를 주로 사용해보도록 하자.
유사한 기능을 가진 ArgumentResolver의 동작과 사용도 이해해보도록 하자.