Spring DispatcherSevlet 동작 원리 Part1

Posted by 열정보이
2019. 2. 5. 20:31 Web

안녕하세요.


오늘은 Spring 의 Front ControllerDispatcherServlet에 대해 알아보려고 합니다!


그렇다면 먼저 Front Controller가 무엇인지, 어떠한 역할을 하는지 알아보도록 하죠.


MVC 아키텍처는 보통 Front Controller 패턴과 함께 사용됩니다.

Front Controller를 둠으로써, 서버로 들어오는 모든 요청을 먼저 받아 공통 작업(보안,한글 디코딩 등)을 수행할 수 있고, 필요에 따라 세부 Controller에 작업을 위임해주고, Client에 보낼 View를 최종적으로 선택해 생성해줄 수 있죠.


Spring에서 제공하는 DispatcherServlet이 바로 Spring MVC 패턴의 Front Controller입니다.

다음은 DispatcherServlet이 동작하는 구조입니다.



위 순서에 따라 어떤 작업이 발생하는지 알아보도록 할까요.


1. HTTP 요청


자바 서버의 서블릿 컨테이너(Tomcat 등등)는 HTTP 프로토콜을 통해 들어온 요청이 스프링의 DispatcherServlet에 할당된것이라면, 해당 요청 정보를 DispatcherServlet에 전달해주죠.

보통 우리는 web.xml에 DispatcherServlet이 전달 받을 URL 패턴을 정의합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
        
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
cs


하단에 <url-pattern> 태그를 보면 *.do 로 값이 들어가있죠?

이 의미는 '*.do 로 되는 url 패턴은 DispatcherServlet에게 보내준다' 라는 의미가 됩니다.


이렇게 요청을 받은 DispatcherServlet은 모든 요청에 대해 공통적으로 처리되어야만 하는 작업들을 수행합니다.

보안이나 한글 디코딩 같은 작업 말이죠.

그 이후에 요청에 맞는 작업을 처리하는 컨트롤러에게 작업을 위임해주죠.


2. 요청


먼저 어떤 Controller에게 요청을 위임할지를 결정합니다.

DispatcherServlet은 '핸들러 매핑 전략' 을 이용해서 어떤 핸들러(Controller)에게 작업을 위임할지 결정합니다.


이러한 전략은 HandlerMapping 인퍼테이스를 구현해서 만들 수 있습니다.

그렇다면 이런 질문을 할 수 있죠.

"나는 HandlerMapping 인터페이스를 구현한적이 없는데? 어떻게 알아서 되는거야?"


프로젝트를 만들면 위에 코드를 다시 봐볼까요?

저희는 web.xml에 DispatcherServlet에 대한 정보를 설정했고, 설정하는 URL을 servlet-context.xml에 하겠다고 명시했습니다.

그럼 servlet-context.xml에 어떻게 명시되어 있는지 볼까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
    
    <!-- Enables the Spring MVC @Controller programming model -->
    <annotation-driven />
    
    <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
    <resources mapping="/resources/**" location="/resources/" />
 
    <!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
    <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <beans:property name="prefix" value="/WEB-INF/views/" />
        <beans:property name="suffix" value=".jsp" />
    </beans:bean>
    
    <context:component-scan base-package="com.test.test" />    
</beans:beans>
 
cs


저희가 주목해야 하는 코드는 <annotaion-driven /> 입니다.

<annotaion-driven />을 명시하게 되면, DispatcherServlet의 전략 중, HandlerMapping  전략과 HandlerAdapter 전략을 Default 값으로 명시하게 됩니다. 그렇기에 저희는 어떠한 전략을 구현하지 않아도, Default 값을 사용할 수 있는것이죠.


참고로, Default로 설정되는 HandlerMapping 전략은

BeanNameUrlHandlerMappingDefaultAnnotationHandlerMapping 전략입니다.


BeanNameUrlHandlerMapping 전략은 bean 으로 등록한 name을 HTTP 요청을 통해 들어온 URL와 비교하여, 일치한다면 해당 URL을 처리할 bean을 해당 빈으로 선택하는 방법입니다. 직관적이고 편리할 수 있으나, bean의 개수가 많아지고 복잡해 질 경우 관리가 더욱 어려워져 큰 프로젝트에서는 잘 사용하지 않는다고 합니다.


