본문 바로가기

Programing/JVM(Java, Kotlin)

후벼파는 스프링 - @RequestMapping의 원리

예전에 "Spring 3.0 시작 - Hello World 동작원리"라는 글에서 Spring MVC 템플릿을 분석하여 정리한 적이 있다.

그 당시에는 대강 org.springframework.web.servlet.DispatcherServlet에서 초기화를 해준다고 하는데, 어떤 원리로 동작하는지 내부 구현이 궁금해서 소스를 찾아보았다.


1) 시작은 web.xml

 내용은 크게 리스너 클래스를 등록하는 부분과 애플리케이션 요청을 처리할 서블릿에 대한 것으로 구분할 수가 있는데, 내가 관심을 가지고 있는 @RequestMapping은 후자랑 관련이 있다.

 결국 /라는 HTTP GET요청이 @RequestMapping(value = "/", method = RequestMethod.GET) 로 지정되어 있는 메서드의 코드까지 연결이 되는 것이 이 글의 추적범위이다.


2) DispatcherServlet의 계층 구조

 DispatcherServlet 클래스의 구현을 확인하기 위해 어떤 인터페이스와 클래스를 구현하고 있는지를 알 필요가 있었다. (사실은 필요 없었을지도..)


3) 시작은 init 메소드

 서블릿에 대해 공부를 해봤으면 서블릿의 생명주기에 대해 잘 알고 있을 것이다. 보통 아래와 같은 상태를 거치게 된다.

맞다. init() 메소드에 의해 초기화 할 문장들이 시작이 된다.

하지만 직접 init()이 호출이 되는 것은 아니고 꼬리에 꼬리를 물고 실행이 된다.

잘 따라가 보자.
(서블릿의 init()이 누가 호출을 하냐로 올라갈 수 있지만 실제 구현체에 따라 다를 수 있을 것이다. Apache Tomcat의 경우에는 org.apache.catalina.startup.Bootstrap의 main함수가 시작한다.)


4) init()의 꼬리의 꼬리를 물고

javax.servlet.GenericServlet.init()

-> org.springframework.web.servlet.HttpServletBean.init() -> initServletBean()

우선 추상 클래스 GenericServlet의 init() 메소드는 실제 상속받은 추상 클래스 HttpServletBean.init()로 위임된다. HttpServletBean가 추상 클래스라는 것은 이 클래스를 다른 클래스가 상속을 받아 구현을 해야 한다는 것이다.

그런데 HttpServletBean.init()의 내부에서는 initServletBean 메서드를 호출한다.


5) initServletBean()의 꼬리의 꼬리를 물고

 org.springframework.web.servlet.HttpServletBean.initServletBean()

 -> org.springframework.web.servlet.FrameworkServlet.initServletBean() -> initWebApplicationContext()


org.springframework.web.servlet.FrameworkServlet.initServletBean()의 코드를 옮겨보면 아래와 같다.

@Override

protected final void initServletBean() throws ServletException {

       getServletContext().log(

                    "Initializing Spring FrameworkServlet '" + getServletName() + "'");

       if (this.logger.isInfoEnabled()) {

             this.logger.info(

                    "FrameworkServlet '" + getServletName() + "': initialization started");

       }

       long startTime = System.currentTimeMillis();

 

       try {

             this.webApplicationContext = initWebApplicationContext();

             initFrameworkServlet();

       }

       catch (ServletException ex) {

             this.logger.error("Context initialization failed", ex);

             throw ex;

       }

       catch (RuntimeException ex) {

             this.logger.error("Context initialization failed", ex);

             throw ex;

       }

 

       if (this.logger.isInfoEnabled()) {

             long elapsedTime = System.currentTimeMillis() - startTime;

             this.logger.info(

                    "FrameworkServlet '" + getServletName()

                    + "': initialization completed in " + elapsedTime + " ms");

       }

}


스프링 MVC를 초기에 가동시킬 때 콘솔에 주르르륵 올라가는 메세지가 바로 위에 있다.

콘솔 메세지 예)

INFO : org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'appServlet': initialization started

