Backend/Spring

Filter, Interceptor

olsohee 2024. 1. 4. 11:45

Filter

필터는 서블릿이 지원하는 기능이다. 필터는 다음과 같은 특징을 갖는다.

  • 필터의 흐름: HTTP 요청 ➡️ WAS ➡️ 필터 ➡️ 서블릿(디스패처 서블릿) ➡️ 컨트롤러
  • 즉, 디스패처 서블릿의 앞 단에서 필터가 동작하기 때문에, 필터는 스프림 범위 밖에서 처리되는 것이다. (스프링의 시작이 디스패처 서블릿이라고 이해하면 된다.)
  • 즉, 스프링 컨테이너가 아닌 톰캣과 같은 서블릿 컨테이너에 의해 관리된다.

Filter 등록

필터를 사용하려면 Filter 인터페이스를 구현하고, 해당 객체를 필터로 등록해야 한다. 그러면 서블릿 컨테이너가 필터를 싱글톤으로 생성하고 관리한다. 

Filter 인터페이스

public interface Filter {

    default void init(FilterConfig filterConfig) throws ServletException {}
    
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;

    default void destroy() {}
}
  • init(): 필터 객체를 초기화하기 위한 메소드이다. 서블릿 컨테이너가 1번 init() 메소드를 호출하여 필터 객체를 초기화한다. 그리고 이후의 HTTP 요청에 대해서는 init() 메소드를 호출하지 않고 doFilter() 메소드만 호출한다.
  • doFilter(): 매핑 url에 맞는 HTTP 요청이 들어올 때마다, 요청이 디스패처 서블릿으로 전달되기 전에 서블릿 컨테이너에 의해 호출되는 메소드이다. 파라미터로 FilterChain이 있는데 FilterChain의 doFilter()를 통해 다음 필터의 실행을 호출한다. 
  • destroy(): 필터 객체를 서비스에서 제거하고 사용하는 자원을 반환하기 위한 메소드이다. destroy() 메소드는 서블릿 컨테이너가 종료될 때 서블릿 컨테이너에 의해 1번 호출된다. 

FilterRegistrationBean

필터를 등록하는 방법은 여러가지가 있는데, 스프링 부트를 사용한다면 FilterRegistrationBean을 사용하면 된다.

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter()); // 등록할 필터
        filterRegistrationBean.setOrder(1); // 필터의 순서 
        filterRegistrationBean.addUrlPatterns("/*"); // 필터를 적용할 url 
        return filterRegistrationBean;
    } 
}

Filter도 스프링 빈으로 등록된다

필터는 서블릿 기술이기 때문에, 다음과 같이 필터는 스프링 범위 밖인 서블릿 범위에서 관리된다. 

따라서 필터가 스프링 컨테이너에 빈으로 등록된다는 것이 이해되지 않을 수 있다. 그러나 우리는 위에서 WebConfig에서 FilterRegistrationBean을 통해 필터를 빈으로 등록했다. 과연 정말 필터가 빈으로 등록되었는지 테스트를 통해 확인해보자.

 

우선 필터를 등록하는 코드를 보자. 

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter()); 
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*"); 
        return filterRegistrationBean;
    } 
}

위와 같이 필터를 등록할 때, @Bean 어노테이션을 사용한다. 즉, @Bean 어노테이션이 붙은 메소드가 반환하는 객체를 빈으로 등록하고, 이때 빈 이름은 메소드 이름이 된다. 즉 FilterRegistration 객체가 logFilter라는 이름의 빈으로 등록된다. 

 

따라서 다음 테스트가 통과되는 것이다. 

@Test
public void logFilter() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ServletApplication.class);
    Object bean = ac.getBean("logFilter");
    Assertions.assertThat(bean).isInstanceOf(FilterRegistrationBean.class);
}

그러면 FilterRegistration에 두개 이상의 필터를 등록하면 어떻게 될까? 다음과 같이 NewFilter를 추가로 필터로 등록하면, 

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean newFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new NewFilter());
        filterRegistrationBean.setOrder(2);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }
}

다음 테스트가 통과한다. 즉, logFilter와 newFilter라는 이름의 스프링 빈이 등록되는데, 두 빈 모두 FilterRegistrationBean인 것이다.

@Test
public void logFilter() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ServletApplication.class);

    Object bean1 = ac.getBean("logFilter");
    Assertions.assertThat(bean1).isInstanceOf(FilterRegistrationBean.class);

    Object bean2 = ac.getBean("newFilter");
    Assertions.assertThat(bean2).isInstanceOf(FilterRegistrationBean.class);
}

Interceptor

