필터와 인터셉터 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편 - 백엔드 웹 개발 활용 기술
'Spring' 카테고리의 다른 글
[Spring] DI 의존성 주입 방식과 생성자 주입을 사용해야 하는 이유 (0) | 2022.11.22 |
---|---|
[Spring] ArgumentResolver란? (0) | 2022.11.18 |
[Spring] @Transactional의 트랜잭션 전파레벨 (0) | 2022.11.17 |
[Spring] SpringBootTest 테스트 격리 방법 (0) | 2022.11.15 |
[Spring] Spring Bean Validation의 @Valid vs @Validated (0) | 2022.11.14 |