관리 메뉴

생각해보기

스프링 부트와 서블릿 본문

Spring

스프링 부트와 서블릿

정한_s 2023. 8. 12. 12:19

스프링 프레임 워크를 공부하면서, 서블릿, 서블릿 컨테이너, 서블릿 컨텍스트 등에 궁금해졌습니다. 각각의 용어를 정리하고, 스프링 부트가 어떻게 실행 되는지 확인을 해보고자 합니다.

  1. 서블릿 (Servlet):
    • Java에서 HTTP 요청을 처리하기 위해 사용되는 Java 클래스입니다.
    • HTTP GET, POST, PUT, DELETE 등의 요청을 처리할 수 있습니다.
    • 웹 페이지의 동적 콘텐츠 생성을 위해 사용됩니다.
  2. 서블릿 컨텍스트 (ServletContext):
    • 웹 애플리케이션의 전체 범위에 걸쳐 공유되는 정보를 저장하는 객체입니다.
    • 웹 애플리케이션의 전체 생명 주기 동안 유지되며, 모든 서블릿과 JSP는 동일한 ServletContext 객체를 공유합니다.
    • 서블릿 간의 데이터 공유, 애플리케이션 수준의 초기화 파라미터 접근, 웹 애플리케이션의 다양한 리소스에 접근하는 기능을 제공합니다.
  3. 서블릿 컨테이너 (Servlet Container):
    • 서블릿의 실행 환경을 제공하는 웹 서버 컴포넌트입니다.
    • HTTP 요청을 받아서 적절한 서블릿에 전달하고, 서블릿의 실행 결과를 웹 브라우저에게 반환하는 역할을 합니다.
    • 서블릿의 생명 주기를 관리합니다 (생성, 초기화, 서비스, 제거).
    • Tomcat, Jetty, WildFly 등이 대표적인 서블릿 컨테이너입니다.

쉽게 요약하자면 다음과 같습니다.

  • 서블릿 컨테이너는 웹 애플리케이션을 실행하는 환경을 제공합니다. 웹 애플리케이션 내부에는 여러 개의 서블릿이 있을 수 있습니다.
  • 서블릿 컨텍스트는 웹 애플리케이션 내 모든 서블릿들 사이에 공통으로 사용되는 정보를 저장하고 공유하는 역할을 합니다.
  • 각 서블릿은 요청을 처리하기 위한 비즈니스 로직을 포함하며, 서블릿 컨테이너의 관리 하에 실행됩니다.

그렇다면 spring boot가 실행 될 때는 어떤 과정을 거치는 지 확인하겠습니다.

  1. Spring Boot 애플리케이션 시작: public static void main 메서드에서 SpringApplication.run() 메서드가 호출됩니다. 해당 메서드는 Spring Boot 애플리케이션의 시작점입니다.
  2. ApplicationContext 생성: SpringApplication 클래스는 애플리케이션 유형 (웹 애플리케이션, 배치 애플리케이션 등)을 판별합니다. webApplicationType이 서블릿인 경우 AnnotationConfigServletWebServerApplicationContext 이 사용됩니다. 이 컨텍스트는 Spring Boot 애플리케이션의 주 ApplicationContext로, 모든 스프링 빈과 구성을 포함합니다.
  3. Servlet 컨테이너 준비: 위의 ApplicationContext 내부에서는 ServletWebServerFactory 빈 (예: TomcatServletWebServerFactory)을 찾아서 해당 빈을 사용하여 서블릿 컨테이너 인스턴스를 생성하고 준비합니다.
  4. Servlet 컨텍스트 생성: 서블릿 컨테이너가 시작되면서 웹 애플리케이션을 위한 ServletContext가 생성됩니다. 이때, SpringBootServletInitializer와 관련된 설정이 있으면 해당 설정이 적용됩니다.
  5. DispatcherServlet 등록: DispatcherServlet은 Spring MVC의 중심 컴포넌트로 웹 요청을 적절한 컨트롤러로 전달합니다. Spring Boot는 DispatcherServlet을 자동으로 생성하고 서블릿 컨테이너에 등록합니다. DispatcherServlet은 자신만의 WebApplicationContext를 가지며, 이는 주 ApplicationContext (위에서 생성된 것)의 자식 컨텍스트로 설정됩니다.
  6. 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과 같은 서블릿 컨테이너는 시작되며 웹 요청을 수신 대기합니다.

 

 

https://gowoonsori.com/spring/architecture/

https://mangkyu.tistory.com/213

'Spring' 카테고리의 다른 글

jpa를 살펴보자  (0) 2023.10.09
spring security와 filter  (0) 2023.08.20
Autowire 동작 방식 알아보기  (0) 2023.08.05
Transactional 어노테이션에 대해 알아보자  (0) 2023.07.21
스프링 request와 경로 매핑  (0) 2023.07.07
Comments