[Spring Security] Filter 에 대한 오해와 진실 (doFilter의 정확한 작동방식에 대한 심층분석과 Filter의 흐름) (Spring 200 OK but No Content)
# [만연한 오해]
doFilter() 기준으로 위쪽이 API 호출 전처리 과정이고, 아래쪽이 API 호출 후처리 과정이다.
(틀린 말은 아니나 상황에 따라 아닌 경우가 있으므로, 이에대해 확실히 알고 넘어갈 필요가 있다.)
@Component //default 제일 마지막 필터로
public class TestFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// API 호출 전 로직?
chain.doFilter(request, response);
// API 호출 후 로직?
}
}
# doFilter()의 작동 원리 (재귀호출 패턴)
doFilter() 메소드의 Implementation 을 따라가면, 크게 3가지 종류가 나온다.
[VirtualFilterChain, PassThroughFilterChain, MockFilterChain] 이다.
눈여겨 보아야 할 것은 VirtualFilterChain과 PassThroughFilterChain이며,
doFilter()의 종료시점에 우리가 흔히 알고 있는 방식인 Filter -> API Controller 로 Request와 Response가 전달된다.
(물론 Controller 앞단엔 Interceptor가 위치한다.)
그런데 VirtualFilterChain 의 doFilter()의 종료시점은 doFilter()의 호출횟수가 FilterChain에 등록된 Filter의 갯수와 같아지는 시점이다.
이상적으로는 맨 마지막 Filter의 doFilter()의 종료 시점부터 재귀호출이 종료되듯이 하나씩 return 되며 후처리 로직이 실행된다.
** doFilter()의 호출횟수가 Filter의 갯수보다 많을 경우, Thread 하나가 죽어버리며 오류를 뱉어내지만 Spring은 멀티쓰레드 환경이기에 서비스는 유지되며 API호출도 정상적으로 이루어진다.
** doFilter()의 호출횟수가 Filter의 갯수보다 적을 경우, API 에 도달하지 못하며 그 상태로 하나씩 return 되기 시작하면서 결국 default status code 인 200과 텅빈 Body 가 Response로 Client에게 내려간다. (Response 를 변형하지 않았을 경우)
** 재귀호출의 특성으로 인해, 아래의 예시가 탄생된다
# API 호출 이후, 작동하는 doFilter()이후 로직은 Filter(1)과 Filter(2)만 해당된다. (CASE 1 & CASE 2)
(Filter(3)의 doFilter() 이후 로직은 API호출전에 실행된다.)

이러한 특성으로 인해, Filter단에서 Controller로의 진입시기를 커스터마이징 할 수 있으며 return 키워드를 썼을때의 결과또한 가늠해볼 수 있다.
물론 진입시기를 커스터마이징하는건 이해하기 쉬운 로직을 위해 지양하는 것이 좋다.
하지만, 이 과정을 알아야 적절한 return 키워드 삽입으로 예기치 못한 오류를 피할 수 있다.
이러한 특성을 모르고 return 을 아무데나 끼워넣다간 '200 OK' 와 함께 텅빈 Body가 함께오는걸 목격할 수 있다. 😥
# [VirtualFilterChain, PassThroughFilterChain, MockFilterChain] 코드분석
[VirtualFilterChain]
사진에는 나와있지 않지만 this.currentPosition=0 으로 시작한다.
아래 코드를 보면, currentPosition 과 size 가 같으면 originalChain으로 넘어가게 된다.
그게 아닐 시, doFilter()가 호출될때마다 currentPosition++ 되는 형태이다.
(필터가 남아있으면 currentPosition++ 을 노린거겠으나, 그건 각 필터가 doFilter()를 한번씩만 호출했을 경우다)
** @Component 로 작성하는 필터들과 SpringSecurityFilterChian 에 있는 필터들은 VirtualFilterChain 방식으로 작동한다.

[MockFilterChain]
MockFilterChain의 종료시점은 Iterator가 더이상 다음 Filter를 가지지 않았을 때다.
해당 클래스명으로 검색결과, 테스팅에 쓰는 용도인 것 같다.

[PassThroughFilterChain]
PassThroughFilterChain 은 종료시점에 servlet.service에게 request 와 response 를 전달한다.

# 테스트 코드
@Component // default order = 2147483647 // 가장 뒤에 위치한 필터
@Slf4j
public class TestFilter1 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("TestFilter1 호출됨");
//로직이 끝났으니 return 되어 TestFilter1을 호출한 TestFilter2의 chain.doFilter 가 종료된다.
}
}
@Component
@Slf4j
@Order(value = 2147483646)
public class TestFilter2 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("TestFilter2 호출됨");
chain.doFilter(request, response); // 실행순서 2번
log.info("TestFilter2 호출됨(after DoFilter[1])"); // API 호출 전 실행
chain.doFilter(request, response); // 실행순서 3번
log.info("TestFilter2 호출됨(after DoFilter[2])"); // API 호출 후 실행
//모든 로직 실행 후 return -> TestFilter3의 chain.doFilter()로 진행
}
}
@Component
@Slf4j
@Order(value = 2147483645)
public class TestFilter3 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("TestFilter3 호출됨");
chain.doFilter(request, response); // 실행순서 1번
log.info("TestFilter3 호출됨(after DoFilter)"); // API 호출 후 실행
}
}
# Reference 는 나의 삽질