ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Filter와 Interceptor의 차이
    Spring 2021. 9. 30. 00:01

    Filter

    필터란 리소스에 대한 요청(서블릿/정적 콘텐츠)이나 리소스의 응답 또는 둘 다에 대해 필터링 작업을 수행하는 개체를 말한다.


    // javax.servlet
    Interface Filter {
      void destroy();
      void doFilter(ServletRequest request, ServletResponse response, FilterChain chain);
      void init(FilterConfig filterConfig);
    }

    doFilter 메서드에서 필터링 작업을 수행한다. 모든 필터는 초기화 매개변수를 얻을 수 있는 FilterConfig 객체에 대한 액세스 권한을 가지고 있으며 필터링 작업에 필요한 리소스를 로드하는 데 사용할 수 있는 ServletContext에 대한 참조를 가지고 있다.


    필터의 사용 예시

    • Authentication
    • Logging
    • Encryption
    • Tokenizing

    Filter Method

    init

    service에 필터를 적용할 수 있음을 나타내기 위해 컨테이너에 의해 호출되는 메서드

    서블릿 컨테이너는 필터를 인스턴스화 한 후 init 메서드를 정확히 한 번 호출한다. 필터링 작업의 수행을 요청하기 전 init 메서드가 성공적으로 완료되어야 한다.

    init 메서드가 다음 중 하나인 경우 ServletException을 throw하며 서비스에 배치할 수 없다.

    • ServletException 발생
    • 웹 컨테이너서 정의한 기간 내에 반환되지 않을 경우

    doFilter

    컨테이너에 의해 클라이언트가 chain 끝에 있는 리소스를 요청할 때, 요청/응답 쌍이 체인을 통해 전달될 때 마다 호출되는 메서드

    이 메서드에 전달된 FilterChain을 통해 필터는 Request와 Response를 체인의 다음 엔티티에 전달할 수 있다.

    doFilter의 일반적 동작 과정은 다음과 같다.

    1. Request 검토
    2. (선택적)입력 필터링을 위해 Request 객체를 사용자 구현 로직으로 감싸서 콘텐츠 또는 헤더를 필터링
    3. (선택적)출력 필터링을 위해 Response 객체를 사용자 구현 로직으로 감싸서 콘텐츠 또는 헤더를 필터링
    4.  
      • FilterChain 객체의 (chain.doFilter())를 이용해서 chain에 연결된 다음 엔티티를 호출하거나
      • 요청/응답 쌍을 다음 엔티티로 넘기지 않아서 Filter Chain에 Request 처리를 중단함
    5. filter chain의 다음 엔티티를 호출한 후 Response의 헤더를 직접 설정

    destroy

    컨테이너에 의해 필터가 서비스 중이 아님을 나타내기 위해 호출되는 메서드

    1️⃣ doFilter 메서드에 있는 모든 스레드가 종료되거나 2️⃣ timeout period가 지난 후 딱 한 번만 호출된다. 컨테이너가 destroy 메서드를 호출한 후에는 filter의 이 인스턴스에서 doFilter 메서드를 다시 호출하지 않는다.

    이 메서드를 사용하면 filter가 보유중인 자원(ex. 메모리, 파일, 스레드)를 정리하고 영속상태가 필터의 현 상태와 동기화 되도록 만들 수 있다.


    (Handler)Interceptor

    💡 이 글에서 언급하는 Interceptor란 Spring MVC의 HandlerInterceptor를 의미한다.

    Interceptor(이하 인터셉터)를 알아보기 전 HandlerMapping을 다시 한 번 정의하자면, DispatcherServlet이 Request를 처리할 수 있게 method와 URL을 연결시키는 Spring MVC 구성요소다.

    DispatcherServlet은 메서드를 (실제로)실행하기 위해 HandlerAdapter를 사용한다.

    Handler Interceptor는 이 전반적 Context에 포함되어 작업을 수행한다. 요청을 1️⃣ 처리하기 전, 2️⃣ 처리한 후, 3️⃣ (view의 렌더링이) 완료된 후에 작업을 처리하기 위해 Handler Interceptor를 사용한다.

    인터셉터는 cross-cutting concern(횡단 관심사)를 처리하기 위해 사용하거나, Spring 모델에서 사용하는 logging, 파라미터 변경 등의 반복적인 코드 작업을 방지할 수 있다.

    
    void pizzaMethod() {
      System.out.println("pizza method has been called");
      /*
      Business Logic
      ...
      */
    }
    
    void hamburgerMethod() {
      System.out.println("hamburger method has been called");
      /*
      Business Logic
      ...
      */
    }

    예시와 같은 로깅 작업은 메서드 들에서 반복적으로 사용될 수 있는 코드면서, 비즈니스 로직과는 관련이 없다.

    따라서 이러한 cross-cutting 관심사를 Interceptor로 분리하는 것은 Spring의 AOP와 관련이 있다.


    Interceptor 설정

    👉(클릭) pom.xml setting for Interceptors
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>

    Interceptor 사용 in Spring

    프레임워크에서 HandlerMapping으로 작업하는 Interceptor는 HandlerInterceptor를 implements해야 한다.

    • prehandle() - 실제 핸들러가 실행되기 전 호출되어 view가 실행되지 않는다.
    • poseHandle() - 핸들러가 실행된 후 호출된다.
    • afterCompletion() - 전체 Request가 완료되고 view가 생성된 후 호출된다.

    세 가지 메서드를 통해 모든 종류의 pre-, pose- 처리를 할 수 있다.

    HandlerInterceptorHandlerInterceptorAdapter의 차이

    • 전자는 위의 세 가지 메서드를 모두 override 해야 한다
    • 후자는 필요한 메서드만 override 하면 된다

    메서드의 반환 값으로 request가 handler에 의해 추가로 처리되어야 할 지 여부를 알 수 있다(boolean).


    👉(클릭) handler 메서드의 사용 예시
    @Override
    public boolean preHandle(
      HttpServletRequest request,
      HttpServletResponse response, 
      Object handler) throws Exception {
        // your code
        return true;
    }
    @Override
    public void postHandle(
      HttpServletRequest request, 
      HttpServletResponse response,
      Object handler, 
      ModelAndView modelAndView) throws Exception {
        // your code
    }
    @Override
    public void afterCompletion(
      HttpServletRequest request, 
      HttpServletResponse response,
      Object handler, Exception ex) {
        // your code
    }

    HandlerInterceptor@Controller Annotation이 표시 된 모든 클래스에 Interceptor를 적용하는 DefaultAnnotationHandlerMapping Bean에 등록된다.


    Interceptor 활용 - Logger

    Web Application의 개발 시 사용하는 Logging은 Interceptor 활용의 대표적인 예시다.

    아래의 예시에서 Log4J를 활용한 Interceptor 로깅 방식을 설명한다.

    public class LoggerInterceptor extends HandlerInterceptorAdapter {
        ...
    }

    우선 HandlerInterceptorAdapter를 상속받아 LoggerInterceptor 클래스를 정의해준 뒤

    private static Logger log = LoggerFactory.getLogger(LoggerInterceptor.class);

    위와 같은 코드를 사용하면 Log4J를 이용해서 로깅을 할 수 있다.

    Request로부터 URI와 Parameter를 전달받아 로깅하는 예시이다. 이 예시(특수한 경우)에서는 로깅 시 Parameter에 비밀번호와 같은 민감한 정보가 포함되었을 경우, 정보를 그대로 표시하는 것이 아닌 *(별표)로 치환하도록 작성했다.

    @Override
    public boolean preHandle(
      HttpServletRequest request,
      HttpServletResponse response, 
      Object handler) throws Exception {
    
        log.info("[preHandle][" + request + "]" + "[" + request.getMethod()
          + "]" + request.getRequestURI() + getParameters(request));
    
        return true;
    }
    👉(클릭) 추가 코드
    private String getParameters(HttpServletRequest request) {
        StringBuffer posted = new StringBuffer();
        Enumeration<?> e = request.getParameterNames();
        if (e != null) {
            posted.append("?");
        }
        while (e.hasMoreElements()) {
            if (posted.length() > 1) {
                posted.append("&");
            }
            String curr = (String) e.nextElement();
            posted.append(curr + "=");
            if (curr.contains("password") 
              || curr.contains("pass")
              || curr.contains("pwd")) {
                posted.append("*****");
            } else {
                posted.append(request.getParameter(curr));
            }
        }
        String ip = request.getHeader("X-FORWARDED-FOR");
        String ipAddr = (ip == null) ? getRemoteAddr(request) : ip;
        if (ipAddr!=null && !ipAddr.equals("")) {
            posted.append("&_psip=" + ipAddr); 
        }
        return posted.toString();
    }
    
    private String getRemoteAddr(HttpServletRequest request) {
        String ipFromHeader = request.getHeader("X-FORWARDED-FOR");
        if (ipFromHeader != null && ipFromHeader.length() > 0) {
            log.debug("ip from proxy - X-FORWARDED-FOR : " + ipFromHeader);
            return ipFromHeader;
        }
        return request.getRemoteAddr();
    }

    return value를 활용해서 핸들러에게 추가 처리가 필요한 지 여부를 알린다. false를 반환하면 스프링은 request의 처리가 완료되었고 추가 처리가 필요하지 않다고 가정한다.

    @Override
    public void postHandle(
      HttpServletRequest request, 
      HttpServletResponse response,
      Object handler, 
      ModelAndView modelAndView) throws Exception {
    
        log.info("[postHandle][" + request + "]");
    }

    poseHandle 메서드는 HandlerAdapter는 호출되었지만 view는 아직 렌더링 되지 않았을 때 호출된다.

    ModelAndView에 속성을 추가하거나 핸들러 메서드로 요청 처리 시간을 측정하는 데 활용할 수 있다.

    @Override
    public void afterCompletion(
      HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) 
      throws Exception {
        if (ex != null){
            ex.printStackTrace();
        }
        log.info("[afterCompletion][" + request + "][exception: " + ex + "]");
    }

    afterCompletion 메서드를 활용하면 view가 렌더링 된 이후이기 때문에 request와 response 데이터 및 예외에 대한 정보에 대해 로깅할 수 있다.


    Interceptor Configuration

    인터셉터를 Spring Configuration에 추가하려면 WebMvcConfigurer를 구현하는 WebConfig 클래스 내에서 addInterceptor() 메서드를 재정의해야 한다.

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggerInterceptor());
    }

    또는 xml 파일을 이용해 설정할 수 있다.

    <mvc:interceptors>
        <bean id="loggerInterceptor" class="com.baeldung.web.interceptor.LoggerInterceptor"/>
    </mvc:interceptors>

    configuration이 적용되면 인터셉터가 활성화되고 로깅을 활용할 시 애플리케이션의 모든 요청이 올바르게 기록된다.

    스프링 인터셉터가 여러 개 구성된 경우, preHandle() 메서드는 구성 순서대로 실행되는 반면 postHandle()과 afterCompletion() 메서드는 역순으로 호출된다.

    Spring Boot를 사용할 경우 @EnableWebMvc Annotation을 사용하면 Boot의 auto configuration이 손상될 수 있으니 사용하면 안 된다.


    Filter vs Interceptor

    Interceptor는 Spring Framework의 구성 요소지만, Filter는 Servlet의 구성 요소다. Filter는 Request 요청이 서블릿에 도달하지 못하도록, 혹은 Response가 클라이언트에 도달하지 못하도록 차단할 수 있다.

    Spring SecurityAuthentication 및 Authorization에 필터를 적용한 대표적인 예시다. DelegatingFilterProxy라는 단일 필터를 적용해서 Spring Security를 설정하면, 들어오는 트래픽과 나가는 트래픽 모두를 가로챌 수 있다. 따라서 Spring Security는 Spring MVC 외부에서 사용할 수 있다.

    @Component
    public class LogFilter implements Filter {
    
        private Logger logger = LoggerFactory.getLogger(LogFilter.class);
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
          throws IOException, ServletException {
            logger.info("Hello from: " + request.getLocalAddr());
            chain.doFilter(request, response);
        }
    
    }

    위에서 로깅에 Interceptor를 적용한 것 처럼, Filter Interface를 implement해서 로깅을 할 수도 있다.

    doFilter 메서드를 override한 후, FilterChain 객체를 이용해 request를 허용하거나 차단할 수 있고 @Component 어노테이션을 붙임으로써 Spring에서 해당 Filter를 사용할 수 있다.


    차이점

    image

    Filter는 Request가 DispatcherServlet에 도달하기 전에 위치해서 동작하지만, HandlerInterceptor는 DispatcherServlet과 Controller 사이에서 동작한다.

    Filter는 Spring MVC와 별개의 위치에서 동작하기 때문에 분리를 원하는 작업에 적용하기 좋다.


    Filter의 사용

    • Authentication
    • Logging and auditing
    • Image and data compression
    • Any functionality we want to be decoupled from Spring MVC

    Interceptor의 사용

    • Handling cross-cutting concerns such as application logging
    • Detailed authorization checks
    • Manipulating the Spring context or model

    결론적으로,

    Filter는 Request가 Controller와 Spring MVC 외부에 도달하기 전 조작할 수 있다.
    HandlerInterceptor는 애플리케이션의 cross-cutting 관심사에 적용하기 적합하다. 대상 Handler 및 ModelAndView 객체에 대한 액세스를 제공하기 때문에 더 세분화 된 제어에 알맞다.


    참고 자료

Designed by Tistory.