[Spring] SpringMVC 동작 원리 구현

곽동현·2022년 6월 30일
0

스프링 입문하기

목록 보기
8/8
post-thumbnail

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와의 요청 반환 과정을 실습해보고 배워보고자 한다.

서블릿(Servlet)

  • 한 프로세스를 공유하는 스레드를 만들어서 요청을 처리한다.
  • 이로인해, 빠르다는 장점을 가지고 있다.

서블릿 엔진 또는 서블릿 컨테이너

  • 세션 관리
  • 네트워크 서비스
  • MIME 기반 Message 인코딩 디코딩
  • 서블릿 생명주기 관리

서블릿 애플리케이션 실행

  • 서블릿 애플리케이션은 서블릿 컨테이너가 실행할 수 있으므로, 반드시 서블릿 컨테이너를 사용해야 한다.

  • 서블릿 컨테이너가 서블릿 애플리케이션 실행하는 방법(서블릿의 생명주기)

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 클래스를 생성해보자.

  • 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는 이 기반으로 만들어졌다. 이에 들어온 요청과 응답을 위한 서블릿 필터와 서블릿 리스너가 필요하다.


서블릿 리스너와 필터

서블릿 리스너는 서블릿 컨테이너에서 발생하는 주요 이벤트들을 감지하고 각 이벤트에 특별한 작업이 필요한 경우 사용한다.

  • Listener
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");
    }
}

리스너의 초기 설정을 커스텀했다. 서블릿 컨테이너가 실행되고 종료되는 라이프사이클에 따른 설정 작업이 실행되도록 하는 로직이다.

서블릿 필터는 들어온 요청을 서블릿으로 보내고, 서블릿이 작성한 응답을 클라이언트로 보내기 전에 특별한 처리가 필요할 때 사용한다.

  • Filter
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 연동

Spring Container는 IoC Container로 불리기도 한다. 또한 ApplicationContext로 주로 사용된다. ApplicationContextServletContext에 등록해보자.

먼저 pom.xml에 (spring-webmvc) 의존성을 추가해야 스프링 컨테이너를 사용할 수 있다. (위에 명시)

  • 리스너 추가
    • 만들어둔 서블릿 어플리케이션 생명주기에 맞춰 스프링 컨테이너를 등록해주기 위해선, Spring이 제공하는 리스너를 필요로 한다.
    <!--ContextLoaderListener는 애플리케이션 컨텍스트(스프링 컨테이너)를 만들어 서블릿 컨텍스트에 등록하는 역할-->
    <listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
  • ContextLoaderListener는 ApplicationContext를 만들어 ServletContext에 등록하는 역할을 한다.
  • ApplicationContext를 서블릿 애플리케이션의 생명주기에 맞춰서 바인딩 해주는 것이다.
  • 등록되어 있는 서블릿이 사용될 수 있도록, ContextLoaderListener를 활용하여 ApplicationContext를 만들어 ServletContext에 등록을 해준다.
  • Java 설정 파일 추가

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 {

}

정리하자면, ContextLoaderListenerAnnotationConfigWebApplicationContext를 만들 때 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/*");
 }
}

WebApplicationWebApplicationInitializer의 구현체이다.
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

profile
읽고 쓰며 생각합니다 💡

0개의 댓글