리루

4. Servlet (써블릿) - Servlet Application에 Spring MVC 적용 본문

#Spring

4. Servlet (써블릿) - Servlet Application에 Spring MVC 적용

뚱보리루 2020. 2. 2. 20:10

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% 할인하는 기간도 간간히 있으니 잘 활용하세용~)