DispatcherServlet 요청처리 실제 코드 살펴보기

구동희·2022년 10월 9일
2
post-thumbnail
post-custom-banner

글을 쓰게된 계기

우아한테크코스 레벨4 미션을 진행하면서 스프링 MVC와 유사한 구조를 직접 구현해보게 되었다.
그러면서 실제 Spring은 DispatcherServlet을 어떻게 구현하고 있는지 궁금해졌고 코드를 살펴보게 되었다.
DispatcherServlet에 대한 다른 분들의 이해에 조금이나마 도움이 되면 좋겠다😊

DispathcerServlet

클라이언트의 요청을 전달받아 요청에 맞는 Controller를 찾고 Controller가 반환한 결과값을 View에게 전달하여 알맞은 응답을 생성하는 일을한다.

❓❓ 몇가지 의문점들

스프링은 왜 Servlet 여러개가 아닌 DispathcerServlet 하나만으로 클라이언트의 요청을 처리할까?

1. 중복 코드를 제거하기 위해

만약 한개의 URL에 servlet 하나를 매핑한다면, 각 servlet 마다 JSP에 Model 값을 넘겨주는 코드들이 중복되어 수행되어야 한다.
하지만 DispathcerServlet 하나만 사용함으로써 이러한 중복 코드를 제거할 수 있다.

2. 컨트롤러의 서블릿 의존성을 제거할수 있다.

DispathcerServlet 이 서블릿으로 모든 요청을 받고 알맞는 컨트롤러를 호출 함으로써 자연스럽게 컨트롤러는 Servlet 으로 구현하지 않아도 된다. 또한, ControllerHttpServletRequestHttpServletResponse 를 직접 넘기지 않아도 되기때문에 Controller의 서블릿에 대한 의존성을 제거 할 수 있다.

DispathcerServlet 내부 구조 살펴보기

DispathcerServlet 은 다음과 같은 상속구조를 가진다.

가장 상위에 Java 표준인 Servlet 인터페이스를 구현한 HttpServlet 을 상속하고 있음을 알 수 있다.
그래서 DispatcherServlet 은 Java 표준을 기반으로 만들어졌음을 확인할수 있다.

그럼 맨위에서 살펴본 요청 처리에 대한 흐름을 코드를 보면서 살펴봅시돠.

우선, Servlet 은 HTTP 요청이 들어오면 이에 대한 로직 수행을 Service() 을 통해 처리한다.

이에 대한 로직은 아래의 사진과 같이 DispathcerServlet 의 바로 위 부모 클래스인 FrameworkServlet 에 명시되어 호출된다.

아래의 사진은 FrameWorkServletservcice() 메서드에 대한 선언부 이다.

💡 사진의 초록색 글씨 주석 설명에서 알수 있듯이 Http MethodPATCH 인 경우에만 인터셉터하여 처리 하도록 오버라이드를 했다고 한다.

❓왜 PATCH 명령어만 FrameworkServlet 에서 처리하도록 했을까?

FrameworkServlet 의 부모 클래스인 HttpServeltservice() 메서드에서 patch 를 지원하지 않기 때문이다.

실제로 코드를 따라가보니 Http Methodpatch 인 경우에 대한 처리를 HttpServlet.service() 는 지원하지 않는다.

그렇다고 Spring 에서 java EE 표준을 변경할수도 없는 노릇이기 때문에 사진과 같이 if/else 분기를 나눠준것 같다.
예전 Http 프로토콜에는 Patch method가 없던게 아닐까 조심스럽게 추측해본다.😮

아무튼, PATCH 명령어의 경우 위 사진과 같이 FrameworkServlet.processRequest() 를 호출한다.

한번 따라가 보자.

따라가보면 processRequest()는 내부에서 doService() 메서드를 호출한다.
doService()을 따라가 보면 아래와 같이 추상 메서드이다.

추상메서드인 doService() 의 구현부는 최종 final classDispathcerServlet에 구현되어있다.

그래서 servletservice() 가 호출되면 FrameworkServlet을 통해 최종적으로 DispathcerServletdoService() 메서드가 호출됨을 알 수 있다.

Http MethodPATCH 인 경우 DispatcherServlet 이 요청을 처리하는 것이다😮

그럼 PATCH 가 아닌경우는 어떻게 요청을 처리할까?

다시 돌아와서 FrameworkServlet.service() 메서드로 돌아가 코드를 살펴보자.

Http method가 PATCH 가 아닌 경우에는 super.service() 를 호출한다.

❓❓ 그럼 HttpServlet 의 service 로직만 처리하고 끝나는 것 아닌가?

위 사진은 FrameworkServlet.service() 내부의 super.service() 내부를 따라간
HttpServlet 클래스의 service() 메서드 로직 일부이다.

