스프링 부트 필터의 동작 구조
전반적인 흐름
1. 클라이언트 요청이 발생하면, 해당 요청은 서블릿 컨테이너(Tomcat, Jetty 등)에 전달된다.
2. 서블릿 컨테이너는 요청을 처리하기 전에 등록된 <<필터 체인(filter chain)>> 을 통해 요청을 필터링한다.
3. 필터 체인에 등록된 필터들은 등록된 순서대로 요청을 가로채고, 각 필터는 필요에 따라 요청을 수정하거나, 특정 조건에서 요청을 차단할 수도 있다.
4. 모든 필터를 통과한 후에야, 서블릿 또는 스프링 MVC에 요청이 도달한다.
스프링에서 필터를 구현하는 방식
스프링에서 필터는 두 가지 방식으로 사용할 수 있다.
1. 서블릿 필터 (Servlet Filter)
- javax.servlet.Filter 인터페이스를 구현하는 필터
- 스프링 부트에서 <<FilterRegistrationBean>> 을 사용하여 필터를 등록할 수 있다.
2. 스프링 시큐리티 필터 (Spring Security Filter)
- 인증 및 인가를 처리하기 위한 스프링 시큐리티의 필터 체인
- 여러 개의 시큐리티 필터들이 요청을 검증한 후 요청이 처리된다.
필터 사용 예시 (서블릿 필터)
1. 기본 필터 구현
1.1 LoggingFilter.java
package com.example.demo.filter;
import javax.servlet.*;
import java.io.IOException;
public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("LoggingFilter initialized");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("LoggingFilter: Request is being filtered");
chain.doFilter(request, response);
System.out.println("LoggingFilter: Response is being filtered");
}
@Override
public void destroy() {
System.out.println("LoggingFilter destroyed");
}
}
1.2 AuthenticationFilter.java
package com.test.test1.filter;
import com.test.test1.wrapper.CharResponseWrapper;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
public class AuthenticationFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("AuthnticationFilter initialized");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 요청 데이터를 읽음
String requestData = request.getParameter("body");
System.out.println("Request data: " + requestData); // 요청 데이터 출력 (예: 클라이언트가 보낸 'body' 파라미터)
// 응답을 수정하기 위해 response를 래핑, 이렇게 하면 버퍼에 저장이 되고 실제 데이터를 수정할 수 있다.
CharResponseWrapper wrappedResponse = new CharResponseWrapper((HttpServletResponse) response);
// 다음 필터 또는 서블릿(컨트롤러) 호출 -> 최종 필터까지 가서 최종 필터의 응답값을 변형하면서 최초 여기 까지 옴
chain.doFilter(request, wrappedResponse);
// 원래 응답 데이터를 가져옴
String originalResponseContent = wrappedResponse.getCapturedResponse();
System.out.println("Original Response: " + originalResponseContent);
// 응답 데이터를 수정 (예: 응답 본문에 요청 데이터를 추가)
String modifiedResponseContent = originalResponseContent + "\nModified response with request data: " + requestData;
byte[] responseBytes = modifiedResponseContent.getBytes("UTF-8");
// content-length는 바이트 수를 요구한다. 영어, 띄어쓰기는 전부 1바이트지만, 한글은 개당 3바이트이기 때문에 단순 글자 길이로 했을 경우 전부 못 읽어 낸다.
// UTF-8은 참고하자면 특정 언어를 컴퓨터 언어 비트인 저급언어로 변형하는 즉, 인코딩 방식이다.
response.setContentLength(responseBytes.length);
System.out.println(responseBytes.length); // -> 한글로 인해서 영어로만 되어 있다면 52바이트가 56바이트가 된다.
// 수정된 응답을 클라이언트에게 전송
//문자열을 직접 담아서 보내고, 이 문자열은 내부적으로 바이트 배열로 변환되어 전송한다. 그러면 클라이언트는 이 바이트 배열을 다시 문자열로 디코딩하여 사용자가
//볼 수 있는 형태로 표시
// 최종 필터까지 간 뒤, 역순으로 돌아오면서 response 데이터가 변형이 일어날텐데, 이때 버퍼에 저장해두면서 변형하고 최종 변형한 뒤, 버퍼에서 클라이언트로 응답
response.getWriter().write(modifiedResponseContent);
// response.getOutputStream().write(responseBytes); -> 이거는 바로 바이트 단위로 전송
}
@Override
public void destroy() {
System.out.println("AuthenticationFilter destroyed");
}
}
위 코드 처럼 응답 데이터를 가져와서 탐지 등을 하거나 변조를 한 뒤 응답값으로 보낼 수 있다.
2. 필터 등록
여러 필터를 등록하려면 <<FilterRegistrationBean>> 을 사용하여 각각의 필터를 설정해야 한다.
package com.example.demo.config;
import com.example.demo.filter.LoggingFilter;
import com.example.demo.filter.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("/api/*"); // 필터가 적용될 URL 패턴
registrationBean.setOrder(1); // 필터 실행 순서 설정
return registrationBean;
}
@Bean
public FilterRegistrationBean<AuthenticationFilter> authenticationFilter() {
FilterRegistrationBean<AuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new AuthenticationFilter());
registrationBean.addUrlPatterns("/api/*"); // 필터가 적용될 URL 패턴
registrationBean.setOrder(2); // 필터 실행 순서 설정
return registrationBean;
}
}
3. 필터 실행 순서
setOrder(int order) : 필터의 실행 순서를 설정한다. 숫자가 작을수록 먼저 실행된다. 위의 예에서는 LoggingFilter가 먼저 실행되고, 그 다음에 AuthenticationFilter 가 실행된다.
3.5 Controller 설정
package com.test.test1.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/api/hello")
public String hello() {
return "Hello, World";
}
}
4. 애플리케이션 실행
이제 애플리케이션을 실행하고 /api/ 로 시작하는 요청을 보내면, 등록된 필터가 차례로 실행된다.
위의 Controller를 사용해서 Postman에 url인 http://localhost:8080/api/hello 를 요청하면 해당 필터를 정해준 순서대로 거친다.
LoggingFilter initialized
AuthenticationFilter initialized
LoggingFilter: Request is being filtered
AuthenticationFilter: Checking authentication
AuthenticationFilter: Response is being filtered
LoggingFilter: Response is being filtered
5. 전반적인 흐름
클라이언트 -> 필터 -> 컨트롤러 -> 필터 -> 클라이언트
요즘 많이 접하고 있는 필터 부분 한 번 정리해 보았다. 이번에는 Spring Boot를 사용할 때다 보니 Gradle를 활용한 필터 구조였다.
다음 번에는 전통적인 필터 적용 방식인 web.xml를 활용해서 해보려고 한다.