Spring

[Spring] 필터와 인터셉터 (Filter vs Interceptor)

DEV숨 2022. 11. 16. 22:31

필터와 인터셉터 Filter vs Interceptor

Spring은 공통적으로 여러 작업을 처리함으로써 중복된 코드를 제거할 수 있도록 많은 기능들을 지원하고 있다.

필터와 인터셉터가 그 중 하나이다. 

 

 

여기서 말하는 공통의 관심사란 무엇일까?

다음과 같은 상황을 가정해보자.

로그인 한 사용자만 상품 관리 페이지에 들어갈 수 있다.

상품관리 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성하면 되겠지만, 만일 등록, 수정, 삭제, 조회 등 컨트롤러가 늘어나면 어떻게 될까?

로그인 한 사용자인지 아닌지 체크하는 로직이 중복되어 들어갈 것이다.

이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 것을 공통 관심사라고 한다.

여기서 등록, 수정, 삭제, 조회 등 여러 로직에서 공통으로 인증에 대해서 관심을 가지고 있다. 이러한 공통 관심사는 Spring의 AOP로도 해결가능하다.

그러나 웹과 관련된 공통 관심사는 서블릿 필터 또는 인터셉터를 사용하는게 좋다. 그 이유는 웹과 관련된 공통의 관심사는 HTTP의 헤더나 URL의 정보들이 필요한데, 서블릿 필터나 스프링 인터셉터는 HttpServletRequest를 제공한다.

 

 

서블릿 필터(Filter)

필터란?

필터는 J2EE 표준 스펙 기능으로 스프링의 디스패처 서블릿에 요청이 전달되기 전/후에 url 패턴에 맞는 모든 요청에 대해 부가작업을 처리할 수 있는 기능을 제공한다. 디스패처 서블릿은 스프링의 가장 앞단에 존재한며, 필터는 스프링 범위 밖에서 처리된다.

즉, 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너가 관리하며 디스패치 서블릿 전/후에 작업이 일어난다. 그러나 필터 또한 스프링 빈으로 등록은 된다고 한다.

 

 

필터 사용법

필터를 사용하려면 javax.servlet의 Filter 인터페이스를 구현해야한다.

package javax.servlet;

import java.io.IOException;

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(): init 메서드는 필터 객체를 초기화하기 위한 메서드이다. 서블릿 컨테이너가 생성될 때 호출된다.
  • doFilter(): doFilter 메서드는 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
  • destroy(): 필터 종료 메서드이다. 서블릿 컨테이너가 종료될 때 호출된다.

 

 

필터 흐름

HTTP 요청 → WAS → 필터 → 디스패쳐 서블릿 → 컨트롤러

 

로그인 요청 필터 사용시

HTTP 요청 → WAS → 필터(로그인한 사용자라면) → 디스패쳐 서블릿 → 컨트롤러

HTTP 요청 → WAS → 필터(로그인한 사용자가 아니라면 서블릿 호출 X)

 

필터체인이란?

필터는 체인으로 구성되는데, 중간에 필터를 자유롭게 추가할 수 있다. 예를 들어서 로그를 남기는 필터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 필터를 만들 수 있다.

 

필터 체인 사용 예시

HTTP 요청 → WAS → 필터1 → 필터2 → 필터3 → 디스패쳐 서블릿 → 컨트롤러

필터1에서 chain.doFilter(request, response) 메서드를 호출한다.

다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. 만약 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다.

 

 

필터 사용 예제

필터 객체 생성

@Slf4j
public class CustomLogFilter 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 httpServletRequest = (HttpServletRequest) request;
        String requestURI = httpServletRequest.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");
    }
}
  • doFilter():
    • HTTP요청이 들어오면 doFilter가 호출된다.
    • ServletRequest는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이다. HTTP를 사용하면 다운 캐스팅 해준다.
    • chain.doFilter(): 다음 필터가 있으면 다음 필터를 호출한다.

필터 빈으로 등록하기

@Configuration
public class CustomWebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new CustomLogFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");
        return filterFilterRegistrationBean;
    }
}

생성한 필터를 등록하는 방법에는 여러가지가 있다.

