작성 개요
평소 개인적으로 사용하기도 하고 현업에서도 사용하고 있는 Spring을 충분히 이해하고 있는가?라는 생각을 가지고 동작 원리에 대해 다시 한 번 정리하게 되었습니다.
어디까지 분석할 것인가?
어디까지 쓸 것인가를 가장 고민하게 되었는데,
본 글을 쓰게 된 목적이 MVC 과정을 어떻게 수행하는지가 목적이기 때문에, 그 과정에서 개별 어노테이션이 어떻게 동작하는지 까지 분석하는 것은 오버스펙일듯 하여 추후 세부적으로 접근 가능하게끔 하는 가이드 문서쯤 어딘가..? 라는 글을 적는게 좋을 것 같았습니다..ㅎㅎ
SpringBoot의 시작
SpringBoot는 어떻게 시작될까요?
SpringBoot는 다음과 같이 다양한 실행 방식을 제공합니다.
- Intellij 사용 시, [Gradle > bootRun]
- command 사용 시, java -jar {jar파일경로}
- Application의 main 메서드 실행
그런데, 그거 아시나요..?? 사실 위 과정 모두 결국 Application의 main 메서드를 실행한다는 사실을..😱
Spring, SpringBoot 또한 결국 Java를 실행하는 매개체이기 때문에, 결국 진입점이 되는 main메서드가 필요합니다.
그래서, main 메서드를 실행하면 어떻게 동작하는데??
제일 궁금한 부분이죠. 우리가 설정했던 @Configuration, @Component는 어떻게 동작하며.. 내장 톰캣은 사용한다는데, 어디서 주입이 되고.. 싱글톤이라는 Bean은 어떻게 관리가 되는 것인가..🧐
이번 글에서는 main 메서드가 실행될 때, Configuration이 설정되는 부분과 Component를 어디서 스캔하게 되고, Context는 어떻게 관리되며 톰캣은 어디서 띄우게 되는지를 알아보려고 합니다.
시작하기에 앞서, main 메서드 내부의 `SpringApplication`의 run 메서드 내부는 다음과 같이 구성되어 있습니다.
public ConfigurableApplicationContext run(String... args) {
Startup startup = SpringApplication.Startup.create();
if (this.registerShutdownHook) {
shutdownHook.enableShutdownHookAddition();
}
DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
ConfigurableApplicationContext context = null;
this.configureHeadlessProperty();
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
Throwable ex;
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = this.printBanner(environment);
context = this.createApplicationContext(); // 설정 반영
context.setApplicationStartup(this.applicationStartup);
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); // 설정 반영
this.refreshContext(context); // 톰캣 재시작
this.afterRefresh(context, applicationArguments);
startup.started();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), startup);
}
listeners.started(context, startup.timeTakenToStarted());
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
ex = var10;
throw this.handleRunFailure(context, ex, listeners);
}
try {
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
return context;
} catch (Throwable var9) {
ex = var9;
throw this.handleRunFailure(context, ex, (SpringApplicationRunListeners)null);
}
}
Spring은 그 규모가 매우 크기 때문에, 위 코드에서도 볼 수 있듯이 세부적인 용도에 맞게 메서드들이 세밀하게 분리되어 있습니다.
우리가 사용하는 @ComponentScan을 통해 관리되는 @Configuration, @Controller 등의 어노테이션은 내부적으로 createApplicationContext()와 prepareContext() 과정을 거쳐 동작합니다.
이 과정에서 ConfigurationClassPostProcessor 클래스가 어노테이션 설정 작업을 처리하며 중요한 역할을 합니다.
(어노테이션의 동작 원리에 대한 내용은 별도의 글에서 더 깊이 다뤄볼 예정입니다.)
내장 톰캣 실행 원리
저는 SpringBoot를 공부하면서 "내장 톰캣"이 가장 신기했는데요, 예전에 학교를 다니면서 Spring을 공부했을 때는 이클립스에서 톰캣 설정을 따로 해줬어야 하는 엄청 불편함을 겪었었습니다.
그래서 SpringBoot에서는 이러한 불편함을 어떻게 해소했을지 궁금해서 내부를 한 번 살펴보니,
SpringApplication의 run 메서드 내부에는 다음과 같이 createApplicationContext()를 통해 현재 실행하려는 서버의 타입 기반의 정보를 넘겨주게 되고, 이를 기반으로 웹서버를 띄우는 작업을 하게 됩니다.
class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
...
try {
...
context = this.createApplicationContext();
...
}
}
protected ConfigurableApplicationContext createApplicationContext() {
return this.applicationContextFactory.create(this.webApplicationType); // 일반적인 MVC 서비스는 아래 Servlet을 호출
}
}
---
class ServletWebServerApplicationContextFactory implements ApplicationContextFactory {
public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {
return webApplicationType != WebApplicationType.SERVLET ? null : this.createContext();
}
private ConfigurableApplicationContext createContext() {
return (ConfigurableApplicationContext)(!AotDetector.useGeneratedArtifacts() ? new AnnotationConfigServletWebServerApplicationContext() : new ServletWebServerApplicationContext());
}
}
톰캣은 위 코드에서 마지막으로 호출 된 AnnotationConfigServletWebServerApplicationContext에 의해서 생성되는데,
해당 클래스는 ServletWebServerApplicationContext를 확장한 클래스로, 아래 단계를 통해 WebServer를 호출하게 됩니다.
public class AnnotationConfigServletWebServerApplicationContext extends ServletWebServerApplicationContext implements AnnotationConfigRegistry {
...
public AnnotationConfigServletWebServerApplicationContext(String... basePackages) {
this();
this.scan(basePackages);
this.refresh(); // <==== 생성자에서 refresh 호출
}
...
}
public class ServletWebServerApplicationContext extends GenericWebApplicationContext implements ConfigurableWebServerApplicationContext {
...
protected void onRefresh() {
super.onRefresh();
try {
this.createWebServer(); // <==== 웹서버 생성
} catch (Throwable var2) {
Throwable ex = var2;
throw new ApplicationContextException("Unable to start web server", ex);
}
}
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = this.getServletContext(); // <==== Tomcat 서버를 가져옴
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
ServletWebServerFactory factory = this.getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
createWebServer.end();
this.getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.webServer));
this.getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this, this.webServer));
} else if (servletContext != null) {
try {
this.getSelfInitializer().onStartup(servletContext);
} catch (ServletException var5) {
ServletException ex = var5;
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
this.initPropertySources();
}
// 웹서버 팩토리에서 실행할 웹서버를 가져옵니다.
protected ServletWebServerFactory getWebServerFactory() {
String[] beanNames = this.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new MissingWebServerFactoryBeanException(this.getClass(), ServletWebServerFactory.class, WebApplicationType.SERVLET);
} else if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
} else {
return (ServletWebServerFactory)this.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
}
...
}
getWebServer()는 어떤 웹서버들을 가져올 수 있을까요??
getWebServer()로 호출할 수 있는 구현체들은 다음과 같습니다. 아래 사진의 구현체를 보시면 WebFlux에서 사용하는 Jetty와 MVC에서 사용하는 Tomcat을 확인할 수 있습니다.
톰캣 실행도 알겠고.. ComponentScan도 어떻게든 설정되는건 알겠는데, 우리가 생성한 Bean은 어떻게 관리되는거야..??
Bean 생성과 관리는 Spring 개념의 꽃이라고 할 수 있습니다.
Spring의 @ComponentScan으로 감지된 Class는 BeanDefinitionRegistry의 registerBeanDefinition 메서드 내부에서 다음과 같이 존재하는 BeanName인지를 검사하는 로직을 확인할 수 있습니다.
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
...
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
...
BeanDefinition existingDefinition = (BeanDefinition)this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) { // 존재하는 Bean인지 검사
if (!this.isBeanDefinitionOverridable(beanName)) {
throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
}
...
this.beanDefinitionMap.put(beanName, beanDefinition);
} else {
if (this.isAlias(beanName)) {
String aliasedName = this.canonicalName(beanName);
if (!this.isBeanDefinitionOverridable(aliasedName)) {
if (this.containsBeanDefinition(aliasedName)) {
throw new BeanDefinitionOverrideException(beanName, beanDefinition, this.getBeanDefinition(aliasedName));
}
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Cannot register bean definition for bean '" + beanName + "' since there is already an alias for bean '" + aliasedName + "' bound.");
}
this.removeAlias(beanName);
}
...
}
if (existingDefinition == null && !this.containsSingleton(beanName)) {
if (this.isConfigurationFrozen()) {
this.clearByTypeCache();
}
} else {
this.resetBeanDefinition(beanName);
}
}
...
}
정리
Spring Boot가 웹 애플리케이션 서버와 Bean 관리를 어떻게 하는지 간단히 알아보았는데요,
Spring Boot는 SpringApplication.run()을 통해 실행되고, 내부적으로 ConfigurationClassPostProcessor 클래스를 통해 어노테이션 등의 설정이 등록됩니다. 이 설정이 등록되고, @ComponentScan으로 감지 된 클래스 들이 BeanDefinitionRegistry를 통해 관리된다는 것을 알게 되었네요.
또한, Web Application Server인 Tomcat이 어떻게 내부적으로 실행되는지 알게 된 시간이었습니다.
글을 작성하면서 문득 "내가 정말 Spring을 잘 알고 있는 걸까?"라는 생각이 들어서 시작한 내부 분석을 통해 Spring과 조금 더 친해지는 계기가 되었습니다.
'Spring Framework > spring' 카테고리의 다른 글
[SpringBoot] ApplicationEvent를 활용한 이벤트 발행/구독 (0) | 2024.12.28 |
---|---|
[SpringBoot] @ComponentScan 동작 원리 (0) | 2024.12.09 |
MSA 환경에서 Kotlin + Spring Rest Docs + Swagger UI 적용 (0) | 2023.09.14 |
Spring 컨트롤러 테스트 + Rest Docs에서 시간 개선 (0) | 2023.09.13 |
@RequestBody와 @ModelAttribute (0) | 2020.04.29 |