[Spring] 필터(Filter)와 인터셉터(Interceptor)의 차이, 동작원리

필터(Filter)란?

필터(Filter)
[그림 1] 필터(Filter)

 

필터(Filter)는 서블릿(Servlet)에 도달하는 요청(Request)이나 서블릿에서 클라이언트에게 보내는 응답(Response)을 중간에 조작하거나 막을 수 있는 JAVA 진영의 기술이다.

 

Filter는 Spring MVC에 종속되지 않아 스프링 환경이 아닌 다른 환경에서도 사용가능하다.

 

예를 들어 Spring Security는 Filter를 이용한 Authentication 및 Authorization 기술이다. Spring Security를 사용하면 모든 요청과 응답을 중간에서 검증할 수 있다. 이러한 이유로 Spring Security는 Spring MVC 환경이 아닌 곳에서도 사용할 수 있다.

 

 

Filter Chain

필터는 체인으로 구성될 수 있고 중간에 필터를 자유롭게 추가할 수 있다. 

 

예를 들어

 

HTTP 요청 -> WAS -> 로그 필터 -> 검증 필터 -> 디스패처 서블릿 -> 컨트롤러

 

위와 같이 필터 체인을 구성해 하나의 필터에 하나의 기능을 두는 식으로 구성할 수 있다.

(참고로 나는 필터 하나에 하나의 기능만을 두는 것을 선호하는 편이다)

 

 

Filter Interface

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

 

필터는 위와 같은 Filter 인터페이스를 구현하여 사용할 수 있다. 

각각 메서드는 다음과 같은 기능을 한다.

 

  • init() : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출
  • doFilter() : 클라이언트의 요청이 올 때마다 해당 메서드가 호출된다. 여기에 필터의 로직을 구현하면 된다.
  • destory() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출

 

필터 사용 예시

public class LogFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("log filter init");
    }
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();
        String uuid = UUID.randomUUID().toString();
        
        try {
            log.info("REQUEST [{}][{}]", uuid, requestURI);
            chain.doFilter(request, response);
        } catch (Exception e) {
             throw e;
        } finally {
             log.info("RESPONSE [{}][{}]", uuid, requestURI);
        }
    }
    
    @Override
    public void destroy() {
        log.info("log filter destroy");
    }
}

 

위 필터는 간단하게 로그를 남기는 필터이다. 

 

먼저 init() 메서드가 호출되면서 필터를 초기화하고, doFilter()가 호출되며 필터 로직이 실행된다.

중간에 chain.doFilter(request, response) 부분은 다음 필터가 있으면 필터를 호출하고 필터가 없으면 서블릿을 호출하는 필터 체인 부분이다. 이건 doFilter()에서 필수인 로직으로 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다.

 

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

 

스프링 부트를 사용한다면 FilterRegistrationBean을 이용해 필터를 등록할 수 있다.

 

  • setFilter() : 등록할 필터를 지정
  • setOrder() : 필터 체인 순서 지정
  • addUrlPatterns() : 필터를 적용할 URL 패턴을 지정한다. 서블릿 URL 패턴과 동일하다.

 

Request/Response 객체 조작

자주 사용하는 기능은 아니지만, 필터는 인터셉터와 다르게 Request와 Response를 조작해 다른 객체로 바꿀 수 있다.

필터 체인을 통해 doFilter(request, response)를 호출할 때 ServletRequest/ServletResponse를 구현한 다른 객체를 만들어 넘길 수 있기 때문이다.

 

따라서 필터 체인으로 다른 필터나 서블릿에게 원하는 객체를 넘길 수 있다.

 


 

인터셉터(Interceptor)란?

인터셉터(Interceptor)
[그림 2] 인터셉터(Interceptor)

 

인터셉터(Interceptor)는 Spring MVC가 제공하는 기술로 디스패처 서블릿에서 컨트롤러 사이의 요청 및 응답을 조작할 수 있는 기술이다.

 

필터와 비슷하지만 인터셉터는 필터보다 더 정교하고 다양한 기능을 제공한다.

인터셉터는 Spring MVC에 특화된 기능을 제공하므로 특별히 필터를 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 게 더 편리하다.

 

Interceptor Chain

인터셉터도 필터와 마찬가지로 체인을 구성할 수 있다.

 

HTTP 요청 -> WAS -> 디스패처 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러

 

