-
[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
- 서블릿 생명 주기