DispatcherServlet과 스프링 컨테이너 생성 과정

ssongkim·2022년 1월 17일
3
post-thumbnail

앞서 DispatcherServlet이 무엇인지, 스프링 빈과 이를 관리하는 역할을 수행하는 스프링 컨테이너에 대해 알아보았다.
이번 시간에는 DispatcherServlet과 스프링 컨테이너가 어떻게 생성되는지 생성 과정을 알아보도록 하겠다.

DispatcherServlet의 생성시점

DispatcherServlet는 하나의 서블릿이며 톰캣이 실행돼 서블릿 컨테이너(Tomcat)가 web.xml 배포기술자 파일을 통해 웹 애플리케이션의 서블릿 컨텍스트를 초기화하는 시점에 옵션에 따라 lazy loading혹은 pre loading방식으로 생성된다.

서블릿 컨텍스트


하나의 톰캣으로 여러 웹 애플리케이션을 띄울 수 있는데 ServletContext란 톰캣이 실행되면서 서블릿과 서블릿 컨테이너 간 연동을 위해 사용되는 Context로,
하나의 웹 애플리케이션마다 하나의 서블릿 컨텍스트를 가진다. 서블릿 컨테이너가 생성될 때 컨텍스트가 생성되고 종료되면 소멸된다. [이전 게시글 Tomcat부분 참고]

DispatcherServlet 생성방법

DispatcherServlet은 서블릿 컨테이너가 컨텍스트를 초기화하는 시점에 생성된다고 하였다. Tomcat 내부의 웹애플리케이션마다 하나씩 존재하는 서블릿 컨텍스트를 초기화하는 방법이 여러 존재한다.

1. web.xml 배포기술자 설정

webapp/WEB-INF 디렉토리 안에 web.xml을 설정해서 서블릿 컨텍스트를 초기화하며 DispatcherServlet을 생성하는 방법이다.

web.xml

...
...
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
		
	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
...
...

Lazy Loading 생성

디스패처 서블릿은 하나의 서블릿이다. 서블릿이 lazy loading 방식일 경우 톰캣 구동 시 서블릿 인스턴스를 생성해두지 않으므로 처음에는 서블릿 컨테이너에 DispatcherServlet 인스턴스가 없을 것이다. 클라이언트로부터 최초로 요청을 받을 때 서블릿 컨테이너는 DispatcherServlet에 대한 객체를 생성하고 다음 요청부터는 싱글톤으로 활용한다.

Pre Loading 생성

해당 서블릿에 load-on-startup옵션이 있다면 pre loading 방식으로 서블릿 인스턴스를 생성한다는 의미이며 서블릿 컨텍스트를 초기화하는 시점에 미리 DispatcherServlet 인스턴스를 생성한다.

servlet-context.xml는 DispatcherServlet 내부에서만 사용하고자하는 애플리케이션 컨텍스트 생성을 위한 설정파일인데 밑에 스프링 컨테이너 생성부분에서 다루도록 하겠다.

2. WebApplicationInitializer 구현

서블릿3.0버전 이후부터 web.xml 없이도 서블릿 컨텍스트 초기화 작업이 가능해졌다. 프레임워크 레벨에서 직접 초기화할 수 있게 도와주는 ServletContainerInitializer API를 제공하기 때문이다.
스프링 부트를 사용할 때 web.xml이 없었던 이유도 여기있던 것..!

스프링은 웹 모듈내에 이미 ServletContainerInitializer를 구현한 클래스가 포함되어 있고, 이는 스프링 웹 애플리케이션이 최초로 구동되었을 때 WebApplicationInitializer 인터페이스를 구현한 클래스를 찾아 초기화 작업을 위임하는 역할을 수행한다.

처음에 스프링 프레임워크 프로젝트를 생성하면 web.xml 방식일 것이다. 이를 ServletContainerInitializer 구현체 방식으로 변경할 수가 있다. 그 방법은 따로 알아보자

