Web-Spring

Springboot에서 SpringSecurity는 어떻게 초기화되는가? - 상

devKhc 2025. 5. 3. 00:53

개요

흔히 Springboot에서 로그인 관련 인증/인가를 구현한다고 하면 기계처럼 블로그를 뒤져서라도 SpringSecurity를 도입하려고 시도해요

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

또한 SecurityConfig와 같은 Java Configuration 파일을 생성하여 SecurityFilterChain Bean을 구성하고

@EnableWebSecurity와 같은 어노테이션을 붙여서 마무리해요.

이 과정까지 성공한다면 SpringSecurity 덕에 쉽게 인증/인가를 구현할 수 있습니다.

여기까지는 단순 사용을 위한 구현레벨로서 코드를 작성하였던 거라 실제 동작이 궁금했어요.

이번 포스팅을 통해 Springboot와 Security 영역 클래스의 내부코드를 살펴보며 어떻게 SpringSecurity 영역의 객체들이 초기화되고, SecurityFilter들이 동작하기 위한 구성이 어떤 식으로 이루어지는지 알아볼 수 있어요.

사전지식

SpringSecurity를 가지고 회원가입부터 이어지는 로그인 플로우를 개발해 본 경험이 필요해요.

Springboot의 컨텍스트(ApplicationContext, ServletContext 등)들이 어떤 것들이 있는지 용어만이라도 들어봤다면 오케입니다!

우선 boot가 켜지면

Springboot가 켜지면 내부적으로 현재 켜지려고 하는 Springboot 앱이 SEVLET 기반인지 REACTIVE 기반인지 구분해 내고

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    // 수많은 코드들...
    this.properties.setWebApplicationType(WebApplicationType.deduceFromClasspath());
    // 수많은 코드들...
}
public enum WebApplicationType {
    NONE,
    SERVLET, // Spring WebMVC 환경
    REACTIVE; // Spring WebFlux 환경

    private static final String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet",
            "org.springframework.web.context.ConfigurableWebApplicationContext" };

    private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";

    private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";

    private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

    static WebApplicationType deduceFromClasspath() {
        if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
                && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
            return WebApplicationType.REACTIVE;
        }
        for (String className : SERVLET_INDICATOR_CLASSES) {
            if (!ClassUtils.isPresent(className, null)) {
                return WebApplicationType.NONE;
            }
        }
        return WebApplicationType.SERVLET;
    }
}

일반적으로 Springboot에서 Spring WebMVC를 선택하였다면 SEVLET모드로 채택됩니다.

그리고 new SpringApplication().run(args)에서 이 run() 메서드가 Springboot를 위한 구성을 시작해요.

public ConfigurableApplicationContext run(String... args) {

    try {
        context = createApplicationContext();
        // 많은 코드를 생략했습니다..

    return context;
}

createApplicationContext()가 바로 ApplicationContext를 초기화시켜 주는 메서드입니다.

protected ConfigurableApplicationContext createApplicationContext() {
    return this.applicationContextFactory.create(this.properties.getWebApplicationType());
}

SEVLET으로 결정 난 Springboot 타입을 가지고 팩토리를 통해 인스턴스를 만들어내죠

여기서 사용되는 팩토리는 DefaultApplicationContextFactory 에요.

@Override
public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {
    try {
        return getFromSpringFactories(webApplicationType, ApplicationContextFactory::create,
                this::createDefaultApplicationContext);
    }
    catch (Exception ex) {
        throw new IllegalStateException("Unable create a default ApplicationContext instance, "
                + "you may need a custom ApplicationContextFactory", ex);
    }
}

private <T> T getFromSpringFactories(WebApplicationType webApplicationType,
        BiFunction<ApplicationContextFactory, WebApplicationType, T> action, Supplier<T> defaultResult) {
    for (ApplicationContextFactory candidate : SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class,
            getClass().getClassLoader())) {
        T result = action.apply(candidate, webApplicationType);
        if (result != null) {
            return result;
        }
    }
    return (defaultResult != null) ? defaultResult.get() : null;
}

create() 메서드를 따라가면 결국 getFromSpringFactories()를 만나게 되는데,

여기서 하는 것은 다른 팩토리 클래스가 있다면 클래스로더를 통해 런타임환경으로 로드시키고

해당 팩토리에 ApplicationContext를 만드는 것을 위임해요.

