Spring MVC 원리와 구조, Spring Container(ApplicationContext)와 Dispatcher-servlet등 의 구성과 정의를 파악해보았다. 전체적인 흐름에 따라 현 과정을 알기 위해 Spring 환경을 통한 실습을 진행해보자.
- Develop OS : Windows10 Ent, 64bit
- WEB/WAS Server : Tomcat v9.0
- DBMS : MySQL
- Language : JAVA 11 (JDK 11)
- Framwork : Spring 3.1.1 Release
- Build Tool : Maven
- IDE : Intellij 2022
- 빌드 환경은 다를 수 있습니다.
들어가기에 앞서, Spring MVC 프레임워크가 생기기 전부터를 살펴보면 Servlet을 통한 Client와의 요청 반환 과정을 실습해보고 배워보고자 한다.
서블릿 애플리케이션 실행
서블릿 애플리케이션은 서블릿 컨테이너가 실행할 수 있으므로, 반드시 서블릿 컨테이너를 사용해야 한다.
서블릿 컨테이너가 서블릿 애플리케이션 실행하는 방법(서블릿의 생명주기)
init()
: 최초 요청을 서블릿 컨테이너가 받았고, 요청을 처리할 서블릿 인스턴스의 init()을 호출하여 초기화 한다.
최초 요청을 받았을 때 한번 초기화 하고 나면 그 다음부터는 이 과정을 생략한다.
서블릿을 요청한 다음부터는 클라이언트의 요청을 처리할 수 있다.
각 요청은 별도의 스레드로 처리하는데, 이 때 서블릿 인스턴스의 service()
를 호출한다.
service()
안에서 HTTP 요청을 받고 클라이언트로 보낼 HTTP 응답을 만든다.
service()는 보통 HTTP Method에 따라 doGet()
, doPost()
등으로 처리를 위임한다.
결과적으로 요청을 처리하는 것은 doGet, doPost 메소드들인 것이다.
destroy()
: 서블릿 컨테이너 판단에 다라 해당 서블릿을 메모리에서 내려야할 시점에 해당 메소드를 호출한다.
ServletContext란 톰캣이 실행되면서 서블릿과 서블릿 컨테이너 간 연동을 위해 사용되는 Context로,
하나의 웹 애플리케이션마다 하나의 서블릿 컨텍스트를 가진다.
이제 Intellij에서 Maven project를 생성하여 진행해보자.
이후 Spring MVC 프로젝트 생성 과정은 이 곳을 참조했다.
pom.xml
spring-webmvc
의존성은 이후에 추가했던 파일이고
jUnit 의존성이 명시되어 있다.JUnit Scope: test
는 소스 classpath 에서는 사용하지 못하고 테스트를 실행할때만 사용이 가능하다는 뜻이다.
소스 패키지 설정
서블릿을 사용한 애플리케이션을 실행하기 위해선,
Tomcat Server
가 필요하다. (add 해주자)
서블릿 컨테이너 실행 순서는
첫 로딩 : init()
> doGet or doPost
다음 로딩 : doGet or doPost
종료 : destroy()
서블릿 클래스를 생성하기 위해 HelloServlet 클래스를 생성해보자.
...
@Override
public void init() throws ServletException {
System.out.println("init");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
System.out.println("doGet");
resp.getWriter().println("<html>");
resp.getWriter().println("<head>");
resp.getWriter().println("<body>");
resp.getWriter().println("<h1>hello servlet?</h1>");
resp.getWriter().println("</body>");
resp.getWriter().println("</head>");
resp.getWriter().println("</html>");
}
@Override
public void destroy() {
System.out.println("destroy");
}
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>org.example.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
서블릿 컨테이너 기반으로 과정을 커스텀 했을때 알 수 있는 점은 Spring MVC는 이 기반으로 만들어졌다. 이에 들어온 요청과 응답을 위한 서블릿 필터와 서블릿 리스너가 필요하다.
서블릿 리스너는 서블릿 컨테이너에서 발생하는 주요 이벤트들을 감지하고 각 이벤트에 특별한 작업이 필요한 경우 사용한다.
package org.example;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class DemoListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce){
System.out.println("Context Initialized");
sce.getServletContext().setAttribute("name","kwak");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("context destroyed");
}
}
리스너의 초기 설정을 커스텀했다. 서블릿 컨테이너가 실행되고 종료되는 라이프사이클에 따른 설정 작업이 실행되도록 하는 로직이다.
서블릿 필터는 들어온 요청을 서블릿으로 보내고, 서블릿이 작성한 응답을 클라이언트로 보내기 전에 특별한 처리가 필요할 때 사용한다.
package org.example;
import javax.servlet.*;
import java.io.IOException;
public class DemoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException{
System.out.println("Filter init");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter");
filterChain.doFilter(servletRequest, servletResponse); //다음 필터로 연결을 해줘야한다.
//만약 마지막 필터일 경우 다음 연결은 servlet이다.
}
@Override
public void destroy() {
System.out.println("Filter Destroy");
}
}
서블릿 필터와 서블릿 리스너들은 각각 web.xml에 설정을 해주어야 한다.
<listener>
<listener-class>org.example.DemoListener</listener-class>
</listener>
<filter>
<filter-name>demoFilter</filter-name>
<filter-class>org.example.DemoFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>demoFilter</filter-name>
<!-- 적용 서블릿 이름 (만약 여러 개의 서블릿에 적용할 시 url-pattern으로 설정한다.)-->
<servlet-name>hello</servlet-name>
</filter-mapping>
서블릿 파일과 서블릿 리스너, 필터를 생성해봤으니 이제 Spring Container와 연동이 필요하다.
Spring Container는 IoC Container
로 불리기도 한다. 또한 ApplicationContext로 주로 사용된다. ApplicationContext를 ServletContext에 등록해보자.
먼저 pom.xml에 (spring-webmvc)
의존성을 추가해야 스프링 컨테이너를 사용할 수 있다. (위에 명시)
<!--ContextLoaderListener는 애플리케이션 컨텍스트(스프링 컨테이너)를 만들어 서블릿 컨텍스트에 등록하는 역할-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
- ContextLoaderListener는 ApplicationContext를 만들어 ServletContext에 등록하는 역할을 한다.
- ApplicationContext를 서블릿 애플리케이션의 생명주기에 맞춰서 바인딩 해주는 것이다.
- 등록되어 있는 서블릿이 사용될 수 있도록, ContextLoaderListener를 활용하여 ApplicationContext를 만들어 ServletContext에 등록을 해준다.
ContextLoaderListener가 애플리케이션 컨텍스트(AnnotationConfigWebApplicationContext)를 등록할 때 Java 설정 파일을 참고하여 애플리케이션 컨텍스트를 등록한다.
<!-- 애플리케이션 컨텍스트 등록 -->
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<!-- Java 설정 파일 위치 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>org.example.AppConfig</param-value>
</context-param>
AppConfig
package org.example;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class AppConfig {
}
정리하자면, ContextLoaderListener
가 AnnotationConfigWebApplicationContext
를 만들 때 ContextConfigLocation
의 정보(빈 등록하려는 패키지 위치)를 참고하여 만든 후 ServletContext
에 등록하는 것이다.
그렇다면 등록된 사용할 줄 알아야 할 것이다. HelloServlet 클래스에 어플리케이션 컨텍스트를 생성 후 Bean
을 꺼내 사용해보자.
HelloServlet
...
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
ApplicationContext context = (ApplicationContext) getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); //서블릿 컨텍스트에서 애플리케이션 컨텍스트를 꺼내온다.
//애플리케이션 컨텍스트에서 service의 빈을 꺼내온다.
HelloService helloService = context.getBean(HelloService.class);
String name = helloService.getName();
...
...
}
이러한 과정을 통해 서블릿을 활용한 Spring Container 연동으로 웹 어플리케이션을 구현하고자 할 때, 알 수 있는 점은 "복잡하다.." 는 느낌을 가질 수 밖에 없다.
표현하려는 View가 여러 개 일 경우, url 하나 당 서블릿 하나를 만들어야하고 xml이나 자바 설정 파일도 추가해야 하는 것을 알 수 있다. 진행 프로젝트 규모가 크다면 비효율적으로 많이 추가해야 하고 여러 서블릿에서 공통적으로 사용하고 싶은 부분이 생긴다면 이 역시도 구현 시 부담스러운 상황이다.
그래서 스프링은 Front Controller
개념인 Dispatcher-Servlet
을 내놓았다. (MVC 패턴에 가장 중요한 개념이라 계속 강조된다.)
이제 Dispatcher-servlet을 적용해보자
1. web.xml 방식
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 디스패처 서블릿 등록 -->
<!-- 애플리케이션 컨텍스트에 모든 Bean을 등록 -->
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContex</param-value>
</init-param>
<init-param>
<param-name>contextConfigLoaction</param-name>
<param-value>org.example.WebConfig</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>/app/*</servlet-class>
</servlet>
2. 자바를 통해 서블릿 등록
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/*");
}
}
WebApplication은 WebApplicationInitializer의 구현체이다.
Tomcat의 서블릿 컨테이너가 구동되면 웹 애플리케이션의ServletContext
를 생성하고 초기화 하기위해 WebApplicationInitializer 인터페이스를 구현한 클래스를 찾아onStartUp
메소드를 호출하는 것이다.
onStartUp
메소드가 호출되면서 DispatcherServlet을 서블릿 컨텍스트에 동적으로add
해주는 것을 확인하며DispatcherServlet
이 생성되는 것을 확인해 볼 수 있다.
Spring MVC 동작의 기본 원리를 정리했다. 이후 MVC 환경 빈 설정에 대해 실습하자.
https://docs.spring.io/spring-framework/docs/current/reference/html/index.html
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1
https://it-mesung.tistory.com/28?category=830537
https://chung-develop.tistory.com/52