Back End/Spring Security

[Spring Security] Filter 에 대한 오해와 진실 (doFilter의 정확한 작동방식에 대한 심층분석과 Filter의 흐름) (Spring 200 OK but No Content)

DevPing9_ 2021. 12. 8. 22:00

# [만연한 오해]

 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] 이다.

 

눈여겨 보아야 할 것은 VirtualFilterChainPassThroughFilterChain이며,

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 는 나의 삽질

728x90