이번 포스트에서는 Spring Filter에 대한 심화적인 이해를 해보도록 하자
📌 서론
Spring Framework에서 필터는 웹 애플리케이션의 요청과 응답을 조작하는 강력한 도구이다.
필터는 DispatcherServlet으로 가는 요청의 사전 처리와 후처리를 담당한다. 이를 통해 요청과 응답에 대한 중앙집중식 처리가 가능해진다. 필터는 doFilter() 메서드를 통해 요청을 가로채고, 필요한 로직을 실행한 뒤, 요청을 다음 목적지(다른 필터 또는 서블릿)로 전달한다.
지금부터 필터의 핵심 메서드인 doFilter에 대해 상세히 살펴보고, 실제 HTTP 요청 처리 예시를 통해 그 사용법을 알아보자
필터에 대한 자세한 내용은 아래의 포스트를 보고오자
1. doFilter 메서드 이해하기
필터 메서드 살펴보기
public class CustomFilter implements Filter {
@Override
public void doFilter(
ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain
) throws IOException, ServletException {
filterChain.doFilter(servletRequest, servletResponse);
}
}
메서드 동작 순서
- 요청 가로채기: doFilter() 메서드가 요청을 가로채서 필요한 처리를 한다.
- 요청 처리: 요청(servletRequest)에 대한 검증이나 변형 등을 수행한다.
- 체인을 통한 요청 전달: filterChain.doFilter()를 호출하여 다음 단계로 요청을 넘긴다.
- 응답 가로채기: 필터(Filter)는 응답(servletResponse)을 가로채어 추가적인 처리를 할 수 있다.
- 응답 전송: 최종적으로 응답(servletResponse)이 클라이언트에게 전송된다.
2. Spring(Java)에서 Filter 구현해 보기 - doFilter 구현하기
클래스 선언하고 Filter 상속받기
- SpringFilterExample이라는 클래스를 만들고 Filter를 implements 한다. 여기서 Filter는 jakarta.servlet을 선택해야 한다.
구현할 클래스 확인
- Filter를 상속받은 후 implement 할 수 있는 메서드를 확인하면 3개가 존재한다. 바로 doFilter, init, destroy이다.
구현을 한다
- 위의 3개의 메서드를 구현하도록 한다. 처음에는 아무것도 없으니 필요한 로직을 직접 적어주면 된다.
로깅 예시
- 아래는 간단한 로깅 필터의 예시다. 이 필터는 요청 정보를 로그로 기록하고, 요청 처리 시간을 계산해서 다시 로그로 남긴다.
- 이 코드에서 doFilter() 메소드는 요청을 받아서 처리 시작 시간을 로그로 남기고, chain.doFilter() 메소드를 통해 요청을 다음 필터나 서블릿으로 넘긴 후, 처리 완료 시간을 로그로 남겨서 요청 처리에 걸린 시간을 계산한다.
- 작성한 코드
import jakarta.servlet.*;
import java.io.IOException;
public class SpringFilterExample implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 필터 초기화 시 필요한 작업을 여기서 수행
Filter.super.init(filterConfig);
}
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException
{
long startTime = System.currentTimeMillis();
System.out.println("Request Processing Started");
chain.doFilter(request, response); // 요청을 다음 필터나 서블릿으로 넘긴다.
long endTime = System.currentTimeMillis();
System.out.println("Request Processing Completed in " + (endTime - startTime) + " ms");
}
@Override
public void destroy() {
// 필터 종료 시 필요한 작업을 여기서 수행
Filter.super.destroy();
}
}
3. doFilter 메서드가 ServletRequest를 사용하는 이유
doFilter()를 구현할 때 매게 변수로 ServletRequest 타입의 request를 가지게 된다. 여기서 의문이 생겼다. 왜 HttpServletRequest가 아니라 ServletRequest일까?
아래는 doFilter() 메서드를 구현했을 때의 모습이다. 여기서 확인할 수 있듯이 ServletRequest를 사용한다.
프로토콜 독립성
- ServletRequest 인터페이스는 HTTP 프로토콜에 국한되지 않고 다양한 프로토콜에 대한 요청을 처리할 수 있는 유연성을 제공한다. 이는 필터가 HTTP, FTP 등의 프로토콜을 사용하는 요청을 동일한 방식으로 처리할 수 있도록 해준다.
HttpServletRequest의 확장 기능
- HttpServletRequest는 ServletRequest를 확장한 인터페이스로, HTTP 프로토콜에 특화된 추가 기능을 제공한다. 이를 통해 개발자는 HTTP 요청 헤더, 쿠키, 메시지 바디 등에 쉽게 접근할 수 있다.
예시 코드
- 필터에서 HTTP에 특화된 처리가 필요할 때는 다음과 같이 ServletRequest를 HttpServletRequest로 캐스팅하여 사용할 수 있다.
- 이렇게 ServletRequest와 HttpServletRequest를 적절히 사용함으로써, 필터는 HTTP 요청뿐만 아니라 다른 프로토콜을 사용하는 요청에 대해서도 유연하게 대응할 수 있다.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// HTTP 프로토콜에 특화된 처리가 필요한 경우 캐스팅을 사용한다.
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 여기서 HTTP 요청에 특화된 작업을 수행한다.
}
chain.doFilter(request, response);
}
4. Spring Framework에서의 필터 체이닝(FilterChain)
필터 체이닝이란?
- FilterChain은 서블릿 컨테이너가 관리하는 필터들의 연결고리로, 클라이언트의 요청이 서블릿에 도달하기 전에 여러 필터를 순차적으로 거치게 하는 메커니즘이다.
필터 체이닝 설정
- 필터 체이닝의 순서는 필터를 등록하는 순서에 따라 결정되며, web.xml 파일이나 @WebFilter 어노테이션이나 WebApplicationInitializer 인터페이스를 구현하는 방식을 통해서 설정할 수 있다.
web.xml을 사용한 필터 체이닝 설정 예시
- web.xml에서는 <filter-mapping> 태그의 순서에 따라 필터의 실행 순서가 결정된다.
- 예를 들어, LoggingFilter가 AuthenticationFilter보다 먼저 요청을 처리하도록 설정할 수 있다.
<filter>
<filter-name>LoggingFilter</filter-name>
<filter-class>com.example.LoggingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>LoggingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>AuthenticationFilter</filter-name>
<filter-class>com.example.AuthenticationFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AuthenticationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
WebApplicationInitializer 인터페이스를 사용한 필터 체이닝
- 스프링 프레임워크에서는 web.xml 설정 파일 없이도 필터를 등록하고 순서를 지정할 수 있는 방법을 제공한다. 이를 위해 WebApplicationInitializer 인터페이스를 구현하는 방식을 사용할 수 있다.
- 이 인터페이스를 사용하면 서블릿 컨테이너가 시작될 때 필요한 설정을 Java 코드로 직접 작성할 수 있다.
아래의 코드에서 addFilter() 메서드를 사용하여 필터를 등록하고 있고, addMappingForUrlPatterns() 메서드는 필터가 적용될 URL 패턴을 지정한다. 필터 등록 순서에 따라 필터 체이닝 순서가 결정된다.
public class AppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// LoggingFilter 등록
FilterRegistration.Dynamic loggingFilter = servletContext.addFilter("loggingFilter", LoggingFilter.class);
loggingFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
// AuthenticationFilter 등록
FilterRegistration.Dynamic authenticationFilter = servletContext.addFilter("authenticationFilter", AuthenticationFilter.class);
authenticationFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
// 필터 등록 순서가 필터 체이닝 순서를 결정
}
}
@WebFilter 어노테이션을 사용한 필터 체이닝
- 스프링 프레임워크에서는 @WebFilter 어노테이션을 사용하여 필터를 선언적으로 등록할 수 있다.
- 이 어노테이션은 필터 클래스에 직접 적용되며, 필터의 이름과 URL 패턴을 지정할 수 있다. 하지만, 스프링 프레임워크에서는 @Order 어노테이션 같은 것으로 필터의 순서를 지정할 수 없다.
만약 필터의 순서를 지정하고 싶다면, 위에서 사용한 WebApplicationInitializer를 사용하여 필터 등록 순서를 직접 관리해야 한다. @WebFilter 어노테이션으로 등록된 필터는 서블릿 컨테이너가 자동으로 인식하지만, 순서를 지정하려면 WebApplicationInitializer에서 필터를 등록하고 순서를 명시적으로 지정해야 한다는 점을 명심해야 한다.
@WebFilter(filterName = "LoggingFilter", urlPatterns = "/*")
public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
}
@Override
public void destroy() {
Filter.super.destroy();
}
// 로깅 관련 구현 내용
}
@WebFilter(filterName = "AuthenticationFilter", urlPatterns = "/*")
public class AuthenticationFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
}
@Override
public void destroy() {
Filter.super.destroy();
}
// 인증 관련 구현 내용
}
doFilter() 메소드 내에서의 체이닝 처리방식
- doFilter() 메소드 내에서 chain.doFilter(request, response);를 호출함으로써, 현재 필터가 처리를 마치고 요청을 필터 체인의 다음 필터로 넘기게 된다.
- 아래의 코드에서 logRequest와 logResponse는 요청과 응답을 로깅하는 가상의 메서드다. 이렇게 필터 체인을 통해 요청과 응답이 순차적으로 여러 필터를 거치게 되며, 각 필터는 자신의 역할을 수행한 후 다음 필터로 요청을 넘긴다.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 요청에 대한 로깅 처리
logRequest(request);
// 다음 필터로 요청을 넘긴다.
chain.doFilter(request, response);
// 응답에 대한 로깅 처리
logResponse(response);
}
5. Spring Boot에서의 FilterChain과 필터 체이닝
Spring Boot의 필터 체이닝 개념
- Spring Boot에서 필터 체이닝은 자동 구성(auto-configuration)과 자바 기반의 설정을 통해 이루어진다.
- Spring Boot는 @SpringBootApplication 어노테이션을 통해 자동 구성을 활성화하고, 이는 내부적으로 @EnableAutoConfiguration 어노테이션을 포함하고 있다.
- 이 자동 구성은 스프링 부트가 클래스패스, 다양한 프로퍼티 설정들을 기반으로 합리적인 기본값을 적용하게 해 준다. 그래서 개발자가 필터를 직접 등록하지 않아도 스프링 부트가 자동으로 필요한 필터를 적용한다.
필터 등록 및 체이닝 설정
- Spring Boot에서는 FilterRegistrationBean을 사용하여 필터를 등록하고, 필터의 순서를 프로그래밍 방식으로 정의할 수 있다.
- 이 방식은 전통적인 Spring Framework에서 web.xml이나 WebApplicationInitializer를 사용하는 방식보다 더 유연하고 간결하다.
- FilterRegistrationBean을 사용하면 필터의 순서를 setOrder 메서드를 통해서 명시적으로 지정할 수 있다. (하단의 코드 확인)
예시: Spring Boot에서 필터 체이닝 설정
아래의 예시코드는 FilterRegistrationBean을 사용하여 필터를 등록하고 순서를 지정하는 방법이다.
- 아래의 코드에서 LoggingFilter와 AuthenticationFilter는 실제 필터 구현 클래스다. 예를 들어 LoggingFilter는 요청과 응답을 로깅하는 기능을 수행하고, AuthenticationFilter는 사용자 인증을 처리하는 기능을 수행한다.
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<LoggingFilter> loggingFilter() {
FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LoggingFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(1); // 로깅 필터의 순서를 1로 설정
return registrationBean;
}
@Bean
public FilterRegistrationBean<AuthenticationFilter> authenticationFilter() {
FilterRegistrationBean<AuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new AuthenticationFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(2); // 인증 필터의 순서를 2로 설정
return registrationBean;
}
}
class LoggingFilter implements Filter {
// 로깅 관련 구현 내용
}
class AuthenticationFilter implements Filter {
// 인증 관련 구현 내용
}
doFilter 메서드와 체이닝
- doFilter() 메소드 내에서 chain.doFilter(request, response);를 호출하는 것은 Spring Framework와 동일하다.
- 이 호출을 통해 요청이 다음 필터로 넘어가거나, 필터 체이닝의 끝에 도달하면 최종적으로 서블릿이나 컨트롤러로 전달된다.
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 요청에 대한 로깅 처리
logRequest(request);
// 다음 필터로 요청을 넘긴다.
chain.doFilter(request, response);
// 응답에 대한 로깅 처리
logResponse(response);
}
// ... 로깅 관련 메소드 구현 ...
}
필터 체이닝의 흐름 제어
- Spring Boot에서는 OncePerRequestFilter와 같은 특별한 필터를 사용하여, 요청당 한 번만 실행되도록 필터의 실행을 제어할 수도 있다. 이는 중복 실행을 방지하고, 필터의 실행 효율을 높이는 데 도움을 준다.
public class CustomOncePerRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 요청에 대한 처리
// ...
// 필터 체이닝
filterChain.doFilter(request, response);
}
}
📌 마무리
저번 Filter 정리에 이어서 이번에는 Filter를 구현하게 된다면 Override 하게 될 doFilter() 메서드에 대해서 알아봤다.
이 글을 작성하게 된 계기는 왜 doFilter()를 구현할 때 매게 변수로 HttpServletRequest가 아닌 ServletRequest 타입을 받는지 궁금했기 때문이었다.
그래서 이 내용을 조사하다보니 더 궁금한 것들이 생기면서 여러 가지 다른 것들도 같이 조사하게 되었다. 그렇게 내부로 들어가다 FilterChain 방식에까지 궁금증이 생겨 같이 알아보고 정리하게 되었는데 이러한 과정을 통해 스스로 많은 공부가 되었던것 같다.
'Spring 기초 > Spring 기초 지식' 카테고리의 다른 글
[Spring] @Bean을 사용한 스프링 빈 등록 (0) | 2023.11.12 |
---|---|
[Spring] JAR와 WAR 이해하기 (0) | 2023.11.10 |
웹 개발자를 위한 CORS 이해와 Spring Boot에서의 적용 방법 (1) | 2023.11.05 |
Spring: 필터(Filter)가 인터셉터(Interceptor)와 다른점 (1) | 2023.11.04 |
[Spring] 스프링과 자바의 동시성과 병렬 처리 (1) | 2023.10.31 |