SpringBootApplication.run()탐험기

ttomy·2023년 4월 23일
1

spring 모르겠다!

spring mvc를 공부하면서 아래와 같은 그림을 보아왔다.

spring mvc를 써보며 생긴 의문이 있다.

  • 정말 위의 그림처럼 dispatcher servlet이 http요청을 받아 처리를 할까?
    톰캣 서버로 들어온 요청을 dispatcher servlet으로 어떻게 넘기는 걸까?

    -> (톰캣은 따로 돌아가는 서버이고 spring boot application은 내 local에서 돌아가는 프로그램일 것 같은데, 톰캣 서버와 api로 통신하는게 아닌 이상 요청을 넘기는 다른 방법이 있나?)

위와 같은 궁금증이 생겨 spring 프로그램의 시작점인SpringBootApplication.run()을 따라가보았다.


dispatcherServlet의 생성자에 break point를 걸고 디버깅을 해 따라가본다.

SpringBootApplication.run()

위 이미지처럼 SpringBootApplication.run() 메서드를 따라가면 아래 이미지의 run()메서드에서 new SpringApplic ation()으로 생성자를 호출한 후 run()한다.

  • springApplication 생성자
    생성자에서는 애플리케이션 타입을 추론하고, bootstrapRegistryInitializers등을 초기화하는 등의 작업들을 한다. 필요한 작업들이지만, 일단 내 궁금증과 직결되는 부분은 아니니 넘어가겠다.

내가 보고싶은 곳은 run()부분이다.
아래와 같은 작업들을 수행한다.
spring application을 초기화하고 실행 할때의 흐름을 볼 수 있다.

  1. StopWatch로 실행 시간 측정 시작
  2. BootStrapContext 생성
  3. Java AWT Headless Property 설정
  4. 스프링 애플리케이션 리스너 조회 및 starting 처리
  5. Arguments 래핑 및 Environment 준비
  6. IgnoreBeanInfo 설정
  7. 배너 출력
  8. 애플리케이션 컨텍스트 생성
  9. Context 준비 단계
  10. Context Refresh 단계
  11. Context Refresh 후처리 단계
  12. 실행 시간 출력 및 리스너 started 처리
  13. Runners 실행

위와 같은 동작들을 한다.
디버깅하니 이 중 refreshContext()로 들어가 있다.

이 context Refresh단계에서 빈들을 찾아 싱글톤으로 인스턴스화 되고 웹서버를 실행하는 작업이 수행된다고 한다.

내가 보고싶은 건 tomcat과 servlet의 동작에 대한 부분이니 디버깅을 걸고 흐름을 쫓아가보려 한다.

tomcat생성자 디버깅

dispatcherServlet이 요청을 받아 적절한 controller로 매핑해 결과를 반환받는다고 한다.

그럼 톰캣은 spring boot프로젝트에서 어떻게 생성/실행되며, dispatcherServlet과 어떻게 연결 되는 걸까?

톰캣도 어쨋든 서버일텐데, 톰캣이 스프링context의 빈 중 하나인 DispatcherServlet을 공유해서 사용할 수가 있나?
톰캣 서버 초기화 시 스프링의dispatcherServlet으로 접근하는 경로를 제공하는 걸까?

참고: https://sigridjin.medium.com/servletcontainer%EC%99%80-springcontainer%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%A4%EB%A5%B8%EA%B0%80-626d27a80fe5
-> 톰캣도, spring boot프로젝트도 같은 jvm안에서 돌아가는
프로그램이므로 스프링context를 공유하는 게 아닐까 하는 생각이 든다.
하지만 더 공부해봐야 확실히 알것 같다.

Tomcat클래스의 생성자에 break Point를 걸어 디버깅을 해 살펴보자.

  • 디버깅 결과

전체적인 흐름은 SpringApplication.run()에서 시작해 context를 refresh할 때 dispatcherServlet과 관련된 빈이 등록된다.

이 때의 context는 위의 이미지의 우측하단 context에서 보듯이
AnnotationConfigServletWebServerApplicationContext 이다.

상속관계: AbstractApplicationContext <-extends--ServletWebServerApplicationContext <--extends--
AnnotationConfigServletWebServerApplicationContext

이들의 refresh()가 실행된다.
super.refresh() 하여 abstract클래스인 AbstractApplicationContext의 refresh()가 실행된다.

아래의 코드가 AbstractApplicationContext이다. 이 refresh()에서 일반적으로 context에 빈이 전체적인 흐름을 볼 수 있다.