public class SpringFactoriesLoader {

    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
      // 많은 코드들...
    // 위의 경로에 쓰여져있는 ApplicationContextFactory 타입의 클래스를 찾아요
 }
# 제가 사용하는 spring-boot-3.4.2-SNAPSHOT 을 기준으로는
# org.springframework.boot:spring-boot:3.4.2-SNAPSHOT/META-INF/spring.factories 에 쓰여있었어요

# Application Context Factories
org.springframework.boot.ApplicationContextFactory=\
org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContextFactory,\
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContextFactory

# 이제 저기있는 클래스에서 아까 결정된 WebApplicationType을 주입하여 ApplicationContext 객체를 얻어옵니다.
// ServletWebServerApplicationContextFactory 에서...

@Override
public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {
    return (webApplicationType != WebApplicationType.SERVLET) ? null : createContext();
}

private ConfigurableApplicationContext createContext() {
    if (!AotDetector.useGeneratedArtifacts()) {
        return new AnnotationConfigServletWebServerApplicationContext();
        // 결국 이녀석이 ApplicationContext의 실제 인스턴스로 리턴되게 되죠.
    }
    return new ServletWebServerApplicationContext();
}

그렇게 ApplicationContext를 구성하고 나면 @Configuration, @Bean, @Controller 등 빈을 초기화하러 갈 겁니다.

Bean들이 모두 초기화되고 나면,

흔히 boot 내부의 임베디드 톰켓이라고 알고 있는 org.springframework.boot.web.embeded.tomcat 에서 톰켓 서버가 구성되고

그 내부에서 ServletContext를 초기화하게 됩니다.

더보기

Tomcat과 ServletContext에 관한 여담

사실 여기서 저는 시큐리티 내용과는 좀 동떨어지지만 Tomcat을 구성하는 실제 구체클래스가 누군지도 궁금했는데요,

Tomcat은 사실 TomcatWebServer라는 클래스로 둘러 쌓여있고,

start() 메서드를 타고 타고 들어가면 StandardServer라는 클래스를 통해 서버를 실제로 구성하는 모습을 알 수 있었어요.

// TomcatWebServer.java
private void initialize() throws WebServerException {
    synchronized (this.monitor) {
        try {
            // Start the server to trigger initialization listeners
            this.tomcat.start();
        }
        catch (Exception ex) {
        }
    }
}
// Tomcat.class 
public Server getServer() {

    if (server != null) {
        return server;
    }

    server = new StandardServer();
    // 각종 코드들...
    return server;
}

 

이제껏 Tomcat이 웹서버 역할을 수행한다고 알고 있어서 Tomcat이 가장 작은 단위일 줄 알았었는데...

그 내부는 훨씬 더 복잡했었습니다.

또한 SerlvetContext의 실체는 누구인지 살펴보았는데요

StandardXXX는 사실 Server만 있는 것이 아니라 StandardService, StandardEngine, StandardContext 등이 있어요

이들 모두 결국엔 Tomcat의 일원이 되는 거죠

이 중에서 SerlvetContext와 이름이 비슷한 StandardContext를 살펴보면 getSerlvetContext()라는 메서드가 구현되어 있어요.

 

// StandardContext.java
@Override
public ServletContext getServletContext() {
    // This method is called multiple times during context start which is single threaded
    // so there is no concurrency issue
    if (context == null) {
        context = new ApplicationContext(this);
        if (altDDName != null) {
            context.setAttribute(Globals.ALT_DD_ATTR, altDDName);
        }
    }
    return context.getFacade();
}

ServletContext의 구현체는 바로 ApplicationContex였습니다.

되게 처음보고는 당황스러웠습니다.

Springboot 아키텍처적으로는 엄연하게 ServletContextApplicationContext는 다른데 말이죠.

정리해 보자면 ApplicationContext의 정체는 AnnotationConfigServletWebServerApplicationContext()였고

SevletContext의 정체는 ApplicationContext(org.apache.catalina.core) 였답니다.


SpringSecurity Architecture

출처: https://docs.spring.io/spring-security/reference/servlet/architecture.html

공식 문서에 나와있는 아키텍처예요

일반적으로 Springboot 던 전통적인 Spring Framework 던

Client - Filter - DispatcherServlet - HandlerMapping - Interceptor(before) - Controller - Interceptor(after)

라는 전체 동작을 가지죠.