위와 같이 다양한 인터셉터를 중간에 자유롭게 추가할 수 있다.

 

 

인터셉터 사용법

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

 

인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면 된다.

 

  • preHandle() : 컨트롤러 호출 전에 호출된다. (정확히는 핸들러 어댑터 호출 전에 호출)
  • postHandle() : 컨트롤러 호출 후에 호출된다. (정확히는 핸들러 어댑터 호출 후에 호출)
  • afterCompletion() : 뷰가 렌더링 된 이후에 호출된다.

 

인터셉터 예외 상황

만약 컨트롤러에서 예외가 발생하면 postHandle()은 호출되지 않는다.

 

그러나 afterCompletion()은 항상 호출된다.

따라서 이때 예외를 파라미터로 받아 어떤 예외가 발생했는지 로그로 출력할 수 있다.

 

예외와 무관하게 공통 처리를 할 때 afterCompletion()을 활용할 수 있다.

 

 

인터셉터 사용예시

public class LogInterceptor implements HandlerInterceptor {
    public static final String LOG_ID = "logId";
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse
                             response, Object handler) throws Exception {
                             
        String requestURI = request.getRequestURI();
    
        String uuid = UUID.randomUUID().toString();
        request.setAttribute(LOG_ID, uuid);
    
        //@RequestMapping: HandlerMethod
        //정적 리소스: ResourceHttpRequestHandler
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
        }
    
        log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
        return true; //false 진행X
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse
                           response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle [{}]", modelAndView);
    }
    public void afterCompletion(HttpServletRequest request, HttpServletResponse
                                response, Object handler, Exception ex) throws Exception {
        
        tring requestURI = request.getRequestURI();
        String logId = (String)request.getAttribute(LOG_ID);
        log.info("RESPONSE [{}][{}]", logId, requestURI);
        if (ex != null) {
            log.error("afterCompletion error!!", ex);
        }
    }
}

 

위 예시는 로그를 남기는 인터셉터이다.

 

preHandle()에서는 컨트롤러가 호출되기 전에 요청을 처리할 수 있다. 

이때  preHandle()에서 지정한 값을 postHandle, afterCompletion()에서도 사용하고 싶다면 request에 담아두면 된다.

true를 반환하면 정상적으로 다음 인터셉터나 컨트롤러를 호출하고, false라면 진행하지 않는다.

 

preHandler()의 파라미터로 받는 handler에는 호출할 컨트롤러 메서드의 정보가 담겨있다. 만약 @Controller, @RequestMapping을 활용한 핸들러 매핑을 사용한다면 핸들러 정보로 HandlerMethod가 넘어온다.

하지만 @Controller가 아닌 /resources/static 같은 정적 리소스가 호출된다면 ResourceHttpRequestHandler가 핸들러 정보로 넘어온다.

 

 

 

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");
    }
    //...
}

 

인터셉터는 WebMvcConfigurer가 제공하는 addInterceptors()를 사용해서 등록할 수 있다.

 

  • registry.addInterceptor() : 인터셉터를 등록
  • order() : 인터셉터의 호출 순서 등록
  • addPathPatterns() : 인터셉터를 적용할 URL 패턴을 지정
  • excludePathPatterns() : 인터셉터에서 제외할 패턴을 지정

필터와 다르게 인터셉터는 excludePathPatterns 등을 통해 URL 패턴을 매우 정밀하게 지정할 수 있다.

스프링은 서블릿이 제공하는 URL 경로보다 더 세밀한 설정이 가능한데, 이것은 다음 공식 문서를 참고하자.

 

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html

 

PathPattern (Spring Framework 6.1.6 API)

Compare this pattern with a supplied pattern: return -1,0,+1 if this pattern is more specific, the same or less specific than the supplied pattern.

docs.spring.io

 


정리

필터와 인터셉터
[그림 3] 필터와 인터셉터

 

필터와 인터셉터는 [그림 3]처럼 동시에 사용할 수도 있다.

 

둘의 차이를 표로 정리하자면 다음과 같다.

필터와 인터셉터의 차이(Filter vs Interceptor)
[그림 4] 필터와 인터셉터의 차이

 

 


참고

 

[1] https://www.baeldung.com/spring-mvc-handlerinterceptor-vs-filter

[2] https://mangkyu.tistory.com/173

[3] 인프런 김영한 강의 : 

반응형

댓글

Designed by JB FACTORY