@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) {
				...

컨텍스트에 실제로 빈을 intialize하는 부분은 onRefresh()이다.
디버깅도 여기에 걸려있다. 이동해보자.
이 onRefresh()의 실행되는 구현부는 AbstractApplicationContext를 상속받은 ServletWebServerApplicationContext.onRefresh()이다.
(아까 super.onRefresh()에서 하위의 onRefresh()를 호출해 다시 돌아왔다.)

이렇게 servlet빈이 등록되는 과정에서 ServletWebServerApplicationContext안의 TomcatServletWebServerfactory에서getWebServer()를 해올 떄 Server를 가진 Tomcat을 생성한다.

결론적으로 톰캣의 생성은
스프링context중 하나인AnnotationConfigServletWebServerApplicationContext과 그 모클래스들의 refresh()과정에서 톰캣Factory를 통해 WebServer를 가져올때 이뤄진다는 걸 알 수 있었다.

하지만 아직 dispatcherServlet이 톰캣에 어떻게 연결되는지는 확인하지 못했다.
위 이미지인 톰캣ServletWebServerFactory.getwebServer()에서 톰캣을 생성하고 초기화하는 부분을 살펴보면 DispatcherServlet관련 부분을 찾을 수 있을 것 같다.

하지만 이를 알아보려면 톰캣의 구조에 대해 어느정도 알아야 할 것 같다.

주의: 여기서의 context는 ServletContext로, spring의 ApplicationContext와는 다를 수 있다.

톰캣 서버의 service는 connector(coyote)를 통해 요청을 받아 변환하고 요청의 처리는 engine에게 넘긴다. engine은 host,context를 통해 ServletContext안의 매핑된 servlet에게 요청을 넘겨 요청을 처리하고 결과를 반환받는다.
톰캣,servlet Container에 대한 학습이 더 필요할 듯하다. 일단 이 정도로 이해하고 dispatcherServlet과의 연관성을 살펴보겠다. 톰캣의 ServletContext안에 DispatcherServlet 스프링 Bean이 있어 접근가능한 걸까?

DispatcherServlet.doService에 디버깅

servlet은 doService()(doGet,doPost...)을 통해 요청의 처리를 한다.
여기에 디버깅을 해보면 톰캣이 DispatcherServlet에 어떤 흐름으로 접근하는 지 알 것이다. localHost:8080으로 접근하면 breakPoint- doService()에 멈춘다.

  • 디버깅 결과

우선 Thread.run()에서 시작하는 것부터가 명확하게는 이해되지 않는다.
톰캣은 요청을 받으면 처리는 threadPool에서부터 쓰레드를 가져와 넘긴다고 하는데, 그 때문에 thread.run()부터 시작하는 건가 싶다.

전체적인 흐름은 taskThread가 run()되는데, threadPoolExecutor에서 에서 task가 수행되며 NioEndPoint.doRun()이 실행된다.
이 떄 이 endPoint에 대한 handler를가져오고 해당하는 protocol에 따라
Http1Processor.service()가 호출된다. 이 안에서 tomcat의 adapter인 coyoteAdapter.service()를 호출 -> EngineValve invoke -> HostValve invoke -> standardContextValve invoke 를 지나 ApplicationFilterChain들의 Filter들을 거쳐서 dispatcherServlet.service()가 호출된다.

여기서 주목할 점은 최종적으로 dispatcherServlet에 접근하는 것은 ApplicationFilter안에서라는 것이다.

FilterChain에서의 servlet = DispatcherServlet 객체라는 걸 위 이미지의 우측 하단에서 볼 수 있다.

그럼 ApplicationfilterChain에 어떻게 DispatcherServlet을 주입해준 걸까?

FilterChain에 어떻게 dispatcherServlet이 있었나

생성자에 할당하는 부분이 없어 setter를 이용해 할당할거라 예상하고 setServlet()부분에 디버깅을 걸어보았다.

그랫더니 ApplicationfilterChain의 초기화는ApplicationFilterFactory.createFilterChain()안에서
Servlet이 setServlet()되며 이뤄진다.

그러면 ApplicationFilterFactory는 어떻게 dispatcherServlet을 알고 있을까?
거슬러 올라가니
StadardWrapperValve.invoke()안에서
servlet = wrapper.allocate()로 할당되고,
createFilterChain()의 인자로 넘겨준다.

servlet = wrapper.allocate()에 디버깅을 걸고 들어가본다.
instance변수에 이미 dispatcherServlet을 가지고 있다.

어떻게 세팅해줬을까?
StandardWrapper안에는
setServletClass(), setServletName(),setServlet() 등의 servlet관련 함수가 존재한다. 모두 디버깅을 걸어보겠다.

  • 결과
    이에 대한 세팅은 springApplication.run()에서 refreshContext를 할 떄 일어났다. context refresh를 할때부터 StandardWrapper에는 Bean정보가 등록이 되고, 요청이 왔을 때에 실제 객체를 lazy-loadingg한다.
    (lazy-loading을 안하게 하는 방법도 존재한다.)

TomcatServerFactory.getWebServer()안에서
getTomcatWebServer()할떄, TomcatwebServer가 생성되며 initialize()된다.
이때 tomcat.start() -> tomcat안의 server.start() -> standardServer.start() -> engine.start() -> standardEngine.startInternal()되며
inLineExecutorService가 실행된다.
이때 tomcatEmbeddedContext.startInternal() -> TomcatStarter.onStartUp() -> DispatcherServletRegistraionBean.onStartUp()이 실행되며
ApplicationContext안에 addServlet()으로 dispatcherServlet을 등록한다.

이 addServlet()안에서 wrapper에 DispatcherServlet을 세팅해 주고,
filterChain은 wrapper로부터 DispatcherServlet을 받아 호출들에 대해 처리를 요청할 수가 있다.

여기서 거슬러올라가면 DispatcherServletRegistrationBean은 어떻게 dispatcherServlet정보를 알고있는 걸까?
결국 이를 알려면 spring boot가 bean들에 대한 Auto configuration을 어떻게 하는지 알아야 할 듯하다.

참고: https://www.marcobehler.com/guides/spring-boot-autoconfiguration
이에 대한 학습은 나중에 더 해봐야겠다.

학습 중이라 틀린 내용이 있을 수 있습니다. 알려주시면 정정해서 반영하겠습니다.

Reference

https://docs.spring.io/spring-framework/docs/3.0.0.M3/reference/html/ch16s02.html
https://mangkyu.tistory.com/213
https://mangkyu.tistory.com/212
https://mangkyu.tistory.com/216
https://velog.io/@hyunjae-lee/Tomcat-2-%EA%B5%AC%EC%A1%B0
https://sigridjin.medium.com/servletcontainer%EC%99%80-springcontainer%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%A4%EB%A5%B8%EA%B0%80-626d27a80fe5
https://tecoble.techcourse.co.kr/post/2021-06-25-dispatcherservlet-part-1/

0개의 댓글