이제부터 본격적으로 Spring 을 사용해서 웹 애플리케이션을 만들어 본다.
Spring 을 사용한다는 것은 크게 두 가지 의미가 있는데, IoC (Inversion of Control, 제어의 역전) Container 를 사용 한다는 것 또는 Spring Web MVC 를 사용한다는 것이다.
앞서 만들었던 Dynamic Web Project 를 Spring 을 사용하는 프로젝트로 바꿔보자.
Spring 을 사용하는 프로젝트로 바꾸기 전에 의존성이 무엇인지 생각해보려 한다.
자바에서 하나의 클래스가 다른 클래스를 사용할 때 의존 관계가 성립 한다.
class MyClass {
public static void main(String[] ar) {
AnotherClass ac = new AnotherClass();
}
}
class AnotherClass {}
예를 들어 위와 같이 코드를 작성 한 경우 ‘MyClass 클래스가 AnotherClass 클래스에 의존’ 하는 관계가 만들어 진다.
프로젝트의 규모가 크지 않으면 내가 사용 할 라이브러리를 직접 다운받아 classpath 에 넣고 사용하면 된다.
하지만 일반적으로 프로젝트의 규모가 커질수록 사용하는 외부 라이브러리가 많아지고, 라이브러리의 버전을 관리하기가 매우 어려워 진다. 라이브러리도 서로 필요로 하는 다른 라이브러리의 최소 버전이 있을때가 있기 때문이다.
예를 들면 어떤 라이브러리를 사용하기 위해서는 JDK 1.8 버전 이상을 사용해야 한다거나 하는 것들이다.
이런 부분을 일일이 관리하는 것이 때에 따라 매우 복잡하고 힘들기 때문에, 외부 라이브러리 버전 관리 기능을 포함하고 있는 빌드 도구인 Gradle 이나 Maven 을 사용하는 것이 일반적이다.
개인적으로 Maven 에 대한 사용 경험이 있기 때문에 정리에도 Maven 을 사용할 예정이다.
Maven 에 대해 얕게 정리했던 글을 링크로 남겨둔다.
이제 정말로 앞서 만든 Dynamic Web Project 를 Maven 프로젝트로 바꾸고 spring-webmvc 의존성을 추가해보자.
프로젝트를 마우스 우클릭 하고 Configure > Convert to Maven Project 를 클릭 한다.
Maven Project 로 전환하기 위해 POM (Project Object Model) 기본 설정 정보를 입력해 준다.
Maven Project 로 바꾸면 프로젝트 최상단에 pom.xml 파일이 생긴 것을 볼 수 있다.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.nimkoes</groupId>
<artifactId>MyMavenProject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>MavenProject4Spring</name>
<description>My Sample Maven Spring Project</description>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<warSourceDirectory>WebContent</warSourceDirectory>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
pom.xml 파일을 열어보면 위와 같이 생겼다.
이대로 컴파일 하려고 하면 javax.servlet 이 없다고 오류가 발생할 수 있으니 dependency 를 다음과 같이 하나 미리 추가해준다.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.nimkoes</groupId>
<artifactId>MyMavenProject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>MavenProject4Spring</name>
<description>My Sample Maven Spring Project</description>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<warSourceDirectory>WebContent</warSourceDirectory>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
Maven Project 가 되었으니 compile 해보자.
Maven build 를 처음 하면 설정 마법사가 나타난다.
Goals 에 'clean compile' 을 입력하고 'Run' 을 하면 다음과 같은 오류가 발생할 수 있다.
잘 읽어보면 '넌 나에게 JDK 가 아닌 JRE 를 주었어' 라고 알려주고 있다.
다시 한 번 설정 마법사를 띄우고 'JRE' 탭에 들어가 본다.
compile 하려면 JDK 가 필요한게 상식이거늘 JRE 를 제공해주고 있었다.
'Alternate JRE' 를 선택하고 'Installed JREs' 를 클릭해서 설치된 목록을 확인한다.
위에서부터 순서대로 클릭하면서 jre 대신 jdk 경로를 선택해서 수정 한다.
JDK 로 잘 설정이 되었다면 다시 한 번 Run 을 클릭해서 compile 해보자.
정상적으로 설정이 잘 되었다면 다음과 같이 성공 메시지를 볼 수 있다.
[INFO] Scanning for projects... [INFO] [INFO] ---------------------< me.nimkoes:MyMavenProject >---------------------- [INFO] Building MavenProject4Spring 0.0.1-SNAPSHOT [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ MyMavenProject --- [INFO] Deleting C:\dev\workspace\OldStyleDynamicWebApplication\target [INFO] [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ MyMavenProject --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory C:\dev\workspace\OldStyleDynamicWebApplication\src\main\resources [INFO] [INFO] --- maven-compiler-plugin:3.3:compile (default-compile) @ MyMavenProject --- [INFO] Changes detected - recompiling the module! [WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! [INFO] Compiling 3 source files to C:\dev\workspace\OldStyleDynamicWebApplication\target\classes [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.983 s [INFO] Finished at: 2021-06-22T23:51:37+09:00 [INFO] ------------------------------------------------------------------------ |
Dynamic Web Project 를 Maven 프로젝트로 바꾸었으니 Spring IoC 를 사용하기 위해 의존성을 추가 해준다.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.nimkoes</groupId>
<artifactId>MyMavenProject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>MavenProject4Spring</name>
<description>My Sample Maven Spring Project</description>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<warSourceDirectory>WebContent</warSourceDirectory>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
의존성이 정상적으로 추가 되었고 Maven 중앙 저장소에서 정상적으로 다운 받았다면 다음과 같이 Maven Dependencies 에 Spring 이 추가된 것을 볼 수 있다.
다음으로 할 일은 Spring 이 제공하는 listener 를 web.xml 에 등록하는 일이다.
앞서 등록했던 Servlet 의 event listener 중 하나인 ServletContextListener 인터페이스를 구현한 me.nimkoes.sample.MyServletContextListener 대신 ContextLoaderListener 를 등록한다.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<display-name>OldStyleDynamicWebApplication</display-name>
<!-- 일반 Servlet 을 등록 하듯 Filter 를 등록 한다. -->
<filter>
<filter-name>MySampleFilter</filter-name>
<filter-class>me.nimkoes.sample.MyFilter</filter-class>
</filter>
<!-- 모든 또는 일부 Servlet 에 대해 url-pattern 을 사용하여 Filter 를 적용하도록 설정할 수 있다. -->
<filter-mapping>
<filter-name>MySampleFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Spring 이 제공하는 Listener 등록 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>MySample</servlet-name>
<servlet-class>me.nimkoes.sample.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MySample</servlet-name>
<url-pattern>/Hello</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
</web-app>
ContextLoaderListener (5.1.3.RELEASE) 에 대한 설명은 다음과 같다.
Bootstrap listener to start up and shut down Spring's root WebApplicationContext. Simply delegates to ContextLoader as well as to ContextCleanupListener. As of Spring 3.1, ContextLoaderListener supports injecting the root web application context via the ContextLoaderListener(WebApplicationContext) constructor, allowing for programmatic configuration in Servlet 3.0+ environments. |
API 문서를 보면 ContextLoaderListener 가 ServletContextListener 를 구현하고 있는 것을 볼 수 있다.
낯설다고 생각할 수 있지만, ServletContextListener 는 ContextLoaderListener 를 등록하기 위해 방금 삭제한 Listener 임을 기억하면 좋을것 같다.
즉, Spring 을 사용하기 위해 등록한 ContextLoaderListener 도 결국 Servlet 의 event listener 인 ServletContextListener 타입에 불과하다는 것을 뜻한다.
앞에서 SevletContextListener 인터페이스를 구현한 MyServletContextListener 와 다른점이 있다면, 직접 구현한 listener 를 등록해서 사용하지 않고 Spring 에서 미리 구현한 listener 를 등록해서 사용 한다는 것 뿐이다.
ServletContextListener 를 구현하고 있는 ContextLoaderListener 에 대해 정리 하자면, 이 클래스는 Spring IoC 컨테이너 즉, ApplicationContext 를 servlet application 의 life cycle 에 맞춰서 바인딩 해주는 역할을 한다. 다시 말해서 웹 애플리케이션에 등록한 서블릿들을 사용할 수 있도록 ApplicationContext 를 만들어서 servlet context 에 등록 해준다. 그리고 servlet 이 종료되는 시점에 ApplicationContext 를 제거 한다.
그런데 ContextLoaderListener 는 ApplicationContext 를 만들어야 한다. 이 말을 다시 하면 Spring 의 설정 파일이 필요하다는 것을 뜻한다. 왜냐하면 설정 파일이 있어야 ApplicationContext 를 만들 수 있기 때문이다.
ContextLoaderListener 클래스는 ContextLoader 클래스를 상속 받고 있다.
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.context;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* Bootstrap listener to start up and shut down Spring's root {@link WebApplicationContext}.
* Simply delegates to {@link ContextLoader} as well as to {@link ContextCleanupListener}.
*
* <p>As of Spring 3.1, {@code ContextLoaderListener} supports injecting the root web
* application context via the {@link #ContextLoaderListener(WebApplicationContext)}
* constructor, allowing for programmatic configuration in Servlet 3.0+ environments.
* See {@link org.springframework.web.WebApplicationInitializer} for usage examples.
*
* @author Juergen Hoeller
* @author Chris Beams
* @since 17.02.2003
* @see #setContextInitializers
* @see org.springframework.web.WebApplicationInitializer
*/
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
/**
* Create a new {@code ContextLoaderListener} that will create a web application
* context based on the "contextClass" and "contextConfigLocation" servlet
* context-params. See {@link ContextLoader} superclass documentation for details on
* default values for each.
* <p>This constructor is typically used when declaring {@code ContextLoaderListener}
* as a {@code <listener>} within {@code web.xml}, where a no-arg constructor is
* required.
* <p>The created application context will be registered into the ServletContext under
* the attribute name {@link WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE}
* and the Spring application context will be closed when the {@link #contextDestroyed}
* lifecycle method is invoked on this listener.
* @see ContextLoader
* @see #ContextLoaderListener(WebApplicationContext)
* @see #contextInitialized(ServletContextEvent)
* @see #contextDestroyed(ServletContextEvent)
*/
public ContextLoaderListener() {
}
/**
* Create a new {@code ContextLoaderListener} with the given application context. This
* constructor is useful in Servlet 3.0+ environments where instance-based
* registration of listeners is possible through the {@link javax.servlet.ServletContext#addListener}
* API.
* <p>The context may or may not yet be {@linkplain
* org.springframework.context.ConfigurableApplicationContext#refresh() refreshed}. If it
* (a) is an implementation of {@link ConfigurableWebApplicationContext} and
* (b) has <strong>not</strong> already been refreshed (the recommended approach),
* then the following will occur:
* <ul>
* <li>If the given context has not already been assigned an {@linkplain
* org.springframework.context.ConfigurableApplicationContext#setId id}, one will be assigned to it</li>
* <li>{@code ServletContext} and {@code ServletConfig} objects will be delegated to
* the application context</li>
* <li>{@link #customizeContext} will be called</li>
* <li>Any {@link org.springframework.context.ApplicationContextInitializer ApplicationContextInitializer org.springframework.context.ApplicationContextInitializer ApplicationContextInitializers}
* specified through the "contextInitializerClasses" init-param will be applied.</li>
* <li>{@link org.springframework.context.ConfigurableApplicationContext#refresh refresh()} will be called</li>
* </ul>
* If the context has already been refreshed or does not implement
* {@code ConfigurableWebApplicationContext}, none of the above will occur under the
* assumption that the user has performed these actions (or not) per his or her
* specific needs.
* <p>See {@link org.springframework.web.WebApplicationInitializer} for usage examples.
* <p>In any case, the given application context will be registered into the
* ServletContext under the attribute name {@link
* WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE} and the Spring
* application context will be closed when the {@link #contextDestroyed} lifecycle
* method is invoked on this listener.
* @param context the application context to manage
* @see #contextInitialized(ServletContextEvent)
* @see #contextDestroyed(ServletContextEvent)
*/
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
/**
* Initialize the root web application context.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
/**
* Close the root web application context.
*/
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
너무 길어서 전문을 첨부하진 않겠지만 ContextLoader 클래스를 보면 이 클래스가 사용하는 (다시 말하면 ContextLoaderListener 가 사용하는) parameter 들이 있다.
public class ContextLoader {
/**
* Config param for the root WebApplicationContext id,
* to be used as serialization id for the underlying BeanFactory: {@value}.
*/
public static final String CONTEXT_ID_PARAM = "contextId";
/**
* Name of servlet context parameter (i.e., {@value}) that can specify the
* config location for the root context, falling back to the implementation's
* default otherwise.
* @see org.springframework.web.context.support.XmlWebApplicationContext#DEFAULT_CONFIG_LOCATION
*/
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
/**
* Config param for the root WebApplicationContext implementation class to use: {@value}.
* @see #determineContextClass(ServletContext)
*/
public static final String CONTEXT_CLASS_PARAM = "contextClass";
/**
* Config param for {@link ApplicationContextInitializer} classes to use
* for initializing the root web application context: {@value}.
* @see #customizeContext(ServletContext, ConfigurableWebApplicationContext)
*/
public static final String CONTEXT_INITIALIZER_CLASSES_PARAM = "contextInitializerClasses";
/**
* Config param for global {@link ApplicationContextInitializer} classes to use
* for initializing all web application contexts in the current application: {@value}.
* @see #customizeContext(ServletContext, ConfigurableWebApplicationContext)
*/
public static final String GLOBAL_INITIALIZER_CLASSES_PARAM = "globalInitializerClasses";
/**
* Any number of these characters are considered delimiters between
* multiple values in a single init-param String value.
*/
private static final String INIT_PARAM_DELIMITERS = ",; \t\n";
/**
* Name of the class path resource (relative to the ContextLoader class)
* that defines ContextLoader's default strategy names.
*/
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
private static final Properties defaultStrategies;
static {
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized
// by application developers.
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
}
}
/**
* Map from (thread context) ClassLoader to corresponding 'current' WebApplicationContext.
*/
private static final Map<ClassLoader, WebApplicationContext> currentContextPerThread =
new ConcurrentHashMap<>(1);
/**
* The 'current' WebApplicationContext, if the ContextLoader class is
* deployed in the web app ClassLoader itself.
*/
@Nullable
private static volatile WebApplicationContext currentContext;
/**
* The root WebApplicationContext instance that this loader manages.
*/
@Nullable
private WebApplicationContext context;
/** Actual ApplicationContextInitializer instances to apply to the context. */
private final List<ApplicationContextInitializer<ConfigurableApplicationContext>> contextInitializers = new ArrayList<>();
// 이하 생략
}
Spring 설정 파일로 xml 설정 파일을 사용 할 수 있고, java 설정 파일도 사용 할 수 있다. 심지어 두가지 방식을 섞어서 사용할 수도 있다.
xml 설정 파일보다 java 설정 파일을 많이 사용하므로 되도록 java 설정 파일을 사용할 예정이다.
web.xml 파일을 다음과 같이 수정한다. 바뀐 부분은 ContextLoaderListener 가 사용 할 context-param 이 추가 되었다.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<display-name>OldStyleDynamicWebApplication</display-name>
<!-- context-param 은 filer 보다 먼저 작성해야 한다. -->
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>me.nimkoes.sample.MyAppConfig</param-value>
</context-param>
<!-- 일반 Servlet 을 등록 하듯 Filter 를 등록 한다. -->
<filter>
<filter-name>MySampleFilter</filter-name>
<filter-class>me.nimkoes.sample.MyFilter</filter-class>
</filter>
<!-- 모든 또는 일부 Servlet 에 대해 url-pattern 을 사용하여 Filter 를 적용하도록 설정할 수 있다. -->
<filter-mapping>
<filter-name>MySampleFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Spring 이 제공하는 Listener 등록 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>MySample</servlet-name>
<servlet-class>me.nimkoes.sample.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MySample</servlet-name>
<url-pattern>/Hello</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
</web-app>
ContextLoader 클래스에서 보았듯 어떤 타입의 설정 파일을 사용할 것인지에 대한 contextClass 와 설정 파일의 위치를 알려주는 contextConfigLocation 을 정의 하였다.
AnnotationConfigWebApplicationContext 에 대한 API 문서를 보면 이 클래스도 결국 ApplicationContext 를 구현하고 있는 것을 확인할 수 있다.
그리고 이 ApplicationContext 를 만들 때 사용 할 java 설정 파일로 me.nimkoes.sample.MyAppConfig 클래스를 사용하도록 했다.
그럼 이제 MyAppConfig Spring 설정 파일을 다음과 같이 만들어 준다.
package me.nimkoes.sample;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class MyAppConfig {
}
@Configuration annotation 을 사용해서 이 파일이 Spring 설정 파일임을 명시한다.
@ComponentScan annotation 을 사용해서 자기 자신이 위치한 패키지를 포함한 하위 패키지들의 @Component 를 찾아 IoC Container 에 등록하라고 명시한다.
그래서 다음과 같이 Spring IoC Container 에 등록 할 bean 을 하나 정의 하였다.
package me.nimkoes.sample;
import org.springframework.stereotype.Service;
@Service
public class MyHelloService {
public String getName() {
return "nimkoes";
}
}
@ComponentScan 을 명시한 Spring 설정 파일과 동등한 패키지에 있는 클래스 이기 때문에 Scan 대상이 된다.
@Service annotation 이 @Component annotation 이 아닌데 어떻게 bean 으로 등록이 되냐 생각할 수 있겠지만, 이 annotation 이 어떻게 생겼는지 보면 다음과 같이 @Component annotation 이 정의되어 있는것을 볼 수 있다. (코드 내 주석은 삭제하였다.)
package org.springframework.stereotype;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
*/
@AliasFor(annotation = Component.class)
String value() default "";
}
여기까지 작성 했으면 이제 의도한 대로 동작 하는지 확인해 볼 차례이다.
web.xml 로 다시 돌아가서 ServletContextListener 를 구현 한 ContextLoaderListener 가 AplicationContext 를 등록 해준다고 했었다.
등록하는 위치는 ServletContext 이다. ServletContext 는 모든 Servlet 들이 같이 사용할 수 있는 저장소 같은 개념이라고 생각하면 된다.
어떤 이름으로 등록해 주는지는 ContextLoaderListener 를 찾아 들어가면 되는데, (찾아가 봤지만 여기서는 생략한다.) 다음과 같이 set 을 해주는 코드를 볼 수 있다.
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ARRTIBUTE 라는 이름으로 등록해 주는데, 이 값은 다음과 같이 정의되어 있다.
String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
어쨌든, 중요한 것은 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ARRTIBUTE 라는 이름으로 ApplicationContext 를 찾아 사용할 수 있다는 것이다.
web.xml 에 등록해 두었던 MyServlet Servlet 에서 이 값을 꺼내서 사용해보자.
package me.nimkoes.sample;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.WebApplicationContext;
public class MyServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("MyServlet init !!!");
}
/*
* (non-Javadoc)
*
* @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.
* HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ApplicationContext context = (ApplicationContext) getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
MyHelloService myHelloService = context.getBean(MyHelloService.class);
System.out.println("MyServlet doGet called !!! >> " + myHelloService.getName());
// req.getRequestDispatcher("/WEB-INF/view/MyHello.jsp").forward(req, resp);
}
@Override
public void destroy() {
System.out.println("MyServlet destroy !!!");
}
}
ServletContext 에서 ApplicationContext 를 꺼내왔고, 그 AplicationContext 에는 Spring 설정 파일을 통해 등록 된 MyHelloService 가 bean 으로 등록 되어있을 것이고, 그 bean 의 getName 메소드를 사용해서 출력해 보도록 수정하였다.
여기서 주목할 점은 MyHelloService 클래스의 객체를 new 연산자를 사용해서 생성하지 않고, Spring 이 제공해주는 IoC Container 를 사용해서 등록되어있는 bean 을 꺼내와 사용한다는 것이다. 즉, 객체 (bean) 생성의 주체가 개발자(사용자)가 아닌 Spring 으로 넘어갔다.
이제 정말로 서버에 애플리케이션을 올리고 실행해보자.
서버가 정상적으로 실행 되고난 다음 Hello Servlet 을 호출해보자.
6월 23, 2021 11:47:02 오후 org.apache.catalina.core.AprLifecycleListener init 정보: The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: 6월 23, 2021 11:47:02 오후 org.apache.tomcat.util.digester.SetPropertiesRule begin 경고: [SetPropertiesRule]{Server/Service/Engine/Host/Context} Setting property 'source' to 'org.eclipse.jst.jee.server:OldStyleDynamicWebApplication' did not find a matching property. 6월 23, 2021 11:47:02 오후 org.apache.coyote.AbstractProtocol init 정보: Initializing ProtocolHandler ["http-nio-8080"] 6월 23, 2021 11:47:03 오후 org.apache.tomcat.util.net.NioSelectorPool getSharedSelector 정보: Using a shared selector for servlet write/read 6월 23, 2021 11:47:03 오후 org.apache.coyote.AbstractProtocol init 정보: Initializing ProtocolHandler ["ajp-nio-8009"] 6월 23, 2021 11:47:03 오후 org.apache.tomcat.util.net.NioSelectorPool getSharedSelector 정보: Using a shared selector for servlet write/read 6월 23, 2021 11:47:03 오후 org.apache.catalina.startup.Catalina load 정보: Initialization processed in 716 ms 6월 23, 2021 11:47:03 오후 org.apache.catalina.core.StandardService startInternal 정보: Starting service Catalina 6월 23, 2021 11:47:03 오후 org.apache.catalina.core.StandardEngine startInternal 정보: Starting Servlet Engine: Apache Tomcat/8.0.9 6월 23, 2021 11:47:04 오후 org.apache.catalina.core.ApplicationContext log 정보: No Spring WebApplicationInitializer types detected on classpath 6월 23, 2021 11:47:04 오후 org.apache.catalina.core.ApplicationContext log 정보: Initializing Spring root WebApplicationContext 6월 23, 2021 11:47:04 오후 org.springframework.web.context.ContextLoader initWebApplicationContext 정보: Root WebApplicationContext: initialization started 6월 23, 2021 11:47:05 오후 org.springframework.web.context.ContextLoader initWebApplicationContext 정보: Root WebApplicationContext initialized in 849 ms MyFilter init !!! 6월 23, 2021 11:47:05 오후 org.apache.coyote.AbstractProtocol start 정보: Starting ProtocolHandler ["http-nio-8080"] 6월 23, 2021 11:47:05 오후 org.apache.coyote.AbstractProtocol start 정보: Starting ProtocolHandler ["ajp-nio-8009"] 6월 23, 2021 11:47:05 오후 org.apache.catalina.startup.Catalina start 정보: Server startup in 2639 ms MyServlet init !!! MyFilter destroy !!! MyServlet doGet called !!! >> nimkoes |
의도한 대로 잘 동작하는 것을 확인할 수 있다.
하지만 이 방법에는 단점이 있는데, 이렇게 IoC Container 기능만을 사용한다면 Servlet 이 새로 생길 때마다 등록하고 mapping 하는 코드가 계속 추가 되어야 하는데, 그렇게 되면 web.xml 파일이 길어지고 관리하기가 힘들어 진다.
이런 불편함에 대해 Spring 은 디자인 패턴 중 Front Controller Pattern 을 사용해서 해결하고 있다.
즉, 제일 앞쪽에 모든 요청을 처리 한 Controller 를 하나 등록하는 것으로 모든 Servlet 을 등록하는 작업을 대체한다.
모든 요청을 받는 Controller 는 해당 요청을 처리 할 수 있는 Handler 에게 작업을 위임한다.
Spring 은 이렇게 모든 요청을 받아 처리 할 Servlet 을 구현해 두었는데 DispatcherServlet 이 그 역할을 담당 한다.
이 클래스는 Spring Web MVC 의 가장 핵심이 되는 클래스이다.
DispatcherServler 은 ServletContext 에 등록되어 있는 ApplicationContext 가 있다면 이 ApplicationContext 를 부모로 취급하여 자신만의 ApplicationContext 를 하나 더 만든다.
그림에서 Controllers, HandlerMapping, ViewResolver 가 정의되어 있는 WebApplicationContext 가 DispatcherServlet 이 만든 ApplicationContext 이고, services, datasources 등이 정의되어 있는 WebApplicationContext 가 부모 역할을 하는 Root ApplicationContext 가 된다.
현재 작성한 web.xml 파일 기준으로 Root ApplicationContext 는 ContextLoaderListener 가 만든 ApplicationContext 이다.
그렇기 때문에 ContextLoaderListener 가 만드는 ApplicationContext 는 다른 모든 Servlet 에서 사용할 수 있지만, DispatcherServlet 이 만든 ApplicationContext 는 DispatcherServlet 만 사용할 수 있고 다른 Servlet 에서는 사용하지 못한다.
흔한 경우는 아니지만, DispatcherServlet 을 포함한 다른 여러 Servlet 이 공유해서 같이 사용해야 하는 bean 이 있을 경우에는 ContextLoaderListener 가 만드는 ApplicationContext 에 등록해서 사용해야 하고,
그렇지 않은 경우 굳이 이 ApplicationContext 에 bean 을 등록 할 필요는 없다.
그래서 일반적으로 ContextLoaderListener 가 만드는 ApplicationContext 인 Root WebApplicationContext 에는 Web 과 관련된 bean 을 등록하지 않는 편이다.
그도 그럴것이 모든 서블릿에서 공유해서 사용하는데 Web 에 한정된 bean 을 등록해서 사용 하는것은 (불가능하진 않지만) 처음 설계 의도와는 조금 맞지 않는 부분이 있다.
바로 위에 첨부한 그림이 이 내용을 잘 설명해 주고 있다.
지금까지 Spring 의 IoC Container 에 대해 정리해 보았고, 어떻게 동작하는지도 가볍게 살펴 보았다.
DispatcherServlet 을 사용하여 Spring Web MVC 를 사용하는 것에 대해서는 다음에 정리한다.
마지막으로 한가지 기억해둘 만한 것은 DispatcherServlet 도 본질은 Servlet 이라는 것이다.
'Archive > Spring Web MVC' 카테고리의 다른 글
1.5 Spring MVC 동작 원리 마무리 (0) | 2021.06.28 |
---|---|
1.4 DispatcherServlet 기본 동작 원리 (0) | 2021.06.27 |
1.3 Spring MVC 연동 (Root & Servlet ApplicationContext) (0) | 2021.06.24 |
1.1 MVC 와 Legacy Servlet Application (0) | 2021.06.22 |
Spring Web MVC 정리 개요 (0) | 2021.06.22 |