사용자의 요청이 발생하면 서블릿에 도달하여 적절한 형태로 처리됩니다. 하지만 여러 서블릿에서 공통적으로 처리하고 싶은 작업이 있을 수 있습니다. 그렇다면 매번 중복 로직을 각각의 서블릿에 작성해주어야 할까요?
이를 해결하기 위해 서블릿 API 2.3부터는 필터(Filter)라는 기능을 지원합니다. 필터는 HTTP 요청 및 응답에 대해 쉽게 전/후 처리를 할 수 있는 기술입니다. 예를 들어 HTTP 요청 및 응답에 공통적으로 어떤 헤더를 삽입하고 싶다면 필터를 사용할 수 있습니다.
필터 사용 예시
필터는 다음과 같은 기능들을 구현하는데 주로 사용되곤 합니다.
- 인증 필터
- 로깅 및 감시(Auditing) 필터
- 이미지 변환 필터
- 데이터 압축 필터
- 암호화 필터
필터 인터페이스
필터를 사용하고자 한다면 javax.servlet.Filter(혹은 jakarta.servlet.Filter) 인터페이스를 구현해야 합니다. 필터 인터페이스는 다음과 같이 세 가지 메소드로 구성됩니다.
package jakarta.servlet;
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
default void destroy() {
}
}
필터를 만들고자 하는 경우에는 위 필터 인터페이스를 구현하면 됩니다. 다만 구현한다고 해서 필터가 자동으로 적용되는 것은 아니고, 두 가지 방법 중 하나를 선택해서 필터를 등록할 수 있습니다. 첫번째 방법은 web.xml에 정의하는 것이고, 두번째 방법은 @WebFilter 어노테이션을 사용하는 것입니다.
@WebFilter 어노테이션 같은 경우에는 서블릿 API 3.0부터 지원하는 기능이기 때문에, 필터가 등장했을 당시(서블릿 API 2.3)에는 모두 web.xml로 등록을 했다고 봐도 무방할 것 같습니다.
이제 각각의 메소드(init, doFilter, destroy)가 어떤 역할을 하는지 차례대로 알아보도록 하겠습니다.
init
default void init(FilterConfig filterConfig) throws ServletException {
}
필터가 인스턴스화 되고 나면 웹 컨테이너는 각각의 필터에 대해 init 메소드를 한번씩 호출합니다. init 메소드가 호출된 필터는 필터링을 수행하기에 적합한 상태가 되어 있어야 합니다.
인자로는 FilterConfig라는 객체를 받는데, 이는 필터에 관련한 설정 값들을 다루는 객체이며 내부적으로는 서블릿 컨텍스트, 필터 이름, 설정 시 사용한 파라미터 목록 등을 저장하고 있습니다. init 메소드에서 필터를 초기화할 때 이런 설정 값들을 사용할 수 있습니다.
다만 유의해서 보셔야 할 점은, init은 default로 선언된 메소드이기 때문에 필터 구현체가 반드시 오버라이딩 할 필요는 없으며 오버라이딩 하지 않는 경우에는 기본적으로 아무 일도 수행하지 않습니다(NO-OP).
doFilter
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
클라이언트가 어떠한 요청을 보냈을 때 서블릿 컨테이너에 의해 호출이 되는 메소드입니다. 실제 필터링을 하는 기능을 담당하고 있으며, 인자로 ServletRequest, ServletResponse, FilterChain을 받습니다.
FilterChain이라는 객체를 받는 이유는 필터로 하여금 스스로 다음 필터를 호출할 수 있도록 하기 위함입니다. FilterChain에는 아래와 같이 doFilter 메소드만 존재하는데, 해당 메소드를 실행시켜서 다음 필터에게 처리를 맡길 수도 있고, 마지막 필터라면 서블릿에게 요청을 전달할 수 있습니다.
package jakarta.servlet;
public interface FilterChain {
void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException;
}
이렇게 필터는 FilterChain을 기반으로 다음 필터 및 서블릿을 스스로 호출할 수 있는 구조이므로, doFilter 로직 처리 중 문제가 생기면 요청을 다음 필터에게 넘기지 않고 요청 자체가 다음 필터 및 서블릿에 도달하지 않도록 블로킹하는 행위도 가능해집니다.
package org.apache.catalina.core;
public class ApplicationFilterChain implements FilterChain {
// 필터 목록
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
private void internalDoFilter(ServletRequest request, ServletResponse response) {
// 코드 일부 생략
// FilterChain의 끝에 도달했을 때, 서블릿을 호출한다
servlet.service(request, response);
// 코드 일부 생략
}
}
위 코드는 Tomcat에서 사용하는 FilterChain 구현체인 ApplicationFilterChain의 코드 중 일부입니다. 내부적으로는 모든 필터들에 대한 참조를 배열로 갖고 있고, internalDoFilter 메소드에서는 다음과 같이 FilterChain의 끝에 도달했는지를 확인해서 서블릿을 호출하는 작업을 하고 있습니다.
쉽게 설명하기 위해 예시 코드를 간소화했으니 참고해주시면 감사하겠습니다.
destroy
default void destroy() {
}
모든 스레드가 해당 필터를 사용하고 있지 않거나 타임아웃 시간 제한을 넘어서 유휴 상태로 존재하는 경우 서블릿 컨테이너는 destory를 호출합니다. 한 번 destroy가 된 필터는 doFilter를 호출할 수 없게 됩니다. destroy 메소드에는 일반적으로 메모리나 스레드 등의 자원을 정리하는 작업이 포함되게 됩니다.
init 메소드와 마찬가지로 디폴트 메소드로 선언이 되었기 때문에 오버라이딩 하지 않는다면 기본적으로 아무 일도 수행하지 않습니다.
필터 만들어보기
이번에는 필터를 직접 만들어보겠습니다. 필터는 서블릿 컨테이너 환경에서 동작하기 때문에 스프링 부트 환경에서 실습을 진행하도록 하겠습니다.
앞서서 필터를 등록하는 방법은 web.xml을 작성하거나 @WebFilter를 사용하는 두 가지 방법이 있다고 이야기했습니다. 이번 실습에서는 @WebFilter를 사용해서 필터를 등록합니다. 스프링 부트에서 필터를 등록하려는 경우에는 추가로 @Component 어노테이션을 붙여주기만 하면 됩니다.
@WebFilter(urlPatterns = "/*")
@Component
public class ALoggingFilter implements Filter {
private final Logger logger = LoggerFactory.getLogger("ALoggingFilter");
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 전처리
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String httpMethod = httpServletRequest.getMethod();
String requestURI = httpServletRequest.getRequestURI();
String protocol = httpServletRequest.getProtocol();
logger.info("{} {} {}", httpMethod, requestURI, protocol);
chain.doFilter(request, response);
// 후처리
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
int status = httpServletResponse.getStatus();
logger.info("status code = {}", status);
}
}
간단하게 모든 요청에 대해 로깅을 수행하는 예제 코드입니다. 따로 준비작업과 자원 반납 과정이 필요하지 않기에 init 메소드와 destroy 메소드는 오버라이딩하지 않았습니다. 요청이 들어오면 MyLoggingFilter는 HTTP 요청의 request line을 로깅합니다. 그리고 FilterChain의 doFilter 메소드를 호출해 다음 필터 혹은 서블릿이 요청을 처리할 수 있도록 합니다. 모든 요청이 처리된 이후에는 status code를 로깅합니다.
어플리케이션을 실행시키면 urlPatterns를 "/*'로 등록했으므로 모든 요청 및 응답에 대해서 로깅을 수행하는 것을 확인할 수 있습니다.
필터의 순서 보장
필터 순서가 보장되어야 할 때가 있는데, 필터는 어떻게 등록하느냐에 따라 순서를 조정하기 어려울 수도 있으므로 유의해야 합니다. web.xml로 등록하는 경우에는 선언된 순서대로 필터가 작동하지만, @WebFilter로 등록하는 경우에는 순서를 조작할 수 없습니다.
스프링 부트에서는 기본적으로 알파벳 순으로 필터가 등록되며, 이를 조정하고 싶은 경우에는 @Order 어노테이션을 활용하거나 FilterRegistrationBean을 활용하면 됩니다.
마무리
필터에 대한 기본 개념과 어떻게 사용할 수 있는지까지 알아보았습니다. 사실 필터는 서블릿 기술이기 때문에 스프링 부트에서 사용할 수 있으려면 추가적인 메커니즘이 필요한데(DelegatingFilterProxy) 이에 대해서는 추후 따로 정리하도록 하겠습니다.
참고자료
Java™ Servlet Specification Version 2.3
'Spring' 카테고리의 다른 글
쉽게 이해해보는 DispatcherServlet (4) | 2023.10.07 |
---|---|
CGI에서부터 Spring에 이르기까지 (2) | 2023.09.19 |
Query Parameter, Path Variable은 무엇이고 언제 사용할까? (1) | 2023.05.07 |
Spring Interceptor (8) | 2023.05.03 |
ResponseEntity, RequestEntity, HttpEntity (1) | 2023.05.01 |