Dispatcher-Servlet이란?
디스패처 서블릿의 dispatch는 "보내다"라는 뜻을 가지고 있다. 그리고 이러한 단어를 포함하는 디스패처 서블릿은 표현 계층(Presentation layer) 전면에서 HTTP 프로토콜을 통해 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임해주는 중앙집중식으로 처리되는 프론트 컨트롤러(Front Controller)라고 정의할 수 있다.
※ 참고
OSI 7 계층 : 어플리케이션 -> 표현 -> 세션 -> 전송 -> 네트워크 -> 데이터링크 -> 물리
TCP/IP 계층 : 어플리케이션 -> 전송 -> 인터넷 -> 네트워크 엑세스(데이터링크+물리)
DispatcherServlet은 Spring MVC의 핵심 요소이다. 클라이언트로부터 어떠한 요청이 오면 Tomcat(톰캣)과 같은 서블릿 컨테이너가 요청을 받게 된다. 이때 이 모든 요청을 프론트 컨트롤러인 디스패처 서블릿이 가장 먼저 받게 된다. 그러면 디스패처 서블릿은 공통적인 작업을 먼저 처리한 후에 해당 요청을 처리해야 하는 컨트롤러를 찾아서 작업을 위임한다.
여기서 Front Controller(프론트 컨트롤러)라는 용어가 사용되는데, Front Controller는 주로 서블릿 컨테이너의 제일 앞에서 서버로 들어오는 클라이언트의 모든 요청을 받아서 처리해주는 컨트롤러로써, MVC 구조에서 함께 사용되는 디자인 패턴이다.
DispatcherServlet도 *Servlet이다. DispatcherServlet 클래스 코드를 살펴보면 HttpServlet을 상속하고 있다. (정확히는 DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet 상속 구조를 갖고 있다.)
public class DispatcherServlet extends FrameworkServlet {
}
public abstract class FrameworkServlet extends HttpServletBean {
}
public abstract class HttpServletBean extends HttpServlet {
}
Dispatcher-Servlet의 장점
Spring MVC는 DispatcherServlet이 등장함에 따라 web.xml의 역할을 상당히 축소시켜주었다. 과거에는 모든 서블릿을 URL 매핑을 위해 web.xml에 모두 등록해주어야 했지만, dispatcher-servlet이 해당 어플리케이션으로 들어오는 모든 요청을 핸들링해주고 공통 작업을 처리면서 상당히 편리하게 이용할 수 있게 되었다. 우리는 컨트롤러를 구현해두기만 하면 디스패처 서블릿이 알아서 적합한 컨트롤러로 위임을 해주는 구조가 되었다.
정적 자원(Static Resources)의 처리
Dispatcher Servlet이 요청을 Controller로 넘겨주는 방식은 효율적으로 보인다. 하지만 Dispatcher Servlet이 모든 요청을 처리하다보니 이미지나 HTML/CSS/JavaScript 등과 같은 정적 파일에 대한 요청마저 모두 가로채는 까닭에 정적자원(Static Resources)을 불러오지 못하는 상황도 발생하곤 했다. 이러한 문제를 해결하기 위해 개발자들은 2가지 방법을 고안했다.
1. 정적 자원 요청과 애플리케이션 요청을 분리
이에 대한 해결책은 두가지가 있는데 첫번째는 클라이언트의 요청을 2가지로 분리하여 구분하는 것이다.
- /apps 의 URL로 접근하면 Dispatcher Servlet이 담당한다.
- /resources 의 URL로 접근하면 Dispatcher Servlet이 컨트롤할 수 없으므로 담당하지 않는다.
이러한 방식은 괜찮지만 상당히 코드가 지저분해지며, 모든 요청에 대해서 저런 URL을 붙여주어야 하므로 직관적인 설계가 될 수 없다. 그래서 이러한 방법의 한계를 느끼고 다음과 같은 방법으로 처리를 하게 되었다.
2. 애플리케이션 요청을 탐색하고 없으면 정적 자원 요청으로 처리
두번째 방법은 Dispatcher Servlet이 요청을 처리할 컨트롤러를 먼저 찾고, 요청에 대한 컨트롤러를 찾을 수 없는 경우에, 2차적으로 설정된 자원(Resource) 경로를 탐색하여 자원을 탐색하는 것이다. 이렇게 영역을 분리하면 효율적인 리소스 관리를 지원할 뿐 아니라 추후에 확장을 용이하게 해준다는 장점이 있다.
Dispatcher-Servlet(디스패처 서블릿)의 동작 과정
Dispatcher-Servlet(디스패처 서블릿)의 동작 방식
앞서 설명한대로 디스패처 서블릿은 적합한 컨트롤러와 메소드를 찾아 요청을 위임해야 한다. Dispatcher Servlet의 처리 과정을 살펴보면 다음과 같다.
- 클라이언트의 요청을 Dispatcher Servlet에 전달
- 요청 정보를 HandlerMapping을 통해 요청을 위임할 Handler(Controller)를 찾음
- 요청을 Handler(Controller)로 위임할 Handler Adapter를 찾아서 전달함
- Handler Adapter가 컨트롤러로 요청을 위임함
- 비지니스 로직을 처리함
- 컨트롤러가 반환값을 반환함
- Handler Adapter가 반환값을 처리함
- 서버의 응답을 클라이언트로 반환함
아래에서는 디스패처 서블릿의 동작 과정을 좀 구체적으로 살펴보도록 하겠다. 만약 아래의 내용을 완벽히 이해하기 어렵다면 "디스패처 서블릿을 통해 요청을 처리할 컨트롤러를 찾아서 위임하고, 그 결과를 받아오는구나" 정도로만 이해해도 된다.
1. 클라이언트의 요청을 디스패처 서블릿이 받음
앞서 설명하였듯 디스패처 서블릿은 가장 먼저 요청을 받는 프론트 컨트롤러다. 서블릿 컨텍스트(웹 컨텍스트)에서 필터들을 지나 스프링 컨텍스트에서 디스패처 서블릿이 가장 먼저 요청을 받게된다.
이를 그림으로 표현하면 다음과 같다.
실제로는 Interceptor가 Controller로 요청을 위임하지는 않으므로, 아래의 그림은 처리 순서를 도식화한 것으로만 이해하면 된다.
2. 요청 정보를 HandlerMapping을 통해 요청을 위임할 Handler(Controller)를 찾음
디스패처 서블릿은 요청 정보를 바탕으로, 요청을 처리할 컨트롤러를 찾고 해당 메소드를 호출해야 한다.
오늘날에는 주로 @Controller와 @RequestMapping 관련 어노테이션을 조합하여 컨트롤러를 생성하므로, 요청을 처리할 컨트롤러는 주로 HandlerMapping의 구현체 중 하나인 RequestMappingHandlerMapping이 찾아준다.
해당 객체는 내부에서 HashMap으로 (요청 정보, 처리 대상)을 관리하고 있어서, 요청 정보를 Key로 사용하여 요청을 처리할 대상을 찾아온다. 이때 요청을 처리할 대상은 컨트롤러가 아닌 컨트롤러를 가지고 있는 HandlerMethod 객체다.
HandlerMethod를 갖는 이유는 컨트롤러와 컨트롤러의 메소드 등을 포함해 부가적인 정보들이 더욱 필요하기 때문이다.
그리고 찾아온 HandlerMethod를 HandlerMethodExecutionChain으로 감싸서 반환화는데, 그 이유는 컨트롤러로 요청을 넘겨주기 전에 처리해야 하는 인터셉터 등을 포함하기 위해서다.
3. 요청을 Handler(Controller)로 위임할 Handler Adapter를 찾아서 전달함
디스패처 서블릿은 컨트롤러로 요청을 직접 위임하는 것이 아니라 HandlerAdapter를 통해 어댑터 패턴을 적용해 컨트롤러로 요청을 위임한다. 이때 어댑터 인터페이스를 통해 컨트롤러를 호출하는 이유는 컨트롤러의 구현 방식이 다양하기 때문이다.
스프링은 꽤나 오래 전에(2004년) 만들어진 프레임워크로, 트렌드를 굉장히 잘 따라간다. 프로그래밍 흐름에 맞게 스프링 역시 변화를 따라가게 되었는데, 그러면서 다양한 코드 작성 방식을 지원하게 되었다. 과거에는 컨트롤러를 Controller 인터페이스로 구현하였는데, Ruby On Rails가 어노테이션 기반으로 관례를 이용한 프로그래밍을 내세워 혁신을 일으키면서 스프링 역시 이를 도입하게 되었다.
그래서 다양하게 작성되는 컨트롤러에 대응하기 위해 스프링은 HandlerAdapter라는 어댑터 인터페이스를 통해 어댑터 패턴을 적용함으로써 컨트롤러의 구현 방식에 상관없이 요청을 위임할 수 있도록 하였다.
4. Handler Adapter가 컨트롤러로 요청을 위임함
핸들러 어댑터가 컨트롤러로 요청을 넘기기 전에 공통적인 전처리 과정이 필요하다. 요청에 매칭되는 인터셉터들도 실행을 시키고, @RequestParam, @RequestBody 등으로 파라미터를 준비하는 ArgumentResolver도 실행하는 등의 다양한 공통 작업들이 수행된다.
이러한 전처리 작업들이 완료되면 파라미터 값들과 함께 컨트롤러로 요청을 위임한다.
5. 비지니스 로직을 처리함
이후에 컨트롤러는 서비스를 호출하고 우리가 작성한 비지니스 로직들이 진행된다.
6. 컨트롤러가 반환값을 반환함
비지니스 로직이 처리된 후에는 컨트롤러가 반환값을 반환한다. 응답 데이터를 사용하는 경우에는 주로 ResponseEntity를 반환하게 되고, 응답 페이지를 보여주는 경우라면 String으로 View의 이름을 반환할 수도 있다.
7. 핸들러 어댑터가 반환값을 처리함
핸들러 어댑터는 컨트롤러로부터 받은 반환값을 응답 처리기인 ReturnValueHandler가 후처리한 후에 디스패처 서블릿으로 돌려준다. 만약 컨트롤러가 ResponseEntity를 반환하면 HttpEntityMethodProcessor가 MessageConverter를 사용해 응답 객체를 직렬화하고 응답 상태(HttpStatus)를 설정한다. 만약 컨트롤러가 View 이름을 반환하면 View를 반환하기 위한 준비 작업을 처리한다.
8. 서버의 응답을 클라이언트로 반환함
디스패처 서블릿을 통해 반환되는 응답은 다시 필터들을 거쳐 클라이언트에게 반환된다. 이때 응답이 데이터라면 그대로 반환되지만, 응답이 화면이라면 View의 이름에 맞는 View를 찾아서 반환해주는 ViewResolver가 적절한 화면을 내려다.
위 흐름들은 @Controller, @RestController에 따라 ViewResolver 혹은 MessageConverter가 응답을 한다는 것을 확일할 수 있을 것이다.
이번 포스팅에서는 디스패처 서블릿에 대해 이해해보았다. 위의 과정 역시 상당히 많은 부분을 추상화하고 생략하여 핵심 부분만을 설명하였는데, 실제 과정은 훨씬 복잡하고 많은 처리들이 존재한다.
보다 더 깊게 디스패처 서블릿의 실제 소스 코드를 보면서 분석해보고 싶다면 이 글을 참고!
이 글에서는 디스패처 서블릿의 동작 과정과 함께 적용된 디자인 패턴들(전략 패턴, 템플릿 메소드 패턴, 컴포지트 패턴)을 살펴볼 수 있다.