Skip to content

Dev

코드기반 Spring 설정(1)

Spring2 min read

1. RootApplicationContext / WebApplicationContext

Spring을 설정하기 전에 먼저 이해해야하는 부분은 RootApplicationContextWebApplicationContext이다. 처음에는 이 두 개의 용어가 Spring의 특별한 것을 일컫는줄 알았다.

특별한 기술용어를 지칭하는 것이 아닌 Spring의 ApplicationContext를 계층적으로 분리하여 Root(부모)와 Web(자식)으로 나눈 ApplicationContext를 말하는 것이었다.

Root/WebApplicationContext

위의 그림처럼 여러 개의 WebApplicationContext들은 단 하나의 RootApplicationContext에서 설정된 Bean들을 공유하여 사용한다.

Spring에서는 왜 이렇게 하나의 RootApplicationContext와 여러개의 WebApplicationContext를 만들 수 있도록 있도록 설계하였는지에 대해 고민해봤는데, 확장성 때문인것 같다.

일반적으로 Java WebApplication 구조는 클라이언트에 UI 랜더링 혹은 컴포넌트들을 처리하는 프레젠트 레이어, 비즈니스 로직을 처리하는 비즈니스 레이어, 데이터베이스에 접근하는 데이터 엑세스 레이어로 구성되어 있다.

여기서 RootApplicationContext는 비즈니스 레이어와 데이터 엑세스 레이어에 필요한 Bean 생성과 관계설정등에 대한 설정 내용을 담고 있고, WebApplicationContext는 프레젠트 레이어인 SpringMVC와 관련된 설정 내용을 담고 있다.

데이터의 처리와 정합성, 트랜잭션의 일관성 등을 유지해야하는 비즈니스 레이어, 데이터 엑세스 레이어를 굳이 두개를 만들어 중복이 허용되도록 할 필요는 없을 것 같다.

상대적으로 클라이언트와 가장 밀접한 프레젠트 레이어는 다양한 클라이언트의 필요한 부분을 맞춰줘야하기 때문에 WebApplicationContext를 여러개를 두지 않았나라는 생각이 든다.


2. ServletContext 설정

Servlet 3.0 이상부터는 xml대신 Java 코드로 Web과 관련된 설정이 가능하다. Spring에서는 web 설정을 지원 해줄 수 있는 몇 개의 인터페이스를 제공한다. 그 중에서 좀 더 Servlet 설정에 직관적으로 보이는 WebApplicationInitializer를 선택하였다.

1public class ServletInitConfig implements WebApplicationInitializer {
2
3 @Override
4 public void onStartup(ServletContext container) {
5
6 //① RootApplicationContext 생성 및 설정정보 등록
7 AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
8 rootContext.register(RootAppConfig.class);
9
10 //② RootApplicationContext 라이프사이클 설정
11 container.addListener(new ContextLoaderListener(rootContext));
12
13 //③ WebApplicationContext 생성 및 설정정보 등록
14 AnnotationConfigWebApplicationContext dispatcherContext = new AnnotationConfigWebApplicationContext();
15 dispatcherContext.register(WebAppConfig.class);
16
17 //④ DispatcherServlet 생성 및 기타 옵션정보 설정
18 ServletRegistration.Dynamic dispatcher = container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
19 dispatcher.setLoadOnStartup(1);
20 dispatcher.addMapping("/");
21
22 }
23}

WebApplicationInitializer 인터페이스를 구현하면 onStartup 메서드가 만들어진다. onStartup은 ServletContext타입의 변수를 파라미터로 받아 Servlet container를 초기화하는 메서드이다.

(여기서, onStartUp메서드의 ServletContext 파라미터는 SpringServletContainerInitializer[SPI]라는 이 API가 자동으로 감지하여 컨테이너에 부트스트랩 하여 파라미터를 넘겨준다)

Annotation 기반으로 설정을 하기 때문에 AnnotationConfigWebApplicationContext 타입으로 설정을 진행하였다.

① RootApplicationContext 생성 및 설정정보 등록
1@Configuration
2@ComponentScan(basePackageClasses = {MysqlConfig.class, MemberRepository.class, MemberService.class})
3public class RootAppConfig {
4
5 @Bean
6 public ObjectMapper objectMapper(){
7 return new ObjectMapper();
8 }
9}

RootApplicationContext에 등록된 Bean들에 대한 설정코드이다. ComponentScan을 사용하여 Service, Repository, DB설정정보들을 rootContext 변수에 등록하였다.

② RootApplicationContext 라이프사이클 설정

1번의 설정정보가 담긴 rootContext를 ServletContext의 addListener메서드를 통해 ContextLoaderListener인스턴스 생성자 파라미터로 담아 등록하였는데 이 부분이 어떻게 라이프사이클을 결정짓는지 확인해보자.

ContextLoaderListener

