스프링 부트와 서블릿
스프링 프레임 워크를 공부하면서, 서블릿, 서블릿 컨테이너, 서블릿 컨텍스트 등에 궁금해졌습니다. 각각의 용어를 정리하고, 스프링 부트가 어떻게 실행 되는지 확인을 해보고자 합니다.
- 서블릿 (Servlet):
- Java에서 HTTP 요청을 처리하기 위해 사용되는 Java 클래스입니다.
- HTTP GET, POST, PUT, DELETE 등의 요청을 처리할 수 있습니다.
- 웹 페이지의 동적 콘텐츠 생성을 위해 사용됩니다.
- 서블릿 컨텍스트 (ServletContext):
- 웹 애플리케이션의 전체 범위에 걸쳐 공유되는 정보를 저장하는 객체입니다.
- 웹 애플리케이션의 전체 생명 주기 동안 유지되며, 모든 서블릿과 JSP는 동일한 ServletContext 객체를 공유합니다.
- 서블릿 간의 데이터 공유, 애플리케이션 수준의 초기화 파라미터 접근, 웹 애플리케이션의 다양한 리소스에 접근하는 기능을 제공합니다.
- 서블릿 컨테이너 (Servlet Container):
- 서블릿의 실행 환경을 제공하는 웹 서버 컴포넌트입니다.
- HTTP 요청을 받아서 적절한 서블릿에 전달하고, 서블릿의 실행 결과를 웹 브라우저에게 반환하는 역할을 합니다.
- 서블릿의 생명 주기를 관리합니다 (생성, 초기화, 서비스, 제거).
- Tomcat, Jetty, WildFly 등이 대표적인 서블릿 컨테이너입니다.
쉽게 요약하자면 다음과 같습니다.
- 서블릿 컨테이너는 웹 애플리케이션을 실행하는 환경을 제공합니다. 웹 애플리케이션 내부에는 여러 개의 서블릿이 있을 수 있습니다.
- 서블릿 컨텍스트는 웹 애플리케이션 내 모든 서블릿들 사이에 공통으로 사용되는 정보를 저장하고 공유하는 역할을 합니다.
- 각 서블릿은 요청을 처리하기 위한 비즈니스 로직을 포함하며, 서블릿 컨테이너의 관리 하에 실행됩니다.
그렇다면 spring boot가 실행 될 때는 어떤 과정을 거치는 지 확인하겠습니다.
- Spring Boot 애플리케이션 시작: public static void main 메서드에서 SpringApplication.run() 메서드가 호출됩니다. 해당 메서드는 Spring Boot 애플리케이션의 시작점입니다.
- ApplicationContext 생성: SpringApplication 클래스는 애플리케이션 유형 (웹 애플리케이션, 배치 애플리케이션 등)을 판별합니다. webApplicationType이 서블릿인 경우 AnnotationConfigServletWebServerApplicationContext 이 사용됩니다. 이 컨텍스트는 Spring Boot 애플리케이션의 주 ApplicationContext로, 모든 스프링 빈과 구성을 포함합니다.
- Servlet 컨테이너 준비: 위의 ApplicationContext 내부에서는 ServletWebServerFactory 빈 (예: TomcatServletWebServerFactory)을 찾아서 해당 빈을 사용하여 서블릿 컨테이너 인스턴스를 생성하고 준비합니다.
- Servlet 컨텍스트 생성: 서블릿 컨테이너가 시작되면서 웹 애플리케이션을 위한 ServletContext가 생성됩니다. 이때, SpringBootServletInitializer와 관련된 설정이 있으면 해당 설정이 적용됩니다.
- DispatcherServlet 등록: DispatcherServlet은 Spring MVC의 중심 컴포넌트로 웹 요청을 적절한 컨트롤러로 전달합니다. Spring Boot는 DispatcherServlet을 자동으로 생성하고 서블릿 컨테이너에 등록합니다. DispatcherServlet은 자신만의 WebApplicationContext를 가지며, 이는 주 ApplicationContext (위에서 생성된 것)의 자식 컨텍스트로 설정됩니다.
- Servlet 컨테이너 시작: 위의 모든 설정과 등록 작업이 완료되면, Tomcat과 같은 서블릿 컨테이너는 시작되며 웹 요청을 수신 대기합니다.
요약하면, Spring Boot는 애플리케이션 시작 시 주 ApplicationContext를 생성하며, 웹 애플리케이션의 경우 내장된 서블릿 컨테이너 (예: Tomcat)도 함께 시작합니다. 서블릿 컨테이너가 시작될 때, ServletContext 및 DispatcherServlet (⇒ WebApplicationContext)가 생성되고 설정됩니다. 이 때, 주 ApplicationContext는 DispatcherServlet의 WebApplicationContext의 부모 컨텍스트로 설정됩니다.
확인을 위해 코드를 보며 확인을 하겠습니다. 버전은 3.1.1 입니다
1 Spring Boot 애플리케이션 시작
//SpringApplication.java
public class SpringApplication {
...
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
...
// 실재 run 실행.
public ConfigurableApplicationContext run(String... args) {
long startTime = System.nanoTime();
// BootStrapContext 생성
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
// Java AWT Headless Property 설정
configureHeadlessProperty();
// 스프링 애플리케이션 리스너 조회 및 starting 처리
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// Arguments 래핑 및 Environment 준비
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 배너 출력
Banner printedBanner = printBanner(environment);
// 어플리케이션 context 생성
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
// Context 준비 단계
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// Context Refresh 단계
refreshContext(context);
// Context Refresh 후처리 단계
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
// 실행 시간 출력 및 리스너 started 처리
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
// Runners 실행
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
if (ex instanceof AbandonedRunException) {
throw ex;
}
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
if (context.isRunning()) {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
}
}
catch (Throwable ex) {
if (ex instanceof AbandonedRunException) {
throw ex;
}
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
}
2 ApplicationContext 생성
//SpringApplication.java
class SpringApplication {
...
// 실재 run 실행.
public ConfigurableApplicationContext run(String... args) {
...
// 어플리케이션 context 생성
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
...
}
// factory로 생성 위임
protected ConfigurableApplicationContext createApplicationContext() {
return this.applicationContextFactory.create(this.webApplicationType);
}
}
// DefaultApplicationConextFactory에서 webApplicationType에 맞는 Context 제공한다
class DefaultApplicationContextFactory implements ApplicationContextFactory {
...
@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 ConfigurableApplicationContext createDefaultApplicationContext() {
if (!AotDetector.useGeneratedArtifacts()) {
return new AnnotationConfigApplicationContext();
}
return new GenericApplicationContext();
}
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;
}
}
3 Servlet 컨테이너 준비
//SpringApplication.java
class SpringApplication {
...
// 실재 run 실행.
public ConfigurableApplicationContext run(String... args) {
...
// Context Refresh 단계
refreshContext(context);
}
}
// AbstractApplicationContext 호출
class AbstractApplicationContext{
...
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
contextRefresh.end();
}
}
}
...
}
class ServletWebServerApplicationContext{
...
@Override
public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.stop();
}
throw ex;
}
}
...
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
StartupStep createWebServer = getApplicationStartup().start("spring.boot.webserver.create");
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
// 맞는 webServer 가져온다, ServletWebServerFactory
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
// web서버 실행한다
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
}
@FunctionalInterface
public interface ServletWebServerFactory extends WebServerFactory {
// JettyServletWebServerFactory
// TomcatServletWebServerFactory
// UndertowServletWebServerFactory
// 현재 위의 3개의 클래스가 해당 메서드를 구현한다
WebServer getWebServer(ServletContextInitializer... initializers);
}
refreshContext 단계에서 적절한 서블릿 컨테이너(tomcat, netty 등이 구동 된다)
4 Servlet 컨텍스트 생성
// tomcat의 경우 입니다
class TomcatServletWebServerFactory{
...
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
for (LifecycleListener listener : this.serverLifecycleListeners) {
tomcat.getServer().addLifecycleListener(listener);
}
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}
...
// Tomcat의 Context (즉, StandardContext 객체)를 준비하고 구성합니다
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
File documentRoot = getValidDocumentRoot();
TomcatEmbeddedContext context = new TomcatEmbeddedContext();
if (documentRoot != null) {
context.setResources(new LoaderHidingResourceRoot(context));
}
context.setName(getContextPath());
context.setDisplayName(getDisplayName());
context.setPath(getContextPath());
File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase");
context.setDocBase(docBase.getAbsolutePath());
context.addLifecycleListener(new FixContextListener());
ClassLoader parentClassLoader = (this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
: ClassUtils.getDefaultClassLoader();
context.setParentClassLoader(parentClassLoader);
resetDefaultLocaleMapping(context);
addLocaleMappings(context);
try {
context.setCreateUploadTargets(true);
}
catch (NoSuchMethodError ex) {
// Tomcat is < 8.5.39. Continue.
}
configureTldPatterns(context);
WebappLoader loader = new WebappLoader();
loader.setLoaderInstance(new TomcatEmbeddedWebappClassLoader(parentClassLoader));
loader.setDelegate(true);
context.setLoader(loader);
if (isRegisterDefaultServlet()) {
addDefaultServlet(context);
}
if (shouldRegisterJspServlet()) {
addJspServlet(context);
addJasperInitializer(context);
}
context.addLifecycleListener(new StaticResourceConfigurer(context));
ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
host.addChild(context);
configureContext(context, initializersToUse);
postProcessContext(context);
}
}
5 DispatcherServlet 등록
DispatcherServletAutoConfiguration은 DispatcherServlet의 ServletRegistrationBean을 제공합니다. 이 빈은 DispatcherServlet의 등록 정보를 포함하고 있습니다
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfiguration(after = ServletWebServerFactoryAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
public class DispatcherServletAutoConfiguration {
....
@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
@Import(DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
}
...
}
해당 DispatcherRegistrationBean은 다음과 같습니다.
ServletWebServerApplicationContext는 TomcatStarter를 사용하여 ServletContextInitializer 빈들을 실행합니다. ServletRegistrationBean은 ServletContextInitializer를 구현하기 때문에, 이 단계에서 DispatcherServlet이 실제로 Tomcat의 ServletContext에 등록됩니다. 아래는 해당 코드 및 의존성 graph입니다.
// StandardContext
class StandardContext{
...
public boolean loadOnStartup(Container children[]) {
...
// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(), getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
...
}
}
class TomcatStarter{
....
@Override
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
try {
for (ServletContextInitializer initializer : this.initializers) {
// Servlet들이 등록된다
initializer.onStartup(servletContext);
}
}
catch (Exception ex) {
this.startUpException = ex;
// Prevent Tomcat from logging and re-throwing when we know we can
// deal with it in the main thread, but log for information here.
if (logger.isErrorEnabled()) {
logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: "
+ ex.getMessage());
}
}
}
}
class DynamicRegistrationBean{
protected final void register(String description, ServletContext servletContext) {
D registration = addRegistration(description, servletContext);
if (registration == null) {
if (this.ignoreRegistrationFailure) {
logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");
return;
}
throw new IllegalStateException(
"Failed to register '%s' on the servlet context. Possibly already registered?"
.formatted(description));
}
configure(registration);
}
}
6 Servlet 컨테이너 시작
위의 모든 설정과 등록 작업이 완료되면, Tomcat과 같은 서블릿 컨테이너는 시작되며 웹 요청을 수신 대기합니다.