655번 줄을 보면 doGet() 메서드를 호출하는데 느낌으로는 부모 클래스인 HttpServlet.doGet() 만 호출 할 것 같다.

그래서 테스트 코드를 짜서 실험 해봤다.
FrameworkServlet 클래스의 오버라이드 된 service() 메서드에서 super.service() 를 호출하고
HttpServlet의 자식 클래스인 FrameworkServletdoGet() 메서드를 오버라이드 해놓았더니
위 사진의 655번 줄과 같이 HttpServlet 에서 doGet() 을 호출하여도 자식 클래스인 FrameworkServlet.doGet() 을 호출함을 알 수 있었다.

왜지..?🙄

아무튼, 부모 클래스의 메서드를 모두 자식 클래스에 오버라이드 해놓으면 super.xxx() 를 호출하고 내부에서 부모의 메서드를 호출해도 오버라이드 되었기 때문에 자식 클래스의 메서드를 호출한다.

그래서 FrameworkServletdoGet() 메서드를 살펴보면

위와 같이 processRequest() 메서드를 호출하고 이 메서드는 위에서 살펴봤듯이
내부에서 doService() 메서드를 호출한다. POST / PUT / DELETE 도 위와 같다.

processRequest() 에 대해서 다시한번 아래에 사진을 첨부해보았다.

결과적으로 Tomcatservlet을 통해 요청이 들어오고 service() 메서드가 호출되면 FrameworkServlet 을 통해 DispatcherServletdoService() 메서드가 호출되고 요청을 처리한다.

❓❓ 스프링은 왜 굳이 위와 같은 템플릿 메서드 패턴으로 service() 요청을 처리하게끔 했을까?😮

첫번째는, DispatcherServlet 에게 주어지는 로직들을 줄여주고자 한 의도가 아니였을까 생각한다.
템플릿 패턴을 통해 코드의 일부를 DispatcherServlet 으로 부터 분리할수 있기 때문이다.
두번째는, OCP를 위해서이다. 사용자 요청을 받는 service() 메서드와 요청을 처리할 Hanlder를 찾는 로직을 분리하여 리팩터링 지점을 줄이기 위함이 아니였을까 생각한다.


이제, Service() 메서드를 통해 들어온 요청이 어떻게 DispatcherServlet 에게 전달되는지 살펴보았다.

그럼 Service() 메서드가 실제로 수행되는 DispatcherServletdoService() 메서드를 살펴보자.

DispatcherServlet의 doService() 메서드

코드가 길어서 천천히 살펴보면 우선, ServletRequest 의 속성들을 가져와서 스냅샷을 찍어놓는다.

ServletRequest 에 문제가 생기면 복원하려는 용도인가 싶다🙄
자세히 보고싶지만 시간이 오래걸릴것 같아 넘어갔다.

다음으로 ServletRequest 에 무언가 속성들을 설정해준다.

위에 주석으로는 프레임워크 객체를 핸들러 또는 뷰 객체에서 사용할 수 있도록 하기 위함이라고 한다.

이제 다왔다. 최종적으로 doServie() 메서드는 try문 안의 doDispatch() 메서드를 호출한다.

코드를 쭉 보면, doService() 는 일련의 설정들만 거치고 실제 ServletRequest 에 대한 처리는 doDispatch() 에게 위임 함을 알수 있다.

DispatcherServlet의 doDispatch() 메서드

이제 doDispatch() 를 살펴보자.

doDispatch()는 코드가 더길다.🙂
그래서 핵심만 살펴보면 getHandler() 를 통해 요청에 맞는 handler 를 찾아옴을 알 수 있다.

맨 처음 그림에서 살펴보았던 DispatcherServletHandlerMapping 에서 알맞는 핸들러를 가져오는 부분이다.

HandlerMappingDispatcherServlet 보다 더 복잡하게 생겻다.🙂
그래서 우선 이번 포스팅에서는 DispatcherServlet 이 위 사진의 로직들을 코드의 어느 부분에 가지고 있는지만 살펴보자.

그럼 다시 이어서, doDispatch() 메서드의 코드를 보자.

1043번 줄에서 ServletRequest에 알맞은 Handler 를 가져 온 다음
1050번 줄의 getHandlerAdapter() 를 호출하여 Handler 의 요청 처리 메서드를 호출할 수 있는 HandlerAdapter 를 가져온다.

❓❓ 그런데 HandlerMapping 에서 가져온 Handler 로 바로 요청 처리 로직을 수행하면 되는거 아닌가?

단순하게 생각해보면, DispatcherServlet 에서 HandlerMapping을 통해 가져온 Handler 의 요청처리 메서드를 바로 호출하면 될 것 같다.
그런데 불가능하다.😤

왜냐하면 스프링은 Handler 좀 더 쉽게 말해서 Controller를 만드는 방법을 여러가지 가지고 있고 Controller 들의 객체 타입과 요청 처리 메서드가 다 다르기 때문이다.

