리루
4. Servlet (써블릿) - Servlet Application에 Spring MVC 적용 본문
1. Servlet Application에 Spring 적용의 의미란?
- 스프링이 제공하는 IoC 컨테이너를 활용하겠다.
- 스프링이 제공하는 서블릿 구현체 DispatcherServlet을 사용하겠다.
1-1. (임의로 만든 Servlet에서) 스프링이 제공하는 IoC 컨테이너를 활용하겠다.
- 의존성 필요
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
- web.xml 파일 내 Listener 변경(기등록된 리스너를 제거하고, Spring에서 제공하는 ContextLoaderListener 등록)
- ContextLoaderListener는 Spring IoC Container (즉, Application Context)를 Servlet Application 생명 주기에 맞춰서 바인딩해준다.
- Application Context를 Web Application에 등록되어있는 Servlet들이 사용할 수 있도록 Application Context를 만들어서 이 Application Context를 Servlet Context에 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE라는 이름으로 등록해주고(*Servlet Context : Servlet 들이 사용할 수 있는 공용의 정보들을 모아두는 공간), Servlet Context가 종료될 시점에 제거해준다.
public class ContextLoader {
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// ...
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
// ...
}
}
- ApplicationContext를 만들어야하기 때문에 Spring 설정파일이 필요하다. (기본으로는 xml 형태의 설정파일을 사용하지만 java 파일로도 설정파일을 만들 수 있다.)
- ContextLoaderListener 가 AnnotationConfigWebApplicationContext를 만들 때, AppConfig를 이용해 만든다.
- 그러면 ApplicationContext 안에는 HelloService가 Bean으로 등록되어있다.
- 만든 Servlet에서 ApplicationContext 통해서 HelloService를 꺼내 쓸 수 있다.
a. web.xml에 AnnotationConfigWebApplicationContext, AppConfigf를 파라미터로 한 ContextLoaderListener를 추가.
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!-- Parameter for ContextLoaderListener -->
<!-- public static final String CONTEXT_CLASS_PARAM = "contextClass"; -->
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<!-- Parameter for ContextLoaderListener -->
<!-- public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation"; -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>me.json.AppConfig</param-value>
</context-param>
<!-- Register a filter -->
<filter>
<filter-name>myFilter</filter-name>
<filter-class>me.json.MyFilter</filter-class>
</filter>
<!-- Mapping filter to servlet-->
<filter-mapping>
<filter-name>myFilter</filter-name>
<servlet-name>hello</servlet-name>
</filter-mapping>
<!-- Register Servlet Listener-->
<!--<listener>-->
<!--<listener-class>me.json.MyListener</listener-class>-->
<!--</listener>-->
<!-- Register ContextLoaderListener that is provided by Spring-->
<!-- * contextClass(Parameter) : AnnotationConfigWebApplicationContext -->
<!-- * contextConfigLocation(Parameter) : me.json.AppConfig -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Register servlet -->
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>me.json.HelloServlet</servlet-class>
</servlet>
<!-- Mapping Servlet with url pattern -->
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
b. AppConfig.java 생성
- ContextLoaderListner의 contextConfigLocation 파라미터로 사용됨
- @ComponentScan을 이용해 bean 등록
- 과거에는 xml 파일로 많이 사용되었지만 요즘은 xml이 사용되지 않는 추세이므로 java 파일로 대체
package me.json;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class AppConfig {
}
c. HelloService.java 생성
package me.json;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
public String getName(){
return "json";
}
}
d. Servlet에서 ApplicationContext 가져오기 및 등록된 Bean(HelloService.class) 활용
package me.json;
import org.springframework.context.ApplicationContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.springframework.web.context.WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("init");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// Get HelloService Bean that is registered in Application Context
ApplicationContext context = (ApplicationContext) getServletContext().getAttribute(ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
HelloService helloService = context.getBean(HelloService.class);
System.out.println("doGet");
resp.getWriter().print("<html>");
resp.getWriter().print("<head>");
resp.getWriter().print("<body>");
resp.getWriter().print("<h1>Hello Servlet</h1>");
resp.getWriter().print("<h1>Hello" + getName() + "</h1>");
resp.getWriter().print("<h1>Hello" + helloService.getName() + "</h1>");
resp.getWriter().print("</body>");
resp.getWriter().print("</head>");
resp.getWriter().print("</html>");
}
private Object getName() {
// Get attribute value that is set in the Servlet listener
return getServletContext().getAttribute("name");
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
1-2. 스프링이 제공하는 서블릿 구현체 DispatcherServlet을 사용하겠다.
- 서블릿을 만들때마다 web.xml에 설정이 늘어난다.
- 여러 서블릿에서 공통으로 처리하고 싶은 부분이 있는데, (filter로 처리할 수 있긴하지만) Front Controller라는 디자인 패턴이 있다.(모든 요청을 Controller 하나가 받아서 처리한다. 해당 요청을 처리할 핸들러들에게 분배(Dispatch)하는 패턴이다.)
- Spring이 Front Controller 역할을 하는 Servlet을 만들어 놓았는데 그것이 바로 DispatcherServlet이다.
- DispatcherServlet은 Root WebApplicationContext를 상속받아 새로운 Servlet WebApplicationContext를 만든다.
- Root WebApplicationContext : ContextLoaderListener에 의해 ServletContext에 등록된 ApplicationContext로 모든 Servlet에서 사용 가능하다.(Services, Repositories Bean과 같이 다른 DispatcherServlet에서도 공용으로 사용 할 수 있다.)
- Servlet WebApplicationContext : DispatcherServlet에서 Root WebApplicationContext를 상속받아 만든 ApplicationContext로, 해당 DispatcherServlet에서만 사용가능하다. (해당 DispatcherServlet에 한정된 Controller, View 같은 Bean들이 등록된다.)
- DispatcherServlet 은 Front Controller 역할을 한다.
- /app 으로 시작하는 모든 요청은 DispatcherServlet을 통해 들어온다.
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!-- Parameter for ContextLoaderListener -->
<!-- public static final String CONTEXT_CLASS_PARAM = "contextClass"; -->
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<!-- Parameter for ContextLoaderListener -->
<!-- public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation"; -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>me.json.AppConfig</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>me.json.WebConfig</param-value>
</init-param>
</servlet>
<!-- Request that stars with /app/* will come in through DispatcherServlet-->
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
package me.json;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
@Configuration
// An applicationContext has this configuration include only Controller.class as a Bean without using default filters
@ComponentScan(useDefaultFilters = false, includeFilters = @ComponentScan.Filter(Controller.class))
public class WebConfig {
}
- DispatcherServlet 중에 /app/hello에 대해서는 HelloController 내 핸들러가 사용된다.
- HelloController Bean은 DispatcherServlet 내 ApplicationContext에 등록이 되고, 이 ApplicationContext를 만들 때 DispatcherServlet은 현재 ServletContext에 들어있는 ContextLoaderListener가 만들어준 ApplicationContext를 부모로 사용한다. 부모로 사용되는 ApplicationContext는 AppConfig.class를 갖고 만들어지고, 이는 Controller 빼고 모든 Bean을 등록하게 되어있다.
package me.json;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
@Configuration
// An applicationContext has this configuration has defaultFilters as beans and excludes only Controller.class
@ComponentScan(excludeFilters = @ComponentScan.Filter(Controller.class))
public class AppConfig {
}
- 따라서 HelloService.class Bean이 등록되어 있을 것이고, 사용될 수 있다.
package me.json;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
public String getName(){
return "json";
}
}
1-3. DispatcherServlet에 몰빵형
- 항상 이렇게 ApplicationContext에 다한 상속 구조 / 계층구조를 가져야만 하는 것은 아니고, 필요에따라 사용될 수 있다.(DispatcherServlet이 만드는 ApplicationContext에 모든 Bean을 등록할 수 도 있다.)
- 이렇게 되면 Root WebApplicationContext가 따로 없고, DispatcherServlet이 만든 WebApplicationContext가 그냥 Root 처럼 모든 종류의 Bean 을 등록하게 된다.
package me.json;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class WebConfig {
}
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Web Application</display-name>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>me.json.WebConfig</param-value>
</init-param>
</servlet>
<!-- Request that stars with /app/* will come in through DispatcherServlet-->
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
1-4. web.xml 없이 만들어보자!!(web.xml 없이 DispatcherServlet을 등록하는 방법)
- web.xml 지우기
- WebApplicationInitializer 구현
package me.json;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
public class WebApplication implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(WebConfig.class);
context.refresh();
DispatcherServlet dispatcherServlet = new DispatcherServlet(context);
ServletRegistration.Dynamic app = servletContext.addServlet("app", dispatcherServlet);
app.addMapping("/app/*");
}
}
2. 마치며
- 위의 구조는 SpringBoot와는 완전히 다른 구조이다.
- 위의 구조는 Servlet Container가 먼저 뜨고, ServletContainer 안에 등록되는 Servlet Application에다가 (ContextLoaderListener를 등록한다던가 DispatcherServlet을 등록한다던가 해서)Spring을 연동하는 방식이다.
- SpringBoot의 경우에는 Spring Boot Application 이 Java Application으로 먼저 뜨고, 그 안에 tomcat이 내장 서버로 뜬다. (DispatcherServlet 같은)Servlet을 내장 톰켓 안에다가 코드로 등록한다.
[참고] Inflearn 백기선의 스프링 웹 MVC 기반으로 저같은 초보들이 알기 쉽도록 재구성 해보았습니다., (매우 유용하고 친절한 강의라고 생각합니다. 들으시면 기본적인 구조나 원래를 익히는데 도움이 되실 것 같습니다!! 20% 할인하는 기간도 간간히 있으니 잘 활용하세용~)
'#Spring' 카테고리의 다른 글
3. Servlet (써블릿) - 써블릿 리스터와 필터 (1) | 2020.02.02 |
---|---|
2. Servlet (써블릿) - 원시적인 코드로 확인 (0) | 2020.02.02 |
1. Servlet (써블릿) - 기본 설명 (0) | 2020.02.02 |
Dispatcher Servlet의 동작원리 (0) | 2020.02.02 |
JPA Study (0) | 2019.05.19 |