DefaultAnnotationHandlerMapping 전략은 @RequestMapping 어노테이션을 Controller class나 method에 직접 부여하여 명시된 URL과 HTTP요청을 통해 들어온 URL을 비교해 Handler를 mapping 해주는 전략입니다.

method 단위로 URL을 처리할 수 있어 유용하다고 합니다.


이렇게 어떠한 Controller에게 요청을 위임할지 정했다면, 다음은 해당 Controller Object의 메소드를 호출해서 실제로 웹 요청을 처리하는 작업을 해야 합니다.

이때, DispatcherServlet은 해당 Controller의 메소드의 포맷을 모르는데 어떻게 알아서 넘겨줄 수 있을까요?

아무것도 모르는 DispatcherServlet을 위해 Controller의 정보를 제공하는 녀석이 Adapter입니다.

이러한 Adapter는 DispatcherServlet과 Controller 사이에서 다리 역활을 해줍니다.


위 사진 처럼 DispatcherServlet은 Controller 가 어떤 메소드를 가졌고 어떤 인터페이스를 구현했는지 전혀 알지 못합니다.

대신 Controller 의 종류에 따라 적합한 Adapter를 사용하는것이죠.

Adapter는 자신이 담당하는 Controller에 맞는 호출 방법을 이용해 Controller에 작업 요청을 보내고, 결과를 돌려받아 다시 DispatcherServlet에 전달합니다.

DispatcherServlet은 Adapter에 웹 요청 정보가 담긴 HttpServletRequest 타입의 오브젝트를 전달하고, Adapter가 적절히 변환해서 Controller의 메소드가 받을 수 있는 파라미터로 전달해주는 것 입니다.


이러한 전략을 통해 우리는 자유롭게 Controller를 만들어도 DispatcherServlet을 수정할 필요가 없죠.

또한, 어떤 Adapter를 사용할지는 'Handler Adapter 전략'을 통해 결정할 수 있습니다.

무한한 확장성을 자랑하는 Spring의 MVC 패턴 구조의 기반이 되는 방법인것 같습니다.



3. 생성


이렇게 DispatcherServlet은 http 요청을 알맞은 Controller에게 전달하게 되면, Controller는 Model을 생성할 수 있습니다.

Controller의 역할은 크게 'Model을 생성하여 정보를 넣어주는 일''어떤 뷰를 사용할지 결정하는 일' 이 있죠.


Model은 어떻게 사용할까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
public class HomeController {
    
    private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
    
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(HttpServletRequest request, HttpServletResponse reponse,  Model model) {
        
        String name = "열정 보이";
        model.addAttribute("test", name);
        return "home";
    }    
}
cs


위를 보시면, 파라미터를 통해 Model 값을 넘겨 받네요.

그리고 Model 값에 "test" 라는 key에 name object를 전달해줍니다.

이를통해 Model 은 Map 형태의 key, value로 저장되는걸 유추할 수 있겠죠?


그리고 localhost:8080/test/ 를 검색하게 되면 다음과 같은 화면이 나옵니다.



${} 는 Expression Language 라고 합니다. 구글에 검색하면 쉽게 찾으실 수 있습니다.

무튼 Model에 저장된 값을 가져올 수 있죠.

이번에는 localhost:8080:/test 로 처음에 요청을 보내고 Redirect해서 modelLife.jsp로 요청을 다시 보내봤습니다.


물론, Controller에서 아래와 같이 값을 넣어줬구요.


1
2
3
4
5
String name = "열정 보이";
model.addAttribute("test", name);
request.getSession().setAttribute("test2", name);
request.setAttribute("test3", name);
 
cs


위 결과를 보고 알 수 있는것은, 'Model의 생명주기는 request의 생명주기와 같다' 라고 할 수 있겠네요.

forward 로 보내면 당연히 request와 model 모두 값이 남아 있겠죠 ㅎ


그럼 내용이 길어지기 때문에 이번 내용은 여기서 마무리하고,

다음에 더 이어서 진행하도록 하겠습니다~