그래서 위에서 살펴봤던 DispatcherServletgetHandler() 메서드는 아래와 같이 HandlerExecutionChain 이라는 요상한 객체를 반환한다.

그리고 HandlerExecutionChain 이라는 객체내부를 살펴보면

필드로 Object 클래스의 handler 를가지고 있다가 getHandler() 메서드를 통해서 HandlerAdapter 에게 이 handler 객체를 전달한다.

모든 클래스의 부모 클래스인 Object를 클래스 타입으로 handler 를 가지고 있다.😮

아까도 이야기 했듯이 Handler 를 만드는 방식이 다양하기 때문에 클래스 타입을 Object 로 준것이고, Object 타입인 Handler 에게 적절한 요청 처리 메서드를 주기 위해서 스프링은 아래와 같이 HandlerAdapter 들을 가지는 것이다.

다시 이어서, doDispatch() 로직을 쭉 따라가다보면 아래와 같이 ha.handle() 메서드가 나온다.

haHandlerAdapter 의 약자이고 handler 의 적절한 요청 처리 메서드를 실행해준다. 그리고 ModelAndView 객체를 반환한다.

💡 Handler가 다양한 만큼 HandlerAdapter의 구현체 객체도 얼핏 본것만 5개 정도 되었다. 나중에 추가로 살펴볼 예정이다.

💡 doDispatch()메서드의 1062번 줄과 1074번 줄을 보면 익숙한 메서드들이 보인다. 바로 preHandle()과 postHandle() 이다.

Interceptor의 메서드의 이름들인 preHandle()postHandle() 이 보인다.
각각 handler 의 요청처리 메서드가 실행되기 전과 후에 이루어짐을 알 수 있다.
코드를 따라가보니 interceptor 와 관련한 일련의 로직들을 수행 함을 볼 수 있었는데,
실제로 각각 handler 요청 처리 메서드 실행 전후에 1062번 줄과 1074 번 줄에서 interceptor 가 실행되는지는 확인하지 못했다.
무언가 interceptor 와 관련한 일련의 작업들이 발생하는것은 확실하다.

다시 이어서 doDispatch() 코드를 살펴보자.

mappedHandler.applyPostHandle()까지 끝나게 되면 사진과 같이 processDispatchResult() 메서드를 호출한다.

이 메서드는 파라미터로 ModleAndView 객체를 받아오는데, 파라미터로 보아도 view 와 관련한 처리를 해줌을 알 수 있다.

DispatcherServlet의 processDispatchResult() 메서드

processDispatchResult()의 내부 코드를 보면 아래와 같다.

view에 대한 처리를 해주는 코드들을 볼 수 있다.
그러다 실제로 view를 render해주기 위한 render() 메서드를 호출한다.

processDispatchResult() 는 error에 대한 view를 설정하거나 파라미터인 ModelAndView가 없는 경우에 대한 처리를 해주고
실제로 view를 render 하는 메서드는 render() 메서드이다.

DispatcherServlet의 render() 메서드

위 사진은 DispatcherServletrender() 메서드와 관련한 부분이다.

1375번 줄을 보면 파라미터인 ModelAndViewviewName이 있는 경우에 resolveViewName()을 호출하여 view 객체를 가져오고,

1383번 줄을 보면 viewName이 없는 경우에는 파라미터인 ModelAndView에서 View 객체를 가져옴을 알 수 있다.

그림과 같이 상황에 따라서 resolveViewName()를 통해서 viewResolver를 호출하여 view를 가져오는 것이다.

그리고 DispatcherServlet.render() 메서드는 아래 사진과 같이 view.render() 메서드를 통해 최종적으로 클라이언트에게 view를 랜더링한다.

DispatcherServlet의 resolveViewName() 메서드

마지막으로 resolveViewName() 를 살펴보면, viewName을 통해 알맞은 View 객체를 반환 받아옴을 볼 수 있다.

마치면서

이상으로 DispatcherServlet의 요청처리 로직을 살펴보았다.
그림에서 추상적으로 보던 흐름을 실제 코드로 보니 신기하기도 하고 더 잘 이해가 되었다.😊
살펴본 코드들 이외에도 DispatcherServlet과 관련한 메서드들은 상당히 많기 때문에 필요하거나 궁금해지면 추후에 더 살펴봐야겠다.
다른분들의 DispatcherServlet의 요청처리 흐름에 대한 이해에 조금이나마 도움이 되면 좋겠다😊

다음 포스팅 부터는 HandlerMapping, HandlerAdapter, DiContainer 등에 대해서도 코드를 살펴볼 예정이다!

profile
천천히 배워가는 개발꿈나무
post-custom-banner

1개의 댓글

comment-user-thumbnail
2022년 10월 15일

좋네요

답글 달기