스프링 부트를 사용한다면 FilterRegistrationBean을 사용하면 된다.

  • setFilter(): 등록할 필터를 지정한다.
  • setOrder(1): 필터는 체인으로 동작하기 때문에, 낮을 수록 먼저 동작한다.
  • addUrlPatterns(”/*”): 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.

init():

다음과 같이 SpringApplication을 구동하고 서블릿컨테이너가 띄워지면 init() 메서드가 실행되며 로그가 남겨지는 것을 볼 수 있다.

doFilter():

destory():

스프링 어플리케이션을 종료하면 다음과 같이 destory 메서드가 실행되는 것을 확인할 수 있다.

필터만 제공하는 기능

chain.doFilter(request, response); 를 호출해서 다음 필터 또는 서블릿을 호출할 때 request, response를 다른 객체로 바꿀 수 있다. ServletRequest, ServletResponse를 구현한 다른 객체를 만들어서 넘기면 해당 객체가 다음 필터 또는 서블릿에서 사용된다. 잘 사용하는 기능은 아니라고 한다.

 

 

 

인터셉터(Interceptor)란?

스프링 인터셉터도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기능이다. 서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링이 제공하는 기술이다.

스프링 인터셉터는 디스패처 서블릿을 지난다음 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공한다.

디스패처 서블릿이 핸들러 매핑을 통해 요청에 대한 컨트롤러를 찾으면, 그 결과로 실행 체인(HandlerExecutionChain)을 돌려준다. 이 실행 체인에 1개 이상의 인터셉터가 등록되어 있으면 순차적으로 인터셉터를 거쳐 컨트롤러가 실행되고, 인터셉터가 없다면 바로 컨트롤러를 실행한다.

 

 

인터셉터 흐름

HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러

  • 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출 된다.

 

인터셉터 사용법

인터셉터를 사용하려면 org.springframework.web.servlet의 HandlerInterceptor 인터페이스를 구현해야한다.

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

}

HandlerInterceptor 인터페이스에는 세가지 메서드가 있다.

  • preHandle: 컨트롤러 호출 전에 호출된다.(핸들러 어댑터 호출 전에 호출된다.)
    • preHandle 응답 값이 true이면 다음으로 진행하고, false면 더는 진행하지 않는다. false인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다.
  • postHandle: 컨트롤러 호출 후에 호출된다. (핸들러 어댑터 호출 후에 호출된다.)
    • 컨트롤러 이후에 처리해야하는 후처리 작업이 있을 때 사용할 수 있다.
    • 컨트롤러 하위 계층에서 작업을 진행하다가 중간에 예외가 발생하면 postHandle은 호출되지 않는다.
  • afterCompletion: 뷰가 렌더링 된 이후에 호출된다.
    • 요청 처리 중에 사용한 리소스를 반환할 때 사용하기에 적합하다.
    • postHandler과 달리 컨트롤러 하위 계층에서 작업을 진행하다가 중간에 예외가 발생하더라도 afterCompletion은 반드시 호출된다.
    • 이 경우 예외를 파라미터로 받아서 어떤 예외가 발생했는지 로그를 출력할 수 있다.

 

 

로그인 요청 인터셉터 사용 시

HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러(로그인 사용자라면)

HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터(비로그인 시 적절하지 않은 요청이라 판단, 컨트롤러를 호출하지 않는다.)

 

스프링 인터셉터 체인

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

 

 

인터셉터 사용 예제

인터셉터 객체 생성

@Slf4j
public class CustomLogInterceptor 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);
        
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;
        }
        
        log.info("REQUEST(Interceptor)  [{}][{}][{}]", uuid, requestURI, handler);
        return true;
    }

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

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        String requestURI = request.getRequestURI();
        String uuid = (String) request.getAttribute(LOG_ID);
        
        log.info("RESPONSE(Interceptor) [{}][{}]", uuid, requestURI);
        
        if (ex !=  null) {
            log.error("afterCompletion Error!", ex);
        }
    }
}
  • 종료로그를 postHandle이 아니라 afterCompletion에서 실행한 이유?
    • 예외가 발생한 경우 postHandler가 실행되지 않기 때문에.
    • atferCompletion은 예외가 발생해도 호출 되는 것을 보장한다.
  • HandlerMethod
    • 핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다.
    • 스프링은 @Controller, @RequestMapping을 활용한 핸들러 매핑을 사용하는데, 이 경우 핸들러 정보로 HandlerMethod가 넘어온다.
  • ResourceHttpRequestHandler
    • @Controller가 아니라 /resource/static와 같은 정적 리소스가 호출되는 경우 ResourceHttpRequestHandler가 핸들러 정보로 넘어오기 때문에 타입에 따라서 처리가 필요하다.
  • request.setAttribute(LOG_ID, uuid)
    • Interceptor는 싱글톤처럼 사용되기 때문에 멤버변수를 사용하면 위험하다. 따라서 request에 담아 두었다가 꺼내 사용한다.

 

 

인터셉터 빈으로 등록하기

@Configuration
public class CustomWebConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CustomLogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");
    }
}
  • WebConfigurer가 제공하는 addInterceptors를 사용하여 인터셉터를 등록할 수 있다.
  • addPathPatterns: 인터셉터를 적용할 URL 패턴을 지정한다.
  • excludePathPatterns: 인터셉터에서 제외할 패턴을 지정한다.

 

필터와 비교해보면 인터셉터는 addPathPatterns, excludePathPatterns로 매우 정밀하게 URL패턴을 지정해 줄 수 있다.

preHandle():

postHandle() & afterCompletion():

 

Filter vs Interceptor 차이

필터(Filter) 인터셉터(Interceptor)

컨테이너 웹 컨테이너 스프링 컨테이너
Request/Response 객체 조작 가능여부 O X

Request/Response 객체 조작 가능여부란?

  • 필터의 경우 chain.doFilter(request, response)에 request와 response 객체를 넣어줄 수 있다.
  • 인터셉터의 경우 preHandle의 반환값은 boolean으로 request, response 객체를 넘겨줄 수 없다.

 

 

 

reference

https://mangkyu.tistory.com/173

인프런 - 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술