Servlet Filter & Spring Interceptor
Servlet Filter와 Spring Interceptor는 웹과 관련된 공통 관심 사항에 대한 내용을 처리하기 위한 기능이며, 차이점은 어디서 제공하는 기능이냐? 적용되는 위치가 어디냐?로 나누어 볼 수 있다.
여기서 공통 관심 사항이란 단어를 보고서 AOP(Aspect Oriented Programming)를 떠올릴 수 있겠으나 웹과 관련된 공통 관심 사항의 경우에는 HTTP header나 URL 정보 등이 필요한데 이러한 정보를 FIlter 및 Interceptor에서 제공하기 때문에 이를 이용하는 것이 좋다.
웹과 관련된 공통 관심 사항의 대표적인 예시로는 어떠한 요청이 들어왔는지를 기록하기 위한 로깅 기능 또는 특정 요청에는 인증/인가된 유저만 접근이 가능하도록 하는 인증/인가 기능이 있다.
Servlet Filter
이름에서 알 수 있듯이 Filter는 Servlet에서 제공하는 기능이며, HTTP 요청에 대해 다음과 같은 흐름을 통해 적용되게 된다.
WAS로 들어온 HTTP 요청을 Servlet으로 전달하는 흐름 중간에 Filter가 들어가게 되며, HTTP 요청에 대한 공통 관심 사항 로직을 수행하게 된다. 만약 인증/인가 기능의 필터를 적용했는데 인증되지 않은 유저가 인증이 필요한 요청을 보냈다면, 해당 요청은 필터에서 구현한 로직에 따라 동작하게 되며(로그인 페이지로 리다이렉션 시킨다던가 하는 동작) Servlet까지 전달되지 않는다.
이러한 Filter는 여러개의 체인으로 구성될 수 있으며, 이를 이미지로 본다면 다음과 같다.
HTTP 요청은 구성된 필터 체인에 대해 모두 만족되어야 Servlet으로 전달되어 비즈니스 로직을 수행하게 된다.
이러한 Servlet Filter를 구현하는 방법은 "javax.servlet.Filter"(Spring boot 2.x까지) 또는 "jakarta.servlet.Filter"(Spring boot 3.x 이상) 인터페이스를 상속받아 구현 후 "FilterRegistrationBean"에 추가하여 등록하면 된다.
Filter interface에 대한 내용 및 필터 등록 과정은 "모든 HTTP 요청에 대해 로그를 남기는 필터 구현 및 등록" 예제 코드를 통해 설명한다.
예제 코드 (코드까지 들어가면 너무 길어져서 접어뒀습니다.)
모든 HTTP 요청에 대해 로그를 남기는 Filter 코드
import java.io.IOException;
import java.util.UUID;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HttpLogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Filter 초기화 메서드, 해당 메서드는 default 키워드가 적용되어 있기 때문에 생략 가능
log.info("HttpLogFilter call init()");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 필터의 로직을 구현해야 하는 메서드
log.info("HttpLogFilter call doFilter()");
// 각각의 HTTP 요청을 확인하기 위한 uuid 값 생성
String uuid = UUID.randomUUID().toString();
// HTTP 요청을 보낸 URI 확인
String requestURI = ((HttpServletRequest) request).getRequestURI();
try {
// 동작 로직 구현
log.info("# HTTP [{}] URI : [{}] Request", uuid, requestURI);
// 다음 필터 체인 수행 코드, 다음 필터 체인이 없다면 Servlet으로 전달
// 해당 코드가 없다면 더 이상 진행이 안되고 종료됨
chain.doFilter(request, response);
// 로직이 정상적으로 종료되었다는 이야기 => 반환(Response)
log.info("# HTTP [{}] URI : [{}] Response", uuid, requestURI);
} catch (Exception e) {
// 이후 동작에서 예외 발생 시 WAS로 예외를 전달하는 역할
throw e;
} finally {
// try, catch 이후 동작
log.info("# HTTP [{}] URI : [{}] Request finish", uuid, requestURI);
}
}
@Override
public void destroy() {
// Filter 종료 메서드, Servlet container 종료 시점에 호출됨
// init() 메서드와 동일하게 default 키워드가 적용되어 있기 때문에 생략 가능
log.info("HttpLogFilter call destroy()");
}
}
Filter를 등록하는 코드 (아래 방법 말고 @Order 어노테이션으로 간단하게 등록하는 방법도 존재)
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean httpLogFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
// 위에서 구현한 로그 필터를 적용
filterRegistrationBean.setFilter(new HttpLogFilter());
// 해당 필터의 동작 순서를 설정 (필터 체인에서 몇 번째로 동작할 필터인지 지정)
filterRegistrationBean.setOrder(1);
// 해당 필터를 적용 할 요청에 대해 설정, 여기선 모든 요청에 적용
filterRegistrationBean.setUrlPatterns("/*");
return filterRegistrationBean;
}
사실 Servlet Filter는 Bean으로 등록하지 못했었는데, 이는 어찌 보면 당연한 내용이다.
Servlet의 범위는 Spring framework 보다 밖에 위치하기 때문에 Spring framework가 Servlet 범위에서 동작하는 Filter를 Bean으로 등록할 수 없는 것 (Servlet 범위 안에서 Spring framework가 동작하는 것)
하지만 Filter에도 DI와 같은 Spring 기술이 필요했으며 이를 해결하기 위한 방법으로 "DelegatingFilterProxy"라는 것이 등장하였고, 이를 사용하여 Filter 로직 내에서 DI 사용 및 Filter를 Bean으로 등록하여 사용할 수 있게 되었다.
DelegatingFilterProxy 동작 간단 설명
- Spring framework에서 구현한 Filter를 Bean으로 등록
- 해당 Filter를 구현체로 갖는 DelegatingFilterProxy를 생성
- ServletContext가 DelegatingFilterProxy를 Servlet Container에 등록
- DelegatingFilterProxy에 요청이 들어오면 구현체인 Filter(Spring bean)에 위임하여 필터 처리 진행
그리고 Spring boot 등장 이후 내장 웹서버를 지원하게 되면서 Servelet Container에 대한 제어가 가능해지면서 DelegatingFilterProxy를 사용하지 않고도 바로 Filter를 등록하여 사용할 수 있게 되었다. (위 예제 코드가 해당 방법)
Spring Interceptor
이름에서 알 수 있듯이 Interceptor는 Spring에서 제공하는 기능이며, HTTP 요청에 대해 다음과 같은 흐름을 통해 적용되게 된다.
Spring Interceptor의 경우 Spring에서 제공하는 기능이기에 Servlet에서 Controller로 넘어가는 흐름 중간에서 로직을 수행하게 된다.
그리고 Interceptor 또한 Filter와 동일하게 체인으로 구성될 수 있으며, 기본적으로 Filter와 동작 방식이 동일하다고 볼 수 있다.
Spring Interceptor를 구현하는 방법은 HandlerInterceptor interface를 구현하면 되는데, 해당 인터페이스를 보면 3개의 메서드가 존재한다는 것을 알 수 있다. 이 3개의 메서드들은 전부 default 키워드가 적용되어 있기 때문에 필요한 메서드만 오버라이딩하여 구현 후 WebMvcConfigurer interface가 제공하는 addInterceptors() 메서드를 통해 등록하면 된다.
각 메서드별 동작에 대한 설명은 Servlet Filter와 동일한 예제 코드를 통해 설명합니다.
예제 코드
모든 HTTP 요청에 대한 로그를 남기는 Interceptor 코드
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Slf4j
public class HttpLogInterceptor implements HandlerInterceptor {
public static final String HTTP_LOG_UUID_ID = "httpLogUuid";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Controller 호출 전에 호출되는 메서드
String requestURI = request.getRequestURI();
String logId = UUID.randomUUID().toString();
// 해당 HTTP 요청에 대한 UUID 값을 컨트롤러 이후에도 사용하기 위해 등록
request.setAttribute(HTTP_LOG_UUID_ID, logId);
log.info("# HTTP [{}] URI : [{}] Request", logId, requestURI);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 컨트롤러 종료 후 호출되는 메서드, 컨트롤러에서 예외 발생시 호출 안됨
String requestURI = request.getRequestURI();
String logId = (String) request.getAttribute(HTTP_LOG_UUID_ID);
// 예외가 없었다면 로깅
log.info("# HTTP [{}] URI : [{}] Response ModelAndView : [{}]", logId, requestURI, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 컨트롤러 종료 후 호출되는 메서드, 컨트롤러에서 예외 발생시에도 호출됨
// 컨트롤러에서 예외가 발생된 경우 해당 예외가 파라미터로 넘어옴
String requestURI = request.getRequestURI();
String logId = (String) request.getAttribute(HTTP_LOG_UUID_ID);
// 만약 예외가 넘어왔다면 어떤 예외가 발생했는지 로깅
if (ex != null) {
log.error("HTTP [{}] Error!!", logId, ex);
} else {
// 예외가 발생하지 않았다면 일반 로그 남김
log.info("# HTTP [{}] URI : [{}] Request finish", logId, requestURI);
}
}
}
Interceptor를 등록하는 코드
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 위에서 구현한 로그 인터셉터를 적용
registry.addInterceptor(new HttpLogInterceptor)
.order(1) // 해당 인터셉터의 동작 순서를 설정 (인터셉터 체인에서 몇 번째로 동작할지 지정)
.addPathPatterns("/**") // 해당 인터셉터를 적용 할 요청에 대해 설정, 여기선 모든 요청에 적용
.excludePathPatterns("/*.ico", "/css/**"); // 해당 인터셉터를 적용하지 않을 요청에 대해 설정
// 여기선 ".ico"로 끝나는 요청 및 "/css/"에 대한 모든 요청에 대해선 미적용 하도록 설정
// 즉, 모든 설정에 적용을 하지만 ".ico"로 끝나거나 "/css/"에 대한 요청은 제외시킨다
// 여러개의 interceptor 등록은 아래와 같이 추가로 등록하여 해결
// registry.addInterceptor(new MoreInterceptor)
// .order(2)
// .addPathPatterns("/**")
// .excludePathPatterns("/*.ico", "/css/**", "/error", ...);
}
}
Servlet Filter에 비해 Spring Interceptor는 로직 구현을 위한 메서드들이 호출 시점에 따라 명확하게 나누어져 있음을 알 수 있으며, 이렇게 구현한 Interceptor 로직을 어떠한 요청에 적용할지에 대한 설정을 매우 정밀하고 쉽게 할 수 있다. (제외할 요청 설정도 쉬움)
두 기능 중 무엇을 사용해야 하는가?
두 기능은 동일한 동작을 수행하는 한다고 볼 수 있다. 즉, 공통 관심 사항에 대한 기능을 구현할 때 Filter와 Interceptor 중 한 가지를 선택하여 구현 후 적용시키면 된다는 것이다.
그렇다면 둘 중 어떤 기능을 선택해야 하는지에 대한 의문이 생기게 될 텐데 이는 어떠한 기능을 구현하고자 하는지에 따라서 달라진다.
각각의 기능을 제공하는 제공자와 동작 위치를 보면 어느 정도 예상을 해볼 수 있을 수 있는데, Spring과 관련 없이 Web 전체적인 동작에 대한 로직이라면 Servelt Filter를 이용해 구현하는 것이 좋을 것이고 Spring 내부의 동작과 연관이 있는 로직이라면 Spring Interceptor를 이용해 구현하는 것이 좋을 것이다.
또한 위에서 언급하지 않았던 큰 차이점이 있는데 Servlet Filter의 doFilter() 메서드를 떠올려보자.
doFilter() 메서드는 해당 필터의 로직을 구현한 후에 로직이 정상적으로 끝이 났다면 필수로 "chain.doFilter(ServletRequest, ServletResponse)" 메서드를 호출해 줘야 다음 필터 체인으로 넘어가면서 문제없이 동작하게 된다.
즉, Spring Interceptor와는 다르게 자동으로 다음 체인으로 넘어가는 것이 아닌 개발자가 직접 "chain.doFilter()" 메서드를 호출해 줘야 다음 체인으로 넘어간다는 차이점을 알 수 있다. 이 차이점은 다음 체인에 새로운 ServletReqeust와 ServletResponse를 넘길 수 있냐 없냐를 가르는 차이를 만든다.
이러한 차이들을 생각하여 구현하고자 하는 기능이 어떤 영역에서 동작되어야 하는지 판단 후 선택하면 된다.
(참고로 Spring Security의 경우 Servlet Filter를 사용하여 동작)