FilterServletContext 영역이기 때문에, ServletContext이 초기화되어야지 Filter가 등록이 됩니다.

위의 동작을 보면 DelegatingFilterProxy이 있고 그 내부에 FilterChainProxy를 통해 SecurityFilterChain으로 넘어가고 SecurityFilter를 타는 것처럼 보입니다.

여기서 왜 DelegatingFilterProxy가 필요한 걸까요?

사실 저게 있는지도 몰랐습니다.

왜냐하면 addFilterBefore()와 같은 메서드로 저의 커스텀 필터를 끼워 넣으면

그것도 Filter의 일원이 될 줄 알았거든요.

우선 DelegatingFilterProxy가 뭐 하는 건지, boot에서는 어떻게 초기화되는지 알아봅시다.

DelegatingFilterProxy

public DelegatingFilterProxy(String targetBeanName) {
    this(targetBeanName, null);
}

public DelegatingFilterProxy(String targetBeanName, @Nullable WebApplicationContext wac) {
    Assert.hasText(targetBeanName, "Target Filter bean name must not be null or empty");
    setTargetBeanName(targetBeanName);
    this.webApplicationContext = wac;
    if (wac != null) {
        setEnvironment(wac.getEnvironment());
    }
}

@Override
protected void initFilterBean() throws ServletException {
    this.delegateLock.lock();
    try {
        if (this.delegate == null) {
            // If no target bean name specified, use filter name.
            if (this.targetBeanName == null) {
                this.targetBeanName = getFilterName();
            }
            // Fetch Spring root application context and initialize the delegate early,
            // if possible. If the root application context will be started after this
            // filter proxy, we'll have to resort to lazy initialization.
            WebApplicationContext wac = findWebApplicationContext();
            if (wac != null) {
                this.delegate = initDelegate(wac);
            }
        }
    }
    finally {
        this.delegateLock.unlock();
    }
}
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
    String targetBeanName = getTargetBeanName();
    Assert.state(targetBeanName != null, "No target bean name set");
    Filter delegate = wac.getBean(targetBeanName, Filter.class);
    if (isTargetFilterLifecycle()) {
        delegate.init(getFilterConfig());
    }
    return delegate;
}

일단 GenericFilterBean을 상속받기에 엄연히 DelegatingFilterProxy도 하나의 Filter에요.

DelegatingFilterProxy가 갖고 있는 targetBeanName이라는 Bean을 WebApplicationContext 객체를 통해 들고 와서 위임할 Filter로 등록하고 있어요.

아키텍처 그림으로 이해해 보자면 targetBeanName으로 들고 오는 Bean의 인스턴스는 FilterChainProxy일 거예요.

WebApplicationContextApplicationContext의 하위 인터페이스로서 Springboot의 ApplicationContext라고 생각하면 돼요.

그럼 DelegatingFilterProxy를 누가 만들까요?

Springboot에서는 SpringSecurityAutoConfigurationDelegatingFilterProxyRegistrationBean 클래스를 통해 DelegatingFilterProxyServletContext 구성 이후 Filter로 등록되도록 도와줘요

DelegatingFilterProxyRegistrationBean

public DelegatingFilterProxyRegistrationBean(String targetBeanName,
        ServletRegistrationBean<?>... servletRegistrationBeans) {
    super(servletRegistrationBeans);
    Assert.hasLength(targetBeanName, "TargetBeanName must not be null or empty");
    this.targetBeanName = targetBeanName;
    setName(targetBeanName);
}

@Override
public DelegatingFilterProxy getFilter() {
    return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) {

        @Override
        protected void initFilterBean() throws ServletException {
            // Don't initialize filter bean on init()
        }

    };
}
private WebApplicationContext getWebApplicationContext() {
    Assert.notNull(this.applicationContext, "ApplicationContext be injected");
    Assert.isInstanceOf(WebApplicationContext.class, this.applicationContext);
    return (WebApplicationContext) this.applicationContext;
}

이 클래스는 RegistrationBean의 일종입니다.

RegistrationBeanServletContextInitializer의 일종으로, ServletContext가 초기화되어 초기 구성에 필요한 객체들 혹은 행위들을 정의할 때 사용해요.

생각해 보면 DelegatingFilterProxy도 상속은 복잡하지만 결국 Bean인 건데,