INFO : org.springframework.web.context.support.XmlWebApplicationContext - Refreshing WebApplicationContext for namespace 'appServlet-servlet': startup date [Mon Feb 02 14:39:42 KST 2015]; parent: Root WebApplicationContext

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from ServletContext resource [/WEB-INF/spring/appServlet/servlet-context.xml]

INFO : org.springframework.context.annotation.ClassPathBeanDefinitionScanner - JSR-330 'javax.inject.Named' annotation found and supported for component scanning

INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring

INFO : org.springframework.beans.factory.support.DefaultListableBeanFactory - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@c182989: defining beans // ...

INFO : org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Mapped "{[/],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String com.test.example.HomeController.home(java.util.Locale,org.springframework.ui.Model)

INFO : org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped URL path [/resources/**] onto handler 'org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0'

INFO : org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'appServlet': initialization completed in 865 ms


내가 궁금하게 여겼던 부분도 위의 로그에 있다.

Mapped "{[/],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String com.test.example.HomeController.home(java.util.Locale,org.springframework.ui.Model)


6) initWebApplicationContext()의 꼬리의 꼬리를 물고

 보통 하위 클래스에게 구현을 할 수(미룰 수) 있게 하기 위한 메서드는 여태까지는 비어있었다. 하지만 org.springframework.web.servlet.FrameworkServlet.initWebApplicationContext()는 구현이 빡빡하게 되어 있었다.

이 메서드에서는 web.xml에서 등록하는 또 다른 ContextLoaderListener에 대한 객체에 대해 찾는 부분이 들어있다.

WebApplicationContext rootContext =

WebApplicationContextUtils.getWebApplicationContext(getServletContext());

 FrameworkServlet.initWebApplicationContext() -> createWebApplicationContext(WebApplicationContext rootContext);
 
-> createWebApplicationContext(ApplicationContext parent); -> configureAndRefreshWebApplicationContext(wac) -> 

 -> wac.refresh();


7) ConfigurableApplicationContext.refresh()

 여태까지는 상속을 통한 인다이렉션(indirection)이었다면, 이젠 파라메터로 받은 인터페이스의 메서드를 호출을 하였다. 의존성이 드디어 끊어졌다. ConfigurableApplicationContext 인터페이스는 AbstractApplicationContext의 구현에 의해 동작을 수행한다.

 org.springframework.context.ConfigurableApplicationContext <- org.springframework.context.support.AbstractApplicationContext


8) AbstractApplicationContext.refresh()

 refresh메소드 안에서는 여러가지 작업을 하는데, ConfigurableListableBeanFactory 인터페이스를 가지고 메서드를 호출한다.

디버그에서 보니 ConfigurableListableBeanFactory 인터페이스의 구현체는 DefaultListableBeanFactory 클래스임을 알 수 있었다.

public abstract class AbstractApplicationContext

             extends DefaultResourceLoader

             implements ConfigurableApplicationContext, DisposableBean 


 Mapped 메세지는 AbstractApplicationContext.finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) 메소드에서 beanFactory.preInstantiateSingletons() 메소드가 호출될 때 뿌려진다.

9) 빈팩토리

finishBeanFactoryInitialization 메소드는 아래와 같이 구현되어 있다.

protected void finishBeanFactoryInitialization(

   ConfigurableListableBeanFactory beanFactory) {

       // Initialize conversion service for this context.

       if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&

              beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {

              beanFactory.setConversionService(

       beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME,       ConversionService.class));

       }

 

       // Stop using the temporary ClassLoader for type matching.

       beanFactory.setTempClassLoader(null);

 

       // Allow for caching all bean definition metadata, not expecting further changes.

       beanFactory.freezeConfiguration();

 

       // Instantiate all remaining (non-lazy-init) singletons.

       beanFactory.preInstantiateSingletons();

}

디버그에서 확인한 ConfigurableListableBeanFactory인터페이스의 구현체는 DefaultListableBeanFactory였다.


어노테이션을 찾는 작업은 DefaultListableBeanFactory.preInstantiateSingletons() 메서드가 호출되기 전에 수행되는 것으로 보인다.

자세한 분석을 위해서는 DefaultListableBeanFactory 클래스에 대한 공부가 필요해서 이번에는 여기까지만 분석해보았다.