본문 바로가기

legacy/Spring

[Spring] 서블릿 필터와 스프링 인터셉터

 

웹 컨테이너란?

웹 컨테이너는 서블릿과 JSP 등 웹 애플리케이션을 실행시키기 위한 환경을 제공한다. HTTP 요청을 처리하기 위한 기능을 제공하고, 대표적인 웹 컨테이너로 Apache Tomcat, Jetty, GlassFish 등이 있다.

역할

  • 서블릿 생명 주기
    • 서블릿 생성, 초기화, 요청 처리, 소멸
  • 클라이언트 요청 및 응답 처리
    • HTTP 요청을 서블릿으로 전달하고, 서블릿으로부터 응답을 전달받아 클라이언트로 전달한다.

 

1. Servlet Filter

클라이언트의 요청이 Dispatcher Servlet으로 전달되기 전, url 패턴에 맞는 요청을 먼저 받아 처리한다.

사진에서 알 수 있듯이 Servlet Filter는 Spring 영역에 존재하는 것이 아니다.

 

서블릿 필터를 사용하기 위해서는 아래의 Filter 인터페이스를 구현해야 한다.

package jakarta.servlet;


public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {
    }
}
  • init()
    • 애플리케이션이 시작되면 최초 1회에 한하여 필터 객체를 초기화한다.
    • 서블릿 필터로 들어오는 요청은 doFilter()에서 처리된다.
  • doFilter()
    • URI 패턴과 일치하는 HTTP 요청을 받아 처리한다.
    • HTTP 요청이 디스패처 서블릿으로 전달되기 전에, 먼저 받아서 처리한다.
    • doFilter()의 매개변수로 FilterChain이 있는데 요청을 여러 필터를 통해 연쇄적으로 처리할 수 있다.
    • 마지막 필터를 호출한 후, 클라이언트의 요청을 디스패처 서블릿으로 전달한다.
  • destroy()
    • 필터 객체를 제거한다.

 

1-1. @WebServlet을 사용하여 필터 등록하기

@WebServlet 애너테이션을 사용하여 서블릿 필터를 등록할 수 있다. 이때, urlPatterns를 사용하여 HTTP 요청의 URI와 일치하는 경우에만 서블릿 필터를 호출할 수 있도록 할 수 있다.

주의할 점은, @WebServlet 애너테이션을 사용한다고 해서 서블릿이 등록되지 않는다. @SpringBootApplication을 사용하는 클래스에서 @ServletComponentScan 애너테이션을 추가해야 서블릿 필터가 정상적으로 호출된다.

Filter1

@Slf4j
@WebFilter(urlPatterns = "/hello")
public class ExampleFilter1 implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("ExampleFilter1 init");
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
        FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        log.info("ExampleFilter1 Request url: {}", httpRequest.getRequestURI());
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        log.info("ExampleFilter1 destroy");
        Filter.super.destroy();
    }
}

Filter2

@Slf4j
@WebFilter(urlPatterns = "/hello")
public class ExampleFilter2 implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("ExampleFilter2 init");
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
        FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        log.info("ExampleFilter2 Request url: {}", httpRequest.getRequestURL());
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
        log.info("ExampleFilter2 destroy");
        Filter.super.destroy();
    }
}

Application

@SpringBootApplication
@ServletComponentScan
public class CsStudyApplication {

    public static void main(String[] args) {
        SpringApplication.run(CsStudyApplication.class, args);
    }
}

 

HTTP 요청 결과

1. 애플리케이션을 실행하면 필터 객체가 생성 및 초기화된다.

2. HTTP 요청이 들어오면 doFilter()가 호출된다.

이때, 웹 컨테이너에 등록된 모든 필터가 호출된다. 왜냐하면 Filter 인터페이스를 구현하면서 doFilter()에 필터 체인을 통해 연쇄적으로 호출되게 했기 때문이다.

3. 애플리케이션이 종료되면 필터 객체가 삭제된다.

 

 

1-2. @Bean을 사용하여 필터 등록하기

@WebServlet 애너테이션을 제거하고, @Bean을 사용하여 등록한다.

Filter1

@Slf4j
public class ExampleFilter1 implements Filter {
	내부 코드는 이전과 동일하다.
}

Filter2

@Slf4j
public class ExampleFilter2 implements Filter {
	내부 코드는 이전과 동일하다.
}

Config

@Configuration
public class FilterConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean<ExampleFilter1> filter1() {
        FilterRegistrationBean<ExampleFilter1> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new ExampleFilter1());
        filterRegistrationBean.addUrlPatterns("/hello");
        return filterRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean<ExampleFilter2> filter2() {
        FilterRegistrationBean<ExampleFilter2> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new ExampleFilter2());
        filterRegistrationBean.addUrlPatterns("/hi");
        return filterRegistrationBean;
    }
}

 