boot에서는 ApplicationContext가 먼저 생겨나기에 Bean들을 미리 구성할 수 있는 걸로 압니다.

Bean을 로드하는 것으로 Filter를 등록할 수는 없을까요?

기본적으로 FilterServletContext에서만 등록이 가능해요.

ApplicationContext를 통해 DelegatingFilterProxy를 Bean으로 등록한 것이지, Filter로서 등록한 건 아닙니다.

그래서 SerlvetContextIntializer를 구현한 RegistrationBean을 통해 ServletContext 초기화 시점에 등록되도록 구현하는 것이죠.

실제로 다음과 같은 코드를 통해 DelegatingFilterProxyFilter로서 ServletContext에 등록해요

// DelegatingFilterProxy의 super 클래스인 AbstractFilterRegistrationBean<DelegatingFilterProxy>.java

@Override
protected Dynamic addRegistration(String description, ServletContext servletContext) {
    Filter filter = getFilter();
    return servletContext.addFilter(getOrDeduceName(filter), filter);
}

그리고 위의 코드는 타고 타고 올라가서 ServletContextInitializer를 구현한 RegistrationBean에서 호출 트리의 시작점인 register() 메서드를 호출하도록 onStartup()을 구현합니다.

더보기

참고로 ServletContextInitializer의 구현체는 상당히 많으며, 그것들은 ServletContext가 구성된 후 Tomcat이 시작하는 이벤트가 발생하면 한 번에 onStartup() 메서드들이 실행됩니다.

그럼 최종적으로 DelegatingFilterProxyRegistrationBean은 누가 Bean으로 등록할까요?

SecurityFilterAutoConfiguration에서 등록합니다.

public class SecurityFilterAutoConfiguration {

    private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;
    // == "springSecurityFilterChain"

    @Bean
    @ConditionalOnBean(name = DEFAULT_FILTER_NAME)
    // ConditionalOnBean은 조건부 Bean 등록으로, ApplicationContext 초기화 때 name에 해당하는 Bean 정의가 등록되었을 때만 등록합니다.
    public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
            SecurityProperties securityProperties) {
        DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
                DEFAULT_FILTER_NAME);
        registration.setOrder(securityProperties.getFilter().getOrder());
        registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
        return registration;
    }
}

그런데 조건부 Bean Definition 때문에 "springSecurityFilterChain"이라는 Bean이 먼저 등록되어야 하네요.

그러려면 @EnableWebSecurityWebSecurityConfiguration을 먼저 이해해봅시다.

@EnableWebSecurity

흔히 SecurityConfig라는 파일을 통해 SecurityFilterChain Bean을 등록한다고 하면

@EnableWebSecurity

위와 같은 어노테이션을 흔히 사용합니다.

내부를 들여다보면 아래와 같고, 찾으려는 WebSecurityConfiguration이 있어요.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class,
        HttpSecurityConfiguration.class, ObservationImportSelector.class })
@EnableGlobalAuthentication
public @interface EnableWebSecurity {

    /**
     * Controls debugging support for Spring Security. Default is false.
     * @return if true, enables debug support with Spring Security
     */
    boolean debug() default false;

}

@Import는 목록에 있는 클래스들을 @Configuration 어노테이션 형태로 포함시키는 역할이에요

그런데 사실은 저 위의 클래스들은 모두 @Configuration 어노테이션이 이미 달려있습니다.

그럼 Springboot가 켜질 때 초기화 되는 거 아닌가요?라는 의심을 할 수 있어요

물론 Springboot가 켜질 때도 @Configuration 어노테이션이 부과된 Java파일들을 읽어 들이고 초기화하는 건 맞지만,

이는 어디까지나 @EnableAutoConfiguration에 의하여 작동됩니다.

그리고 @EnableAutoConfiguration

org.springframework.boot:spring-boot-autoconfigure:{spring boot 버전}/META-INF/spring-autoconfigure-metadata.properties에서 아래 사진처럼 Security의 일부 XXXConfiguration들은 boot가 켜질 때 구성하도록 되어있어요.

AutoConfiguration 목록들

나머지의 경우에서는 @ComponentScan을 통해 읽어 들여야 하는데,

일반적으로 Springboot에서는 @SpringBootApplication 어노테이션이 있는 장소를 basePackage로 하여금 Bean들을 스캔해요

하지만 Import 절에 있는 클래스들은 모두 basePackage 밖에 있습니다.

