Spring Interceptor와 Servlet Filter 모두 공통 관심사항을 처리할 수 있는 유용한 기능이다.
예를 들어 로그인이 꼭 필요한 서비스에서 로그인 여부를 체크하는 작업이 필요하다고 할 때, 각 컨트롤러에서 보일러 플레이트 코드를 반복해서 작성해야 한다는 불편한 점이 존재한다.
이러한 횡단 관심사(cross-cutting concern)는 스프링에서 지원하는 AOP
를 사용하여 해결할 수도 있지만 웹과 관련된 공통 관심사는 HTTP header 정보, URL 정보등이 필요한 경우가 많기에 이를 지원하는 서블릿 필터 혹은 스프링 인터셉터를 사용하는 것이 좋다.
서블릿 필터와 스프링 인터셉터는 HttpServletRequest
를 포함한 여러 부가 기능을 지원하기에 특정 URL
을 블랙 리스트 혹은 화이트 리스트로 추가하여 필터링을 수행하거나 하지 않게 할수 있다.
Servlet Filter
서블릿 필터는 WAS
↔ Servlet
사이에 동작한다. WAS가 Servlet을 호출하기 전 필터를 먼저 호출하여 필터에 선언된 동작을 수행한다. 필터는 특정 URL을 패턴으로 등록할 수 있다. (/*
)
필터에서 적절하지 않은 요청이라고 판단했을 때, 서블릿으로 접근을 막고 바로 응답할 수 있다.
필터는 체인으로 구성된다. 여러 필터가 체인을 구성하고, 연속적으로 호출된다. 체인 사이에 필터를 자유롭게 추가할 수 있기에 순서 변경에 유연하다.
필터를 구현하려면 jakarta.servlet
의 Filter
인터페이스를 구현하면 된다.
public interface Filter {
// 서블릿 컨테이너가 생성될 때 호출되어 필터를 등록한다.
default void init(FilterConfig filterConfig) throws ServletException {
}
// doFilter를 통해 필터를 호출하고 필터 체인을 따라 연쇄적으로 필터를 호출한다.
// 필터의 로직을 구현한다
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
// 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
default void destroy() {
}
}
필터 인터페이스를 구현하면 서블릿 컨테이너가 필터를 singleton 객체로 생성하고 관리한다. 즉 Stateless와 read-only를 유지하는 것이 안전하다.
간단한 로그 필터를 만들어보자. 먼저 아래 처럼 jakarta.servlet.Filter
를 구현하는 필터를 생성한다.
@Slf4j
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("log filter call doFilter");
HttpServletRequest servletRequest = (HttpServletRequest) request;
String requestURI = servletRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try {
log.info("REQUEST [{}][{}]", uuid, requestURI);
chain.doFilter(request, response); // 없으면 서블릿 호출이 안됨
} catch (Exception e) {
} finally {
log.info("RESPONSE [{}][{}]", uuid, requestURI);
}
}
}
Configuration
클래스에서 FilterRegistrationBean
을 사용하여 빈으로 등록하면 필터가 등록된다.
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean<Filter> logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
setOrder
는 필터 체인에서 등록하고자 하는 필터의 순서를 의미한다. default 값은 Integer.MAX_VALUE
, 가장 후순위의 필터로 등록된다. 가장 높은 우선 순위의 값은 Integer.MIN_VALUE
이다.
실험해보니 여러 개의 필터를 등록했는데 모두 순위를 지정해주지 않으면 빈으로 등록되는 순서에 따라 동작하는 것 같다.
Spring Interceptor
Spring Interceptor는 Spring mvc가 제공하는 Servlet Filter와 유사한 기능이다. 웹과 관련된 횡단 관심사를 처리한다는 공통점이 존재하지만, 적용되는 순서와 범위, 사용방법이 다르다. Interceptor가 Filter보다 더 많은 기능을 제공한다.
Interceptor 또한 Singleton으로 관리되기 때문에 stateless, read-only를 유지하는 것이 좋다. 상태를 사용할 때는 동시성 이슈를 잘 고려해야 한다. (Concurrent Collections, Atomic을 사용하자)
Interceptor는 Spring의 Dispatcher Servlet과 Controller사이에 호출된다.
Interceptor를 구현하려면 org.springframework.web.servlet
패키지의 HandlerInterceptor
인터페이스를 구현하면 된다.
public interface HandlerInterceptor {
// 컨트롤러(정확하게는 핸들러 어댑터) 호출 전
// true를 반환하면 다음 interceptor 혹은 핸들러 어댑터를 호출
// false를 반환하면 인터셉터 및 핸들러 호출하지 않음
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
// 컨트롤러 호출 후 - 핸들러(컨트롤러)가 ModelAndView를 반환한 후
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
// HTTP 요청 완료 이후 - 뷰 랜더링 이후
// 핸들러에서 예외가 발생하더라도 항상 호출됨
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
간단한 로그 필터를 만들어보자. 먼저 아래 처럼 HandlerInterceptor
를 구현하는 클래스를 생성한다.
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
private 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);
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
String uuid = (String) request.getAttribute("logId");
log.info("REQUEST [{}][{}][{}]", uuid, request.getRequestURI(), handler);
}
}
이제 빈으로 등록해야 하는데 Filter와는 방법이 조금 다르다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
}
Configuration 클래스에서 WebMvcConfigurer를 implements하고 addInterceptors 메서드를 오버라이딩 한다. 위 코드처럼 스프링에서 제공하는 형식에 맞추어 인터셉터를 등록하면 잘 동작한다.
차이점
Servlet Filter는 Spring Intercepter가 지원하지 않는 강력한 기능이 존재한다.
바로 chain.doFilter(requset, response)
메서드 이다. 해당 메서드를 사용하여 다음 필터 혹은 서블릿을 호출할 때, request(ServletRequest
), response(ServletResponse
) 객체를 바꿀 수 있다.
Spring Interceptor는 DispatcherServlet 접근 이후에 동작하기 때문에 Handler(Controller)의 정보, Handler(정확하게는 HandlerAdapter)가 반환하는 ModelAndView 그리고 발생한 오류와 응답 이후의 정보까지 접근할 수 있다. 또한 URL패턴을 Filter보다 정밀하게 지정할 수 있다.
나는 일반적으로 Spring 프로젝트에서 예외를 핸들링하는 데 있어 ControllerAdvice를 사용한다. Filter에서 예외를 던지면 ControllerAdvice에서 캐치할 수 없다. Filter는 SpringContext 외부에서 동작하기 때문에 ControllerAdvice가 이를 확인할 수 없는 것이다. 따라서 ControllerAdvice로 예외를 핸들링하길 원한다면 Interceptor를 사용해야 한다. 만약 Filter를 통해 예외를 처리하고 싶다면 doFilter의 인자로 넘어오는 ServletResponse 객체를 사용해야 한다.
Reference
'Spring' 카테고리의 다른 글
주문관리 애플리케이션 S.A (1) | 2024.11.07 |
---|---|
Spring 예외 핸들링 전략 (4) | 2024.03.08 |
SLF4J 와 Log Level 간단 정리 (0) | 2024.03.03 |
[Spring Boot] JPA - MySql 연결 (0) | 2023.03.02 |
[Spring Boot] ThymeLeaf 템플릿 사용 (0) | 2023.03.02 |