인터셉터는 스프링 MVC가 제공하는 기능이다. 인터셉터는 다음과 같은 특징을 갖는다.

  • 인터셉터의 흐름: HTTP 요청 ➡️ WAS ➡️ 필터 ➡️ 서블릿(디스패처 서블릿) ➡️ 인터셉터 ➡️ 컨트롤러
  • 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 등장한다. 
  • 즉, 서블릿 컨테이너에 의해 관리되는 필터와 달리, 스프링 컨테이너에 의해 관리된다.
  • 디스패처 서블릿은 핸들러 매핑을 통해 적절한 컨트롤러를 찾도록 요청하는데, 그 결과로 실행 체인(HandlerExecutionChain)을 받는다. 그리고 이 실행 체인에 1개 이상의 인터셉터가 등록되어 있으면 순차적으로 인터셉터를 실행한 후 컨트롤러를 실행하고, 실행 체인에 인터셉터가 등록되어 있지 않으면 바로 컨트롤러를 실행한다. 

Interceptor 등록

인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하고, 해당 객체를 인터셉터로 등록해야 한다.

HandlerInterceptor

 

public interface HandlerInterceptor {

	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		return true;
	}
    
	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 {}
}
  • preHandle(): 컨트롤러 호출 전에 호출된다(정확히는 핸들러 어댑터 호출 전). 따라서 컨트롤러 이전에 처리해야 하는 전처리 작업이나 요청 정보를 가공하는 경우에 사용하기 적합하다. preHandle()의 반환 값이 true이면 다음 단계로 진행하고, false이면 다음 단계(인터셉터 또는 컨트롤러)를 진행하지 않는다. 
  • postHandle(): 컨트롤러 호출 후에 호출된다(정확히는 핸들러 어댑터 호출 후). 따라서 컨트롤러 이후에 처리해야 하는 후처리 작업의 경우에 사용하기 적합하다. 그리고 컨트롤러에서 예외가 발생하면 호출되지 않는다
  • afterCompletion(): 뷰에서 최종 결과를 생성하는 일을 포함해 모든 작업이 완료된 후에 호출된다. 따라서 요청 처리 중에 사용한 리소스를 반환할 때 사용하기 적합하다. 그리고 postHandle()과 달리, 컨트롤러에서 예외가 발생하더라도 반드시 호출된다. 따라서 예외와 무관하게 공통 처리를 하기 위해서는 postHandle()이 아닌 afterCompletion()에서 진행해야 한다. 그리고 예외를 파라미터로 받아서 로그로 출력할 수 있다.

WebMvcConfigurer.addInterceptors()

인터셉터를 등록하려면 WebMvcConfigurer를 구현하며 addInterceptors() 메소드를 사용하면 된다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor()) // 등록할 인터셉터
                .order(1) // 인터셉터의 순서
                .addPathPatterns("/**") // 인터셉터를 적용할 url
                .excludePathPatterns("/css/**", "/*.ico", "/error"); // 인터셉터를 제외할 url
    }
}

Filter와 Interceptor 비교

공통점으로는 둘 다 웹과 관련된 공통 관심사(ex, 로그인 인증)를 해결하기 위한 기술이라는 점이 있다. 둘의 차이는 다음과 같다.

  • 관리되는 컨테이너
    • 필터는 서블릿 컨테이너에 의해 관리된다.
    • 인터셉터는 스프링 컨테이너에 의해 관리된다.

  • 스프링에 의한 예외 처리 (ex, @ExceptionHandler)
    • 필터는 스프링 이전의 영역이기 때문에 스프링에 의한 예외 처리가 적용되지 않는다. 
    • 인터셉터는 스프링에 의한 예외 처리가 적용된다. 
  • request/response 객체 조작 
    • 필터는 request/response 객체를 조작할 수 있다. 필터는 doFilter() 메소드에서 chain.doFilter()를 통해 필터 체인의 다음 필터를 호출한다. 그리고 chain.doFilter()의 인자로 request/response 객체를 넘겨준다. 따라서 이때 우리가 원하는 request/response 객체를 넘겨줄 수 있다.
    • 인터셉터는 request/response 객체를 조작할 수 없다. 인터셉터는 필터와 달리, 디스패처 서블릿이 실행 체인(HandlerExecutionChain)을 가지며, 순차적으로 인터셉터를 실행시킨다. 그리고 실행되는 인터셉터의 preHandle()의 반환 값이 true이면 다음 인터셉터를 실행시키거나 컨트롤러를 실행시킨다. 즉, 우리가 원하는 request/response 객체를 넘겨줄 수 없고, 기존의 request/response 객체가 고정된다.
public MyFilter implements Filter {

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        // 개발자가 다른 request/response 객체를 넣어줄 수 있다.
        chain.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse());       
    }
}
public class MyInterceptor implements HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // request/response 객체를 교체할 수 없고 boolean 값만 반환할 수 있다.
        return true;
    }
}

Reference