따라서 @EnableWebSecurity가 필요한 거였죠.

저 중에서 WebSecurityConfiguration으로 넘어갑시다.

WebSecurityConfiguration

@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {

    private WebSecurity webSecurity;

    private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;

    private List<SecurityFilterChain> securityFilterChains = Collections.emptyList();

    private List<WebSecurityCustomizer> webSecurityCustomizers = Collections.emptyList();

    @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
    public Filter springSecurityFilterChain() throws Exception {
        boolean hasFilterChain = !this.securityFilterChains.isEmpty();
        if (!hasFilterChain) {
            this.webSecurity.addSecurityFilterChainBuilder(() -> {
                this.httpSecurity.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
                this.httpSecurity.formLogin(Customizer.withDefaults());
                this.httpSecurity.httpBasic(Customizer.withDefaults());
                return this.httpSecurity.build();
            });
        }
        for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
            this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
        }
        for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
            customizer.customize(this.webSecurity);
        }
        return this.webSecurity.build();
    }

    @Autowired(required = false)
    void setFilterChains(List<SecurityFilterChain> securityFilterChains) {
        this.securityFilterChains = securityFilterChains;
    }
}

"springSecurityFilterChain"의 Bean을 WebSecurityConfiguration에서 등록하는 것을 알 수 있어요.

하지만 SecurityFilterChain@Autowired 관계로 인해 SecurityFilterChain Bean부터 찾아야 합니다.

더보기

근데 왜 List일까요?

그건 Multiple SecurityFilterChain을 통해 알 수 있어요.

저는 하나만 사용하기 때문에 깊게 다루진 않겠습니다.

SecurityFilterChain은 흔히 SpringSecurity 블로그 코드에서 흔히 볼 수 있는 Bean이죠

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception {

    return http
            .cors(cors -> )
            .csrf(AbstractHttpConfigurer)
            .httpBasic(AbstractHttpConfigurer)
            .formLogin(AbstractHttpConfigurer)
            .authorizeHttpRequests((authorizeReq) -> {
                authorizeReq
                   // 코드들...
            })
            .oauth2Login(oauth2 ->
                oauth2.authorizationEndpoint(authorization -> authorization.authorizationRequestResolver(customResolver)).
                userInfoEndpoint(c -> c.userService(customOAuth2UserService))
                    .successHandler(oAuth2SuccessHandler)
            )
            .addFilterBefore(커스텀 필터, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(exception ->
                    exception.authenticationEntryPoint(customAuthenticationEntryPoint).accessDeniedHandler(customAccessDeniedHandler))
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

            ).build();
}

결과적으로

"securityFilterChain" Bean을 통해서 WebSecurityConfiguration에서 @Autowired로 Bean을 주입받고

"springSecurityFilterChain"이란 이름의 Bean을 완성시켜요.

주목해야 할 점으로 WebSecurityConfiguration 에서 this.webSecurity.build() 메서드의 실제동작을 보면 아키텍처에서 보았던 익숙한 이름이 하나 보입니다.

더보기

참고로 WebSecurity 클래스에는 build() 메서드가 직접 구현되어있지 않으며,

템플릿 메서드 패턴으로 내부동작을 구현합니다.

build() 메서드는 AbstractSecurityBuilder에 구현되어 있고
doBuild()AbstractConfiguredSecurityBuilder에서

최종적인 performBuild()야 말로 WebSecurity에서 구현되어 있어요

// WebSecurity.java
@Override
protected Filter performBuild() throws Exception {

    List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
    // 수많은 코드들...
    FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
    // 수많은 코드들..
    filterChainProxy.setFilterChainDecorator(getFilterChainDecorator());
    filterChainProxy.afterPropertiesSet();

    Filter result = filterChainProxy;
    // 수많은 코드들...

    this.postBuildAction.run();
    return result;
}

바로 FilterChainProxy로서 DelegatingFilterProxy 내부에 자리 잡아져 있는 녀석이죠.

아키텍처 그림대로 역시 존재했었습니다.


결론

여기까지의 결론을 내보자면 SpringSecurity를 활용하는 개발자가 SecurityFilterChain Bean을 등록하고 @EnableWebSecurity를 코드로 적음으로서 WebSecurityConfiguration 구성이 완료되고

DelegatingFilterProxyRegistrationBeanSecurityFilterAutoConfiguration에 의하여 Bean으로 등록되고,

