Springboot에서 SpringSecurity는 어떻게 초기화되는가? - 상
개요
흔히 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();
}
ServletContex
t의 구현체는 바로 ApplicationContex
였습니다.
되게 처음보고는 당황스러웠습니다.
Springboot 아키텍처적으로는 엄연하게 ServletContext
와 ApplicationContext
는 다른데 말이죠.
정리해 보자면 ApplicationContext
의 정체는 AnnotationConfigServletWebServerApplicationContext()
였고
SevletContext
의 정체는 ApplicationContext(org.apache.catalina.core)
였답니다.
SpringSecurity Architecture
공식 문서에 나와있는 아키텍처예요
일반적으로 Springboot 던 전통적인 Spring Framework 던
Client - Filter - DispatcherServlet - HandlerMapping - Interceptor(before) - Controller - Interceptor(after)
라는 전체 동작을 가지죠.
Filter
는 ServletContext
영역이기 때문에, 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
일 거예요.
WebApplicationContext
는 ApplicationContext
의 하위 인터페이스로서 Springboot의 ApplicationContext
라고 생각하면 돼요.
그럼 DelegatingFilterProxy
를 누가 만들까요?
Springboot에서는 SpringSecurityAutoConfiguration
가 DelegatingFilterProxyRegistrationBean
클래스를 통해 DelegatingFilterProxy
가 ServletContext
구성 이후 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
의 일종입니다.
RegistrationBean
은 ServletContextInitializer
의 일종으로, ServletContext
가 초기화되어 초기 구성에 필요한 객체들 혹은 행위들을 정의할 때 사용해요.
생각해 보면 DelegatingFilterProxy
도 상속은 복잡하지만 결국 Bean인 건데,
boot에서는 ApplicationContext
가 먼저 생겨나기에 Bean들을 미리 구성할 수 있는 걸로 압니다.
Bean을 로드하는 것으로 Filter
를 등록할 수는 없을까요?
기본적으로 Filter
는 ServletContext
에서만 등록이 가능해요.
ApplicationContext
를 통해 DelegatingFilterProxy
를 Bean으로 등록한 것이지, Filter
로서 등록한 건 아닙니다.
그래서 SerlvetContextIntializer
를 구현한 RegistrationBean
을 통해 ServletContext
초기화 시점에 등록되도록 구현하는 것이죠.
실제로 다음과 같은 코드를 통해 DelegatingFilterProxy
를 Filter
로서 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이 먼저 등록되어야 하네요.
그러려면 @EnableWebSecurity
속 WebSecurityConfiguration
을 먼저 이해해봅시다.
@EnableWebSecurity
흔히 SecurityConfig
라는 파일을 통해 SecurityFilterChain
Bean을 등록한다고 하면
@EnableWebSecurity
위와 같은 어노테이션을 흔히 사용합니다.
내부를 들여다보면 아래와 같고, 찾으려는 WebSecurityConfiguratio
n이 있어요.
@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가 켜질 때 구성하도록 되어있어요.
나머지의 경우에서는 @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부터 찾아야 합니다.
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
구성이 완료되고
DelegatingFilterProxyRegistrationBean
이 SecurityFilterAutoConfiguration
에 의하여 Bean으로 등록되고,
ServletContext
가 초기화 되는 시점에 Filter
로 등록되게 됩니다.
즉, 아까 targetBeanName
은 DelegatingFilterProxyRegistrationBean
의 생성자에서부터 넘어가게 되는데,
"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
를 넣어주는 것을 볼 수 있어요
이 말은 즉, ServletContext
는 ApplicationContext
영역 와는 다른 영역이기 때문에, Bean의 개념이 없어요.
따라서 간접적으로라도 Bean들을 꺼내어 사용하려면 Attribute로서 넣어주어야 합니다.
왜 WebApplicationContext
즉, ApplicationContext
객체를 통해서 FilterChainProxy
를 연결해야 했을까?
이거는 어느 정도 제 뇌피셜 + GPT 피셜을 감미해보면
아마 진짜 기본적인 구성은 @EnableAutoConfiguration
을 통해서 Security도 기본설정이 완료될 겁니다.
그런데 보안이란 것은 사실 애플리케이션 전체를 보았을 때 하나의 구성모듈이라고 생각되거든요
그렇다면 필요 없을 때는 끄는 것도 사실 가능해야 할 겁니다.
그래서 중간 관리자인 FilterChainProxy
가 있는 것이 아닐까 추측해 볼 수 있어요.
다음에는 흔히 SecurityFilterChain
을 Bean으로 등록할 때 설정하는 addFilterBefore()
메서드가 어떻게 동작하고 SecurityFilter
들의 순서가 어떻게 잡히는지 살펴볼 거예요.