ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 서블릿 필터와 스프링 인터셉터
    legacy/Spring 2024. 7. 2. 14:31

     

    웹 컨테이너란?

    웹 컨테이너는 서블릿과 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

Designed by Tistory.