본문 바로가기
스프링(부트)/스프링 내용 정리

Servlet과 Spring MVC

by doflamingo 2021. 1. 7.

Servlet과 Spring MVC

서블릿(Servlet)

자바 서블릿(Java Servlet)은 자바를 사용하여 웹페이지를 동적으로 생성하는 서버측 프로그램 혹은 그 사양을 말하며, 흔히 "서블릿"이라 불린다.

서블릿을 사용하는 방법은 서블릿 애플리케이션을 만든 다음 web.xml에 servlet을 등록해주고 servlet을 원하는 url과 매핑해준다.

public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        System.out.println("Do Get");
        resp.getWriter().println("<html>");
        resp.getWriter().println("<header>");
        resp.getWriter().println("</header>");
        resp.getWriter().println("<body>");
        resp.getWriter().println("<h1>Hello</h1>");
        resp.getWriter().println("</body>");
        resp.getWriter().println("</html>");
    }

    @Override
    public void destroy() {
        System.out.println("Servlet Destroy");
    }

    @Override
    public void init() {
        System.out.println("Servlet Init");
    }
}
<!--web.xml-->
<web-app>
  <display-name>Web Application</display-name>

  <servlet>
    <servlet-name>hello</servlet-name>
    <servlet-class>me.doflamingo.HelloServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>hello</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>

</web-app>

이 서블릿 애플리케이션을 실행시키면 서블릿 컨테이너가(WAS) 실행될 때 서블릿이 초기화되고 서블릿 컨테이너가 내려갈 때 서블릿이 제거된다.

그리고 web.xml에서 설정해준 것처럼 /hello라는 url이 불러졌을 때 "hello"가 브라우저에 나오게 된다.

서블릿 리스너(Servlet Listener)

웹 애플리케이션에서 발생하는 주요 이벤트를 감지하고 각 이벤트에 특별한 작업이 필요한 경우에 사용할 수 있다.

서블릿 별로 서블릿 컨텍스트(Servlet Context)라는 저장소같은 공간이 존재하는데 여기에 attribute를 저장해서 서블릿에서 이용할 수도 있다.

이런 관리는 Servlet Listener가 맡아서 한다.

Servlet Listener를 설정하는 방법은 ServletLisntenr 클래스를 생성해서 Listener로 등록하면 된다.

public class MyListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("Context Initialized");
        sce.getServletContext().setAttribute("name", "myname");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("Context Destroyed");
    }
}
<!--web.xml-->
<web-app>
  <display-name>Web Application</display-name>

  <listener>
    <listener-class>me.doflamingo.MyListener</listener-class>
  </listener>
  <servlet>
    <servlet-name>hello</servlet-name>
    <servlet-class>me.doflamingo.HelloServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>hello</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>

</web-app>

위에서 저장해준 attribute를 확인하기 위해서 servlet에서 attribute를 가져다 쓰는 코드를 추가하면

public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        System.out.println("Do Get");
        resp.getWriter().println("<html>");
        resp.getWriter().println("<header>");
        resp.getWriter().println("</header>");
        resp.getWriter().println("<body>");
        resp.getWriter().println("<h1>Hello "+getServletContext().getAttribute("name")+"</h1>");
        resp.getWriter().println("</body>");
        resp.getWriter().println("</html>");
    }

    @Override
    public void destroy() {
        System.out.println("Servlet Destroy");
    }

    @Override
    public void init() {
        System.out.println("Servlet Init");
    }
}

브라우저에 Hello myname이 출력된다.

서블릿 필터(Servlet Filter)

서블릿 필터는 서블릿으로 들어오는 요청이나 나가는 응답 전후에 특별한 처리를 하기 위해서 사용하는 것으로 서블릿 컨테이너에 등록해서 사용할 수 있다.

또한, 필터는 체이닝되어 여러개의 필터를 거칠 수 있다. (Ex. Spring Security)

필터 체이닝 아키텍처

서블릿 필터를 등록하는 방법은 filter 클래스를 만들고 web.xml에 마찬가지로 filter 등록을 해주고 filter-mapping을 해주면 된다.