1public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
2 public ContextLoaderListener() {
3 }
4
5 public ContextLoaderListener(WebApplicationContext context) {
6 super(context);
7 }
8
9 public void contextInitialized(ServletContextEvent event) {
10 this.initWebApplicationContext(event.getServletContext());
11 }
12
13 public void contextDestroyed(ServletContextEvent event) {
14 this.closeWebApplicationContext(event.getServletContext());
15 ContextCleanupListener.cleanupAttributes(event.getServletContext());
16 }
17}

ContextLoaderListener는 ServletContextListener 인터페이스의 구현체이자 ContextLoader 클래스를 상속한 클래스이다. 상위 인터페이스로 강제 구현한 contextInitializedcontextDestroyed 두 개의 메서드를 통해 라이프 사이클을 컨트롤한다.

위의 클래스다이어그램에서 EventListener 인터페이스가 있는데, 이 인터페이스는 구현체가 없는 마커 인터페이스이다. ServletContext의 addListener 메서드를 확인해보면 EventListener 타입의 인자값을 받는것을 확인 할 수 있다.

RootApplicationContext의 eventlistener 설정을 통해 라이프사이클 설정을 한 이유는 컨테이너 생성 시에는 RootContext를 생성해야 Bean들을 사용할 수 있으며, Destoryed될 때에는 메모리해제를 위해 정상적으로 종료가 되야하기 때문이다.

③ WebApplicationContext 생성 및 설정정보 등록
1@Configuration
2@EnableWebMvc
3@ComponentScan(basePackageClasses = MemberController.class)
4public class WebAppConfig implements WebMvcConfigurer {
5
6}

@EnableWebMvc 어노테이션은 Spring의 기본설정들을 담고 있다. (MessageConverter나 ViewResolvers 등 ..) 추가로 Controller를 스캔하여 SpringMVC 구조의 Bean들을 설정할 수 있다.

④ DispatcherServlet 생성 및 기타 옵션정보 설정

ServletContainer에 DispatcherServlet을 생성 후, setLoadOnStartup 부분을 볼 수가 있다. 서블릿은 서버가 올라가게 되면, 최초 request 요청이 들어올 때 초기화가 되는데, 이럴 경우 처음 사용자는 서비스에 불편을 겪게된다. 그래서 서버가 스타트되면 초기화 과정이 이루어질 수 있도록 setLoadOnStartup 메서드를 사용한다. (숫자가 0과 같거나 클 경우 해당)

동작방식은 ServletContext가 초기화 단계 동안 ServletContextListener 객체의 contextInitialized 메서드를 호출하여 Servlet을 인스턴스화하고 초기화한다. (ContedxtLoaderListener 클래스다이어그램 참고)


3. 의존성 주입 순서

위에 설정된 Spring환경을 디버깅 모드로 테스트 해보면 아래의 그림과 같은 의존성 주입 순서가 나온다.

의존성순서

WebApplicationContext는 RootApplicationContext의 Bean들을 필요하기에 RootApplicationContext에 설정된 Bean들을 먼저 생성 후에 WebApplicationContext Bean들을 생성 및 주입하는 순서가 된다.

그렇다면, 두 Context간의 상속구조와 Bean 주입순서를 Spring이 어떻게 만들어넀는지 궁금하기도 하였고, @Repository -> @Service의 Bean 주입 순서는 어떻게 결정했는지도 궁금하였다. 그래서 클래스 다이어그램이랑 인터넷에서 이것저것 찾아봤는데 이 부분은 나의 실력 및 분석 부족으로 인해 다루지 못하였다.

대략 찾아본 결과는 BeanPostProcessor가 Bean 생명주기에 관여하여 Bean이 필요한 순서를 처리한다고 한다.(정확한건 아닙니다.) 아쉽지만, 추상적인 정리로 Bean 의존관계에 대한 순서를 Spring이 보장한다는 것이고, 이거에 대한 특별한 API가 관여하여 처리한다고 결론을 내리기로 했다.

또한, RootContext는 WebContext에 있는 Bean들에 대한 정보를 가져 올 수 없고, 오로지 WebContext가 RootContext에 있는 Bean들에 접근 할 수 있다는 것이다.


4. 정리

  • Spring은 확장성을 위하여 ApplicationContext를 계층적으로 나누었다. 나눈 기준은 객체지향관점에서 변하는 것과 변하지 않는 부분을 나누어 RootApplicationContext와 WebApplicationContext를 분리하였다.
  • 기존 xml 방식에서는 해당 클래스가 다른 클래스에 대한 의존도를 찾아보기가 어려웠는데 Java환경으로 구성할 경우, 각 클래스별 의존도와 구현에 대한 상세한 부분을 알 수 있었다.
  • 마지막으로 설정 오류가 될 경우, 디버깅 모드로 테스트가 가능하기에 좀 더 오류에 대해 대응하기가 쉬웠다.

[Refference]