개요
Spring MVC구조에 대해 공부하고 사용하다가 어떻게 순수 java 코드를 통해 Spring-MVC라는 패턴을 인식하고 작동을 시켜줄 수 있을까 궁금했습니다. Spring Boot는 상당부분 많은 설정들을 알아서 해주기 때문에 Spring Framework를 사용해서 설정해보며 공부해보도록 하겠습니다.
워밍업
Servlet 3.0 이상부터 javax.servlet.ServletContainerInitializer의 등장으로 web.xml 대신 코드 기반으로 servlet, filter, listener 컴포넌트 등록이 가능합니다. 스프링을 이용한다면 SpringServletContainerInitializer를 통해 이 전략을 사용합니다. 최종으로 개발자가 실제 구현해야 할 것은 org.springframework.web.WebApplicationInitializer입니다.
Spring을 실행시키기 위해서 아래 사진을 통해 핵심 인터페이스를 보겠습니다.
1. ServletContainerInitializer
Spring-MVC를 위한 최상위 조상입니다. 스프링뿐만 아니라 서블릿을 코드 기반으로 관리하기 위한 첫 시작입니다. 웹 어플리케이션 시작 시, 라이브러리와 런타임이 알림을 받아 그에 반응해 서브릿, 필터, 리스너 등록을 수행합니다.
인터페이스의 내용은 아래와 같습니다.
package javax.servlet;
import java.util.Set;
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;
}
onStartup() 1개만 있습니다. 해당 인터페이스를 구현할 때 @HandleTypes가 있다면 해당 어노테이션에 있는 클래스의 상속 혹은 구현 클래스들을 onStartup(onStartup(java.util.Set>, javax.servlet.ServletContext)) 메서드의 매개변수로 전달합니다.
해당 인터페이스는 다양한 구현체가 있는데 스프링을 사용하므로 SpringServletContainerInitializer를 알아보겠습니다. 여기서 @HandleTypes를 사용합니다.
2. SpringServletContainerInitializer
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
Spring으로 코드 기반 설정이 가능한 SpringServletContainerInitializer 클래스는 ServletContainerInitializer 인터페이스를 구현합니다. SpringServletContainerInitializer 는 컨테이너 시작 시, 서블릿 호환 컨테이너가 로드하고 초기화되며 onStartup() 메서드를 호출합니다.(스프링 웹 모듈 JAR가 클래스 파일에 존재하는 경우) 이 때 @HandlesTypes에 있는 WebApplicationInitializer 구현체를 자동으로 검사합니다. ServletContainerInitializer와 WebApplicationInitializer를 연결해주는데 ServletContext를 WebApplicationInitializer 구현체에서 초기화하고 위임하는 책임이 있습니다. 2개 모두 onStartup() 메서드가 있어 내부적으로 굉장히 유사합니다.
또한 WebApplicationInitializer를 보조하는 역할에 가까우며, 사실상 WebApplicationInitializer 구현체를 정의하지 않으면 아무런 동작을 하지 않을만큼 의존적입니다.
주의할 것은 SpringServletContainerInitializer, WebApplicationInitializer 구현은 Spring MVC와 무조건적으로 결합(tied)해야 하는 부분은 없습니다(스프링 웹 모듈 JAR에 타입이 제공된다는 것 이외에) . 오히려 ServletContext 를 코드 기반으로 편리하게 설정하는 것이 더 큰 목적입니다. WebApplicationInitializer에는 Spring MVC 요소 이외에 서블릿, 필터, 리스너, 각종 필터를 등록 할 수 있습니다.
SpringServletContainerInitializer의 onStartup() 를 살펴보겠습니다.
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList();
Iterator var4;
if (webAppInitializerClasses != null) {
var4 = webAppInitializerClasses.iterator();
while(var4.hasNext()) {
Class<?> waiClass = (Class)var4.next();
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance());
} catch (Throwable var7) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
} else {
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
var4 = initializers.iterator();
while(var4.hasNext()) {
WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
initializer.onStartup(servletContext);
}
}
}
@HandleTypes에 정의된 클래스 혹은 구체적으로 구현 된 하위 클래스들은 SpringServletContainerInitializer 클래스의 onStartup()메소드의 Set<Class<?>> 안으로 들어옵니다. 따라서 프레임워크는 client-side에 해당 클래스를 구현한 내용들을 요청합니다. (만약 @HandleTypes(Page.class)를 정의했다면 Page.class를 구체적으로 구현하는게 client-side의 역할입니다.)
이 방식은 web.xml에 있는 프레임워크 정보들을 제공하거나 직접적으로 프레임워크를 호출하는 방식과는 정반대입니다. ServletContainerInitializer와 @HandleTypes로 IOC 패턴을 사용합니다. 개발자는 프레임워크를 사용하기 위해서 jar 의존성과 POJO(어노테이션이나 인터페이스의 구현체)를 결합하기만 하면 끝입니다.
이제 개발환경에 따라 DispatcherServlet을 등록해야 합니다.WebApplicationInitializer을 구현하거나, AbstractAnnotationConfigDispatcherServletInitializer을 구현하면 됩니다. 번호순서에 따라 3번을 확인하고 다음에 6번을 하겠습니다.
구현 방법을 보기에 앞서, DispatcherServlet의 구성과 작동 방식을 설명을 하고 넘어가겠습니다.
- DispatcherServlet이란?
servlet의 중심부입니다. Spring web MVC 구조는 요청기반으로 작동하는데 DispatcherServlet이 필요합니다. 요청된 URI를 기반으로 Controller에 요청들을 위임합니다. 즉, 요청은 Controller로 바로 이동하지 않고 이 DispatcherServlet이 가장 먼저 확인합니다. 맨 앞에서 Controller 하나가 받아서 처리하기 때문에 Front Controller라고도 합니다.
웹 어플리케이션에는 1개의 RootApplicationContext가 있으며 ServletApplicationContext는 여러 개 존재할 수 있습니다.
3. WebApplicationInitializer
public interface WebApplicationInitializer {
void onStartup(ServletContext var1) throws ServletException;
}
WebApplicationInitializer는 전통적인 web.xml 방식을 대신하여 코드 기반으로 servletContext를 조작할 수 있습니다. 해당 인터페이스를 구현하면 SpringServletContainerInitializer에 의해 자동으로 감지됩니다. WebApplicationInitializer를 구현 할 때 DispatcherServlet을 등록할 수 있습니다.
- 코드를 사용하지만 xml 파일 이용하기
public class MyWebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic dispatcher =
container.addServlet("dispatcher", new DispatcherServlet(appContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}
- 100% 코드 기반의 설정
public class MyWebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
// Create the 'root' Spring application context
AnnotationConfigWebApplicationContext rootContext =
new AnnotationConfigWebApplicationContext();
rootContext.register(AppConfig.class);
// Manage the lifecycle of the root application context
container.addListener(new ContextLoaderListener(rootContext));
// Create the dispatcher servlet's Spring application context
AnnotationConfigWebApplicationContext dispatcherContext =
new AnnotationConfigWebApplicationContext();
dispatcherContext.register(DispatcherConfig.class);
// Register and map the dispatcher servlet
ServletRegistration.Dynamic dispatcher =
container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}
기존 xml 방식을 100%로 코드 기반으로 대체할 수 있습니다. WebApplicationInitializer는 @Configuration 클래스들을 사용하면 스프링의 코드기반으로만 설정할 수 있습니다. 위의 코드의 경우 AppConfig.class와 DispatcherConfig.class 2개 @Configuration 설정이 필요합니다.
AnnotationConfigWebApplicationContext을 사용하면 xml을 사용하는 XmlWebApplicationContext 방식 이외에 @Configuration이 붙은 클래스가 등록되고 Spring Container를 초기화할 수 있습니다.
SpringConfig 클래스는 제가 만든 웹 프로젝트에서 실제로 사용했었던 코드입니다.
위의 사진에서 4번과 5번 과정이 자바 코드로 구현되는 순간입니다! 각각 AnnotationConfigWebApplicationContext에 의해 생성된 클래스들이 ContextLoaderListener와 DispatcherServlet에 의해 등록됩니다.
AnnotationConfigWebApplicationContext는 WebApplicationContext의 구현체입니다. WebApplicationContext는 ApplicationContext를 상속하며 Web과 관련한 몇가지 기능이 추가되었습니다. WebApplicationContext는 Spring IOC를 구현합니다. bean들과 DI를 포함합니다. 이것들은 Root WebApplication Context 혹은 Servlet WebApplication Context 형태로 감싸집니다.
4번 과정을 보겠습니다.
rootAppContext 객체의 생성은 곧 Spring Container 생성이며 그 이후, AbstractContextLoaderInitializer를 구현하는 ContextLoaderListener를 통해서 Listener에 등록합니다. 스프링의 root WebApplicationContext를 시작하고 종료시키는 lisnter로 ContextLoader와 ContextCleanupListener에 해당 기능을 위임합니다. ContextLoaderListener(WebApplicationContext context) 생성자를 통해 root web application context를 주입할 수 있습니다.
5번 과정을 보겠습니다.
servletAppContext 객체의 생성은 곧 Spring Container의 생성이며 DispatcherServlet이 요청을 받아들이도록 해당 servlet을 등록합니다. 특히 addServlet()하는 servletContext는 해당 onStartup(ServletContext servletContext) 의 매개변수입니다. 그래서 요청한 servletContext들은 DispatcherServlet에서 관리합니다. 아래의 setLoadOnStartup(1)는 servlet 중에 로딩 순서를 첫번째로 하겠다, addMapping("/")은 모든 경로로 요청하는 사항에 대해서 처리하겠다는 의미입니다.
Spring Container 생성 시에는 꼭 4번과 5번이 의존적이지 않습니다. 독립적으로 생성하고 모두 servletContext에 addListener()와 addServlet()을 통해 등록시키면 됩니다. 이렇게 설정하는 것도 가능하지만 6번 방식을 사용하면 좀 더 쉽게 설정이 가능합니다.
6. AbstractAnnotationConfigDispatcherServletInitializer
하나하나 클래스로 Root WebApplicationContext와 Servlet WebApplicationContext를 등록시켜야 했지만, AbstractAnnotationConfigDispatcherServletInitializer을 사용하면 더 간편하게 등록 할 수 있습니다.
- getServletMappings()
servlet.addMapping("/")와 같은 의미입니다. 이제 servlet의 경로를 설정하는 함수를 Override하면 됩니다.
- getServletConfigClasses()
DispatcherServlet을 포함한 Servlet Application Context를 등록합니다. 따로 dispatcherServlet함수를 사용하지 않아도 반환하는 class에 넣어주면 자동으로 해당 영역에 있는 Bean들을 등록해 줍니다.
- getRootConfigClasses()
ContextLoaderListener로 Root Application Context을 등록합니다. 따로 관련함수를 사용하지 않아도 반환하는 class에 넣어주면 알아서 Root WebAppContext로 설정합니다.
- 참고
https://m.boostcourse.org/web326/lecture/258535
'Spring' 카테고리의 다른 글
네이버 지역검색 API를 활용한 맛집 List 제작 - (2) (0) | 2021.08.22 |
---|---|
네이버 지역검색 API를 활용한 맛집 List 제작 - (1) (0) | 2021.08.22 |
Swagger (0) | 2021.08.21 |
@Valid를 위한 Blank, Empty, Null 차이 (0) | 2021.05.13 |
Spring Framework란? 기본 핵심 개념 정리 (0) | 2020.11.10 |