여기서는 filter1과 filter2를 만들어서 두개의 filter를 체이닝 해보겠다.

public class MyFilter1 implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter1 Init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Do Filter1");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {
        System.out.println("Filter1 Destroy");
    }
}

public class MyFilter2 implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter2 Init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Do Filter2");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {
        System.out.println("Filter2 Destroy");
    }
}
<filter>
    <filter-name>filter1</filter-name>
    <filter-class>me.doflamingo.MyFilter1</filter-class>
  </filter>
  <filter>
    <filter-name>filter2</filter-name>
    <filter-class>me.doflamingo.MyFilter2</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>filter1</filter-name>
    <servlet-name>hello</servlet-name>
  </filter-mapping>
  <filter-mapping>
    <filter-name>filter2</filter-name>
    <servlet-name>hello</servlet-name>
  </filter-mapping>

이렇게 추가하고 수행하게 되면 서블릿 컨테이너가 시작하면서 Servlet Listener로 등록해놓은 서블릿 컨텍스트가 먼저 초기화 되고 filter가 초기화 된다.

필터 초기화

그리고 매핑해놓은 서블릿 hello로 요청이 들어오면 filter가 체이닝 되어 서블릿 들어오기 전에 지나게 된다.

필터 체이닝

필터가 없어지는건 역시 서블릿 컨테이너가 내려가면 필터가 제거된다.

서블릿에서 스프링 빈 사용

서블릿에서도 스프링에서 만든 빈을 사용할 수 있다.

사용하는 방법은 스프링 프레임워크에서 제공하는 ContextLoaderListener를 서블릿 리스너로 등록하면 된다.

ContextLoaderListener

ContextLoaderListener는 IOC 컨테이너인 Application Context를 만들어준다.

Application Context를 Servlet Context 생명주기에 맞춰 등록하고 삭제해준다.

IOC 컨테이너는 ServletContext를 통해 사용 가능하다.

등록하는 방법은 Listener로 ContextLoaderListener를 등록해주고 context-param으로 Application Context로 사용할 클래스를 등록해주고, 사용한 config 클래스를 등록해준다.

일단, Config로 사용할 클래스를 만든다.

@ComponentScan
public class AppConfig {
}

그리고 등록할 빈을 하나 만들어준다. 여기서는 Service 하나를 만들었다.

@Service
public class HelloService {

    public String getName() {
        return "My Service";
    }
}

@Componentscan는 어노테이션이 붙어있는 클래스가 있는 경로와 하위 패키지 밑에 @Component가 달려있는 걸 빈으로 등록해준다.

이제 web.xml에 contextLoaderListener와 context-param을 등록한다.

<web-app>
  <display-name>Web Application</display-name>

  <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.doflamingo.AppConfig</param-value>
  </context-param>
  <filter>
    <filter-name>filter1</filter-name>
    <filter-class>me.doflamingo.MyFilter1</filter-class>
  </filter>
  <filter>
    <filter-name>filter2</filter-name>
    <filter-class>me.doflamingo.MyFilter2</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>filter1</filter-name>
    <servlet-name>hello</servlet-name>
  </filter-mapping>
  <filter-mapping>
    <filter-name>filter2</filter-name>
    <servlet-name>hello</servlet-name>
  </filter-mapping>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <servlet>
    <servlet-name>hello</servlet-name>
    <servlet-class>me.doflamingo.HelloServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>hello</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>

</web-app>

Application Context로 AnnotationConfigWebApplicationContext클래스를 사용했는데 이 클래스는 @Configuration@Component가 붙어있는 클래스를 IOC 컨테이너에 빈으로 등록해준다.

그리고 ApplicationContext의 Configuration을 할 클래스를 등록해준다.

서블릿의 doGet 메소드에서 빈이 잘 주입되었는지 확인할 수 있다.