HTTP 요청 결과

“/hello”를 요청하면 “/hi” URI를 처리하는 filter2가 호출되지 않는다.

 

 

2. Spring Interceptor

Controller로 HTTP 요청을 보내기 전에, 이 요청을 가로채어 처리한다. 서블릿 필터의 경우 스프링 영역이 아닌 웹 컨테이너 영역이었다. 반대로 스프링 인터셉터의 경우 사진에서 알 수 있듯이 스프링 영역이다.

 

스프링 인터셉터를 사용하기 위해서는 HandlerInterceptor 인터페이스를 구현해야 한다.

package org.springframework.web.servlet;

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()
    • Controller가 호출되기 전에 실행된다.
    • 컨트롤러로 요청을 전달하기 전에 전처리 작업이 필요한 경우 사용한다.
  • postHandle()
    • Controller가 호출된 후에 실행된다. 단, View가 렌더링 되기 전이다.
    • 매개변수로 ModelAndView를 반환하는데 이는 @Controller를 사용하는 경우에 렌더링 될 View를 전달받는다. 반대로 @RestController는 View가 아닌 JSON 형태의 데이터를 전달받기 때문에 사용되지 않는다.
  • afterCompletion()
    • View가 렌더링 되는 이후에 실행된다. 즉, 모든 작업이 종료되면 호출된다.

 

2-1. Spring Interceptor 등록하기

1. HandlerInterceptor 인터페이스를 구현한다.

@Slf4j
public class ExampleInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
        Object handler) throws Exception {
        log.info("ExampleInterceptor preHandle");
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
        ModelAndView modelAndView) throws Exception {
        log.info("ExampleInterceptor postHandle");
        log.info("handler={}", handler);
        log.info("modelAndView={}", modelAndView);
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
        Object handler, Exception ex) throws Exception {
        log.info("ExampleInterceptor afterCompletion");
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

 

2. 구현한 인터셉터를 등록한다.

WebMvcConfiguer를 구현한 클래스에서 addInterceptors()를 통해 스프링 인터셉터를 등록한다. addPathPatterns()를 통해 특정 요청 URI에 대해서만 스프링 인터셉터가 호출될 수 있도록 할 수 있다.

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ExampleInterceptor())
            .addPathPatterns("/hello");
    }
}

 

HTTP 요청 결과

1. 애플리케이션을 실행한다.

서블릿 필터와 달리 애플리케이션 실행 시점에 preHandle()이 호출되지 않는다. 앞서 말했듯이, 컨트롤러가 호출되기 전에 처리되는 메서드이다.

 

2. URI가 "/hello"인 HTTP 요청이 들어온다.

HTTP 요청을 컨트롤러로 전달하기 전에 preHandle()이 호출된다.

다음으로 Controller에 HTTP 요청을 전달하여 처리된 결과를 반환받는다. “/hello”를 처리하는 컨트롤러의 경우 JSON 데이터를 반환하는 @RestController이다. 따라서 modelAndView = null을 반환받는다.

 

3. 스프링 인터셉터가 종료된다.

 

위 과정이 HTTP 요청이 들어올 때마다 반복된다.

 

 

3. 서블릿 필터와 스프링 인터셉터 차이점

1. 관리되는 컨테이너가 다르다.

서블릿 필터는 스프링 이전의 웹 컨테이너 영역에서 관리되고, 스프링 인터셉터는 스프링 영역에서 관리된다. 따라서 필터의 경우 스프링이 제공하는 기능을 사용할 수 없다. 예를 들면, 스프링이 제공하는 예외처리를 사용할 수 없다. 스프링에서는 @ControllerAdvice를 통해 전역에서 예외를 처리할 수 있지만 서블릿 필터는 그렇지 못한다.

2. Request, Response 객체 조작 가능 여부

서블릿 필터의 doFilter()의 매개변수에는 ServletRequest, ServletResponse를 전달받는다.

package jakarta.servlet;

public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {
    }
}

 

스프링 인터셉터는 매개변수로 HttpServletRequest, HttpServletResponse를 받는다.

package org.springframework.web.servlet;

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

 

필터의 ServletRequest는 최상위 인터페이스이고, 인터셉터의 HttpServletRequest는 ServletRequest의 구현체이다. 서블릿 필터의 경우 필터 체이닝 과정에서 다른 구현체를 바꿔서 넘길 수 있으나, 인터셉터는 그럴 수 없다.

 


 

참고

https://velog.io/@ddangle/Spring-Servlet-과-Servlet-Container

https://dev-coco.tistory.com/173

https://congsong.tistory.com/24

https://mangkyu.tistory.com/173