ServletContext 가 초기화 되는 시점에 Filter로 등록되게 됩니다.

즉, 아까 targetBeanNameDelegatingFilterProxyRegistrationBean의 생성자에서부터 넘어가게 되는데,

"springSecurityFilterChain"이라는 문자열이 담기게 되죠.

그리고 "springSecurityFilterChain"의 정체는 FilterChainProxy입니다.

그럼 다시 DelegatingFilterProxy로 넘어가서 맞춰보면 아키텍처 그림이 코드적으로 완성되게 됩니다.

더보기

물론 아직 의문점이 있을 수 있어요.

아래의 첨언하는 글을 통해 어느 정도 해소될 수 있으면 좋겠습니다.

WebApplicationContext를 찾는 로직이 있는 걸까?

// DelegatingFilterProxy.java
@Nullable
protected WebApplicationContext findWebApplicationContext() {
    if (this.webApplicationContext != null) {
        // The user has injected a context at construction time -> use it...
        if (this.webApplicationContext instanceof ConfigurableApplicationContext cac && !cac.isActive()) {
            // The context has not yet been refreshed -> do so before returning it...
            cac.refresh();
        }
        return this.webApplicationContext;
    }
    String attrName = getContextAttribute();
    if (attrName != null) {
        return WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
    }
    else {
        return WebApplicationContextUtils.findWebApplicationContext(getServletContext());
    }
}

전통적인 Spring Framework에서는 Spring bean Container 보다 SerlvetContext가 먼저 초기화될 수 있어요.

그래서 먼저 초기화되었을 때를 감안하여 ApplicationContext를 찾는 메서드가 같이 있는 것이에요

그런데 getWebApplicationContext() 메서드를 살펴보면 ServletContext에서 찾는 걸 알 수 있는데,

이는 ContextLoadListener를 보면 알 수 있다.

// ContextLoadListener.java

@Override
public void contextInitialized(ServletContextEvent event) {
// 이 메소드는 ServletContextListener 의 구현 메소드로서
// ServletContext가 초기화 될때 사용된다.
    ServletContext scToUse = getServletContextToUse(event);
    initWebApplicationContext(scToUse);
}
// ContextLoader.java

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
        throw new IllegalStateException(
                "Cannot initialize context because there is already a root application context present - " +
                "check whether you have multiple ContextLoader* definitions in your web.xml!");
    }


    try {

        if (this.rootContext == null) {
            this.rootContext = createWebApplicationContext(servletContext);
        }
        if (this.rootContext instanceof ConfigurableWebApplicationContext cwac && !cwac.isActive()) {
            if (cwac.getParent() == null) {
                ApplicationContext parent = loadParentContext(servletContext);
                cwac.setParent(parent);
            }
            configureAndRefreshWebApplicationContext(cwac, servletContext);
        }
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.rootContext);

        // 다양한 코드들 ...

        return this.rootContext;
    }
    catch (RuntimeException | Error ex) {
        logger.error("Context initialization failed", ex);
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
        throw ex;
    }
}

ServletContext의 하나의 Attribute로 ApplicationContext를 넣어주는 것을 볼 수 있어요

이 말은 즉, ServletContextApplicationContext영역 와는 다른 영역이기 때문에, Bean의 개념이 없어요.

따라서 간접적으로라도 Bean들을 꺼내어 사용하려면 Attribute로서 넣어주어야 합니다.

WebApplicationContext 즉, ApplicationContext 객체를 통해서 FilterChainProxy를 연결해야 했을까?

이거는 어느 정도 제 뇌피셜 + GPT 피셜을 감미해보면

아마 진짜 기본적인 구성은 @EnableAutoConfiguration을 통해서 Security도 기본설정이 완료될 겁니다.

그런데 보안이란 것은 사실 애플리케이션 전체를 보았을 때 하나의 구성모듈이라고 생각되거든요

그렇다면 필요 없을 때는 끄는 것도 사실 가능해야 할 겁니다.

그래서 중간 관리자인 FilterChainProxy가 있는 것이 아닐까 추측해 볼 수 있어요.

다음에는 흔히 SecurityFilterChain을 Bean으로 등록할 때 설정하는 addFilterBefore() 메서드가 어떻게 동작하고 SecurityFilter들의 순서가 어떻게 잡히는지 살펴볼 거예요.