@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        System.out.println("Do Get");
        WebApplicationContext webApplicationContext = (WebApplicationContext) getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
        HelloService helloService = webApplicationContext.getBean(HelloService.class);
        resp.getWriter().println("<html>");
        resp.getWriter().println("<header>");
        resp.getWriter().println("</header>");
        resp.getWriter().println("<body>");
        resp.getWriter().println("<h1>Hello "+helloService.getName()+"</h1>");
        resp.getWriter().println("</body>");
        resp.getWriter().println("</html>");
    }

서블릿 컨텍스트에서 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE라는 key값으로 IOC Container(Application Context)가 저장되어 있고 IOC 컨테이너에서 HelloService 빈을 불러서 사용하면 Hello My Name이라는 결과를 확인할 수 있다.

즉, 빈이 잘 주입되었음을 알 수 있다.

스프링 MVC와 서블릿 연동

서블릿을 위처럼 하나씩 연결할 수도 있지만 그러기엔 번거롭고 복잡하다.

그래서 나온 디자인 패턴이 Front Controller라는 맨 앞의 Controller가 앞에서 모든 요청을 받고 각각의 Application Controller에게 각각 위임해주는 방식이다.

Front Controller Design Pattern

Front Controller 역할을 스프링 프레임워크에서는 DispatcherServlet이라는 서블릿이 하게 된다.

DispatcherServletServlet WebApplicationContext를 만든다. 이것은 Root WebApplicationContext를 부모로 가지고 있다.

그래서 Servlet WebApplicationContext가 관리하지 않는 빈은 Root WebApplicationContext로 위임하기도 한다.

원래는 Servlet WebApplicationContext는 Web과 관련된 빈과 비즈니스등 이외의 로직을 가지고 있는 빈으로 구분해서 사용하려고 만들었지만 그냥 Servlet WebApplicationContext에서 전부 관리하기도 한다. (보통은 DisaptcherServlet이 하나만 존재하기 때문에 다른 DispatcherServlet과 Root WebApplicationContext를 공유할 필요가 없음)

Servlet WebApplicationContext는 서블릿만 가지고 있지만 Root WebApplicationContext는 모든 서블릿이 공유한다.

DispatcherServlet

그렇다면 DispatcherServlet을 등록하는 방법을 알아보자.

역시 web.xml에 Servlet 등록하는 방법으로 등록을 해주면 된다. 대신, DispatcherServlet은 Application Context를 생성하므로 어떤 ApplicationContext를 생성하고 어떤 Config를 Context에 사용할지를 context-param으로 넘겨주면 된다.

<web-app>
  <display-name>Web Application</display-name>

  <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.doflamingo.AppConfig</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>app</servlet-name>
    <url-pattern>/app/*</url-pattern>
  </servlet-mapping>

</web-app>

기존에 있던 hello 서블릿과 같이 사용해도 되지만 가독성을 위해 hello 서블릿은 삭제하고 새로 app이라는 서블릿을 DispatcherServlet으로 생성하였고 Context Class와 Config는 동일하게 사용하였다.

그리고 서블릿 매핑은 /app이라는 url path를 가지고 있으면 전부 DispatcherServlet을 지나도록 구성하였다.

이제 DispatcherServlet이라는 Front Controller를 지나서 HelloController라는 Controller에 기능을 위임하도록 HelloController 코드를 추가한다.

@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    @GetMapping("/hello")
    public String getName() {
        return "Hello "+helloService.getName();
    }
}

이렇게 해서 실행하게 되면

/app/hello 로 들어오는 url은 Dispatcher Servlet을 지나 @Controller가 붙은 빈을 찾아서 /hello가 매핑되어 있으면 그 메서드를 수행하게 된다.

즉, "Hello"+ helloService.getName() (Hello My Name)을 return한다.

이 일련의 코드는 https://github.com/bosuksh/SpringWebMVC에서 확인할 수 있다.

커밋 메시지로 구분되었다.

출처 : https://ko.wikipedia.org/wiki/%EC%9E%90%EB%B0%94_%EC%84%9C%EB%B8%94%EB%A6%BF

인프런 스프링 웹MVC 강좌

http://mrbool.com/how-to-implement-filter-in-java-server-page/30541

www.corej2eepatterns.com/FrontController.htm

댓글