WebApplicationInitializer 구현체 자바 코드

public class WebInitializer implements WebApplicationInitializer{
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {        
        ...
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.setConfigLocation("com.studyspring.basic.config");
        
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("DispatcherServlet", new DispatcherServlet(context));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
        ...
        ...
    }    
}

이 코드는 내가 web.xml 형태를 ServletContainerInitializer 형태로 변경하며 작성한 코드이다. 톰캣의 서블릿 컨테이너가 구동되면 웹 애플리케이션의 서블릿 컨텍스트를 생성하고 초기화 하기위해 WebApplicationInitializer 인터페이스를 구현한 클래스를 찾아 onStartUp 메서드를 호출한다.

onStartUp메서드가 호출되면서 DispatcherServlet을 서블릿 컨텍스트에 동적으로 add해주는 것을 확인하며 DispatcherServlet이 생성되는 것을 확인해볼 수 있다.
addServlet 메소드는 서블릿 인스턴스를 동적으로 servletContext에 추가하는 메서드이다!

AnnotationConfigWebApplicationContext 코드 부분은 Application Context를 생성한다는 의미로 밑에서 설명하도록 하겠다.

스프링 컨테이너(Apllication Context) 생성 시점

Application Context는 스프링이 관리하는 빈들이 담겨 있는 컨테이너이다. 스프링 컨테이너는 DispatcherServletinit하는 시점에 생성된다.

우리는 두가지 WebApplicationContext구현체를 사용할 수 있다.

  1. XmlWebApplicationContext
  2. AnnotationConfigWebApplicationContext

즉 다양한 방법의 DispatcherServlet + 스프링 컨테이너 생성 방법이 존재한다.

1. WebApplicationInitializer구현 + XmlWebApplicationContext 사용
2. WebApplicationInitializer구현 + AnnotationConfigWebApplicationContext 사용
3. web.xml설정 + XmlWebApplicationContext 사용
4. web.xml설정 + AnnotationConfigWebApplicationContext 사용

요즘 대세는 당근 WebApplicationInitializer구현 + 자바 설정 파일방식이다. 스프링 부트도 이런 형태인듯.
2번과 3번에 대해서 구현 방법을 알아보겠다 나머지는 이걸 응용하면 비슷비슷하다.

WebApplicationInitializer구현 + AnnotationConfigWebApplicationContext방식

ServletContainerInitializer 구현 방식의 Spring Framework에서 서블릿 컨텍스트를 초기화하며 DispacherServlet, ApplicationContext가 생성되는 곳은 어디인가 확인해보자

public class WebInitializer implements WebApplicationInitializer{
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {        
        ...
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.setConfigLocation("com.studyspring.basic.config");
        
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("DispatcherServlet", new DispatcherServlet(context));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
        ...
        ...
    }    
}

톰캣의 서블릿 컨테이너가 구동되면 웹 애플리케이션의 서블릿 컨텍스트를 생성하고 초기화 하기위해 WebApplicationInitializer 인터페이스를 구현한 클래스를 찾아 onStartUp 메서드를 호출한다, 이 안에 코드를 보면 AnnotationConfigWebApplicationContext 스프링 컨테이너를 생성하여 자바 설정 파일의 위치를 지정해주고 스프링 컨테이너를 생성한다.
그 다음 DispatcherServlet에서 사용하기 위해 매개변수로 넣고 DispatcherServlet 객체를 생성하며 DispatcherServlet을 서블릿 컨텍스트에 동적으로 add해주는 것을 확인할 수 있다.

web.xml설정 + XmlWebApplicationContext 사용방식

web.xml

...
...
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
...
...

servlet-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans ...>

	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->

	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>

	<context:component-scan base-package="com.studyspring.basic" />



</beans:beans>

servlet-context.xml는 DispatcherServlet 내에서 사용할 WebApplicationContext 스프링 컨테이너를 생성하기 위한 설정파일이다. 이름이 꼭 servlet-context.xml일 필요는 없다.
<init-param>부분에서 설정파일의 경로를 넣어주면 DispatcherServlet에서는 자신이 init되는 시점에 XmlWebApplicationContext를 이용해 스프링 컨테이너를 생성한다.
<annotation-driven/>이 뭔지, component-scan이 무엇인지 등등은 이전 게시글을 참고하자[이전게시글]

스프링 컨테이너 계층 구조

기본적으로 스프링에는 하나의 DispatcherServlet이 존재한다.
보통 그럴 일은 잘 없지만 web.xml을 수정하면 DispatcherServlet은 여러개 존재할 수 있다.
위에서 설명했듯 DispatcherServlet은 init되는 시점에 Application Context(스프링 컨테이너)를 생성하는데 여기서 사용되는 Application Context는 해당 DispatcherServlet에서만 사용할 수 있다.

여러 DispatcherServlet이 공유해야하는 ApplicationContext가 있을 수 있다. 이때 사용하는 것이 ContextLoaderListener이다.

web.xml에서 리스너 사용

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>

	<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/root-context.xml</param-value>
	</context-param>

	<!-- Creates the Spring Container shared by all Servlets and Filters -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<!-- Processes application requests -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
  ...
  ...
</web-app>

<context-param>는 서블릿 컨텍스트 내 모든 서블릿이 공유하는 일종의 변수를 선언하는 역할을 한다. <param-name>을 통해 변수 이름을 지정하고 <param-value>를 통해 값을 할당할 수 있으며 모든 서블릿은 해당 값을 꺼내 활용 가능하다.
<context-param>을 통해 contextConfigLocation라는 이름으로 전역 컨테이너 생성 시 불러오고자 하는 AppicationContext 설정파일 경로를 지정한다. 여기서는 설정파일이름이 root-context.xml이다.

ContextLoaderListener는 클라이언트의 요청이 없어도 컨테이너가 구동될 때 Pre-Loading 되는 객체이며 서블릿 컨텍스트 파라미터 중 contextConfigLocation 이름을 가진 파라미터에서 설정파일 경로를 읽어 서블릿 컨텍스트 내 모든 서블릿이 공유하고자하는 전역 Application Context를 생성한다.

주의할 점은 위 <conetxt-param> 으로 contextConfigLoaction을 따로 설정하지 않으면 기본적으로 /WEB-INF/applicationContext.xml 파일을 읽는 것을 시도한다. 파일이 없으면 스프링 컨테이너에 구동 시 오류가 날 수 있다.

WebApplicationInitializer 구현체 에서 리스너 사용

web.xml에서 뿐만 아니라 WebApplicationInitializer 구현체 방식에서도 ContextLoaderListener를 쓸 수 있다.

public class WebInitializer implements WebApplicationInitializer{
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {        

  	//각각 디스패처서블릿에서 사용할 ApplicationContext 여러개 생성
  	...
  	...
    	//DispacherServlet 여러개 생성 후 ApplicationContext 별도 지정
  	...
        ...
  	//공유할 ApplicationContext 별도 생성 및 리스너 등록
	AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext(); 
 	rootAppContext.register(RootAppContext.class);
 	ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
 	servletContext.addListener(listener);

        ...
    }    
}

컨테이너가 꼭 계층구조를 이룰 필요는 없다. DispatcherServlet이 하나라면 리스너는 별도로 등록하지 않아도 스프링MVC는 잘 작동했다.

컨테이너가 계층 구조를 이룰 경우

DispatcherServlet마다 별도로 사용하는 Application Context지역 컨테이너라 하고 ContextLoaderListener를 통해 공유하는 Application Context전역 컨테이너라고 해보겠다.

빈 탐색 시 지역 컨테이너를 우선적으로 확인하고 없을 경우 전역 컨테이너로 가서 탐색을 진행한다. 그래서 곂치는 빈이 있을 경우 지역 컨테이너를 우선하게 된다.

profile
鈍筆勝聰✍️

0개의 댓글