Deep Dive /Java Web /4

4. ApplicationFilterChain → DelegatingFilterProxy.doFilter()

Tomcat의 ApplicationFilterChain이 Spring 필터를 직접 알지 않고, 표준 Servlet Filter인 DelegatingFilterProxy를 호출해 Spring Security 진입점을 여는 단계입니다.


코드 기준 결론

  1. Spring Boot는 SecurityFilterAutoConfiguration.securityFilterChainRegistration(...)에서 DelegatingFilterProxyRegistrationBean을 등록한다.
  2. 등록 타깃 이름은 DEFAULT_FILTER_NAME이며 실제 값은 springSecurityFilterChain이다.
  3. Tomcat은 일반 필터처럼 DelegatingFilterProxy.doFilter(...)를 호출한다.
  4. DelegatingFilterProxy는 Spring 컨테이너에서 타깃 Filter 빈을 조회하고 invokeDelegate(...)로 위임한다.
  5. 이 설계 덕분에 컨테이너(Tomcat)와 보안 구현(Spring Security)이 강결합되지 않는다.

오픈소스 호출 흐름

1) Boot가 프록시 필터를 서블릿 체인에 등록

// SecurityFilterAutoConfiguration
private static final String DEFAULT_FILTER_NAME =
        AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;

@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(...) {
    DelegatingFilterProxyRegistrationBean registration =
            new DelegatingFilterProxyRegistrationBean(DEFAULT_FILTER_NAME);
    registration.setOrder(securityFilterProperties.getOrder());
    registration.setDispatcherTypes(getDispatcherTypes(securityFilterProperties));
    return registration;
}

핵심은 컨테이너에 직접 FilterChainProxy를 꽂는 게 아니라, DelegatingFilterProxy를 등록한다는 점입니다.

2) 요청 시 Tomcat이 DelegatingFilterProxy 호출

// DelegatingFilterProxy
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
        // lazy init
        WebApplicationContext wac = findWebApplicationContext();
        delegateToUse = initDelegate(wac);
        this.delegate = delegateToUse;
    }

    invokeDelegate(delegateToUse, request, response, filterChain);
}

3) 실제 보안 체인으로 위임

// DelegatingFilterProxy
protected void invokeDelegate(
        Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    delegate.doFilter(request, response, filterChain);
}

여기서 delegate가 바로 springSecurityFilterChain 빈이며, 일반적으로 구현체는 FilterChainProxy입니다.


왜 이런 구조인가 (설계 의도)

1. 서블릿 표준 체인과 Spring IoC 체인의 접합부

Tomcat은 Servlet 스펙의 Filter만 이해합니다. Spring Security의 실제 필터 조합은 Spring Bean 생명주기와 의존성 주입이 필요합니다. DelegatingFilterProxy는 이 둘 사이의 접합 어댑터 역할을 수행합니다.

2. Lazy 초기화로 부팅 순서 문제 완화

DelegatingFilterProxy.doFilter() 내부에서 delegate가 없으면 findWebApplicationContext()initDelegate(...)를 수행합니다. 즉, 필터 인스턴스를 요청 시점에 확보할 수 있어서 컨텍스트 초기화 순서 충돌에 유연합니다.

3. 컨테이너 독립성

보안 로직이 특정 서블릿 컨테이너 구현(Tomcat 내부 API)에 의존하지 않습니다. delegate.doFilter(...) 계약만 맞추면 Jetty/Undertow에서도 동일하게 동작합니다.

4. 등록 순서/DispatcherType 제어를 Boot가 통합 관리

SecurityFilterAutoConfiguration에서 setOrder(...), setDispatcherTypes(...)를 설정하므로, 보안 필터 위치를 일관되게 유지할 수 있습니다.


시퀀스 다이어그램

sequenceDiagram participant AFC as ApplicationFilterChain participant DFP as DelegatingFilterProxy participant WAC as WebApplicationContext participant SCP as springSecurityFilterChain
(FilterChainProxy) participant Next as 다음 Filter/Servlet AFC->>DFP: doFilter(request, response, chain) alt delegate 미초기화 DFP->>WAC: findWebApplicationContext() DFP->>WAC: initDelegate("springSecurityFilterChain") WAC-->>DFP: FilterChainProxy Bean 반환 else delegate 이미 존재 DFP->>DFP: 캐시된 delegate 사용 end DFP->>SCP: invokeDelegate() -> doFilter(...) SCP-->>DFP: 보안 처리 계속 진행 DFP-->>AFC: 반환 AFC->>Next: 다음 체인 진행

왜 이게 4번의 본질인가

이 단계의 본질은 “Security 로직 자체”가 아니라, Tomcat 필터 체인에서 Spring Security 체인으로 진입하는 관문입니다.

  • Tomcat 관점: 표준 Filter 하나 실행
  • Spring 관점: 그 Filter가 내부적으로 Security Bean 체인으로 위임

즉 4번은 “보안 정책 수행” 이전의 브리지(Bridge) 단계입니다.


다음 탐구

5. DelegatingFilterProxy → FilterChainProxy.doFilter()