우아한테크코스 레벨4 미션을 진행하면서 스프링 MVC와 유사한 구조를 직접 구현해보게 되었다.
그러면서 실제 Spring은 DispatcherServlet을 어떻게 구현하고 있는지 궁금해졌고 코드를 살펴보게 되었다.
DispatcherServlet에 대한 다른 분들의 이해에 조금이나마 도움이 되면 좋겠다😊
클라이언트의 요청을 전달받아 요청에 맞는 Controller를 찾고 Controller가 반환한 결과값을 View에게 전달하여 알맞은 응답을 생성하는 일을한다.
스프링은 왜 Servlet
여러개가 아닌 DispathcerServlet
하나만으로 클라이언트의 요청을 처리할까?
1. 중복 코드를 제거하기 위해
만약 한개의 URL에
servlet
하나를 매핑한다면, 각servlet
마다 JSP에 Model 값을 넘겨주는 코드들이 중복되어 수행되어야 한다.
하지만DispathcerServlet
하나만 사용함으로써 이러한 중복 코드를 제거할 수 있다.
2. 컨트롤러의 서블릿 의존성을 제거할수 있다.
DispathcerServlet
이 서블릿으로 모든 요청을 받고 알맞는 컨트롤러를 호출 함으로써 자연스럽게 컨트롤러는Servlet
으로 구현하지 않아도 된다. 또한,Controller
에HttpServletRequest
와HttpServletResponse
를 직접 넘기지 않아도 되기때문에Controller
의 서블릿에 대한 의존성을 제거 할 수 있다.
DispathcerServlet
은 다음과 같은 상속구조를 가진다.
가장 상위에 Java 표준인 Servlet
인터페이스를 구현한 HttpServlet
을 상속하고 있음을 알 수 있다.
그래서 DispatcherServlet
은 Java 표준을 기반으로 만들어졌음을 확인할수 있다.
그럼 맨위에서 살펴본 요청 처리에 대한 흐름을 코드를 보면서 살펴봅시돠.
우선, Servlet
은 HTTP 요청이 들어오면 이에 대한 로직 수행을 Service()
을 통해 처리한다.
이에 대한 로직은 아래의 사진과 같이 DispathcerServlet
의 바로 위 부모 클래스인 FrameworkServlet
에 명시되어 호출된다.
아래의 사진은 FrameWorkServlet
의 servcice()
메서드에 대한 선언부 이다.
💡 사진의 초록색 글씨 주석 설명에서 알수 있듯이 Http Method
가 PATCH
인 경우에만 인터셉터하여 처리 하도록 오버라이드를 했다고 한다.
FrameworkServlet
에서 처리하도록 했을까?FrameworkServlet
의 부모 클래스인 HttpServelt
은 service()
메서드에서 patch
를 지원하지 않기 때문이다.
실제로 코드를 따라가보니 Http Method
가 patch
인 경우에 대한 처리를 HttpServlet.service()
는 지원하지 않는다.
그렇다고 Spring
에서 java EE
표준을 변경할수도 없는 노릇이기 때문에 사진과 같이 if/else
분기를 나눠준것 같다.
예전 Http 프로토콜에는 Patch
method가 없던게 아닐까 조심스럽게 추측해본다.😮
아무튼, PATCH 명령어의 경우 위 사진과 같이 FrameworkServlet.processRequest()
를 호출한다.
한번 따라가 보자.
따라가보면 processRequest()
는 내부에서 doService()
메서드를 호출한다.
doService()
을 따라가 보면 아래와 같이 추상 메서드이다.
추상메서드인 doService()
의 구현부는 최종 final class
인 DispathcerServlet
에 구현되어있다.
그래서 servlet
의 service()
가 호출되면 FrameworkServlet
을 통해 최종적으로 DispathcerServlet
의 doService()
메서드가 호출됨을 알 수 있다.
Http Method
가 PATCH
인 경우 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
의 자식 클래스인FrameworkServlet
에doGet()
메서드를 오버라이드 해놓았더니
위 사진의 655번 줄과 같이HttpServlet
에서doGet()
을 호출하여도 자식 클래스인FrameworkServlet.doGet()
을 호출함을 알 수 있었다.
왜지..?🙄
아무튼, 부모 클래스의 메서드를 모두 자식 클래스에 오버라이드 해놓으면 super.xxx()
를 호출하고 내부에서 부모의 메서드를 호출해도 오버라이드 되었기 때문에 자식 클래스의 메서드를 호출한다.
그래서 FrameworkServlet
의 doGet()
메서드를 살펴보면
위와 같이 processRequest()
메서드를 호출하고 이 메서드는 위에서 살펴봤듯이
내부에서 doService()
메서드를 호출한다. POST
/ PUT
/ DELETE
도 위와 같다.
processRequest()
에 대해서 다시한번 아래에 사진을 첨부해보았다.
결과적으로 Tomcat
의 servlet
을 통해 요청이 들어오고 service()
메서드가 호출되면 FrameworkServlet
을 통해 DispatcherServlet
의 doService()
메서드가 호출되고 요청을 처리한다.
service()
요청을 처리하게끔 했을까?😮첫번째는, DispatcherServlet
에게 주어지는 로직들을 줄여주고자 한 의도가 아니였을까 생각한다.
템플릿 패턴을 통해 코드의 일부를 DispatcherServlet
으로 부터 분리할수 있기 때문이다.
두번째는, OCP를 위해서이다. 사용자 요청을 받는 service()
메서드와 요청을 처리할 Hanlder를 찾는 로직을 분리하여 리팩터링 지점을 줄이기 위함이 아니였을까 생각한다.
이제, Service()
메서드를 통해 들어온 요청이 어떻게 DispatcherServlet
에게 전달되는지 살펴보았다.
그럼 Service()
메서드가 실제로 수행되는 DispatcherServlet
의 doService()
메서드를 살펴보자.
코드가 길어서 천천히 살펴보면 우선, ServletRequest
의 속성들을 가져와서 스냅샷을 찍어놓는다.
ServletRequest
에 문제가 생기면 복원하려는 용도인가 싶다🙄
자세히 보고싶지만 시간이 오래걸릴것 같아 넘어갔다.
다음으로 ServletRequest
에 무언가 속성들을 설정해준다.
위에 주석으로는 프레임워크 객체를 핸들러 또는 뷰 객체에서 사용할 수 있도록 하기 위함이라고 한다.
이제 다왔다. 최종적으로 doServie()
메서드는 try문
안의 doDispatch()
메서드를 호출한다.
코드를 쭉 보면, doService()
는 일련의 설정들만 거치고 실제 ServletRequest
에 대한 처리는 doDispatch()
에게 위임 함을 알수 있다.
이제 doDispatch()
를 살펴보자.
doDispatch()
는 코드가 더길다.🙂
그래서 핵심만 살펴보면 getHandler()
를 통해 요청에 맞는 handler
를 찾아옴을 알 수 있다.
맨 처음 그림에서 살펴보았던 DispatcherServlet
이 HandlerMapping
에서 알맞는 핸들러를 가져오는 부분이다.
HandlerMapping
은 DispatcherServlet
보다 더 복잡하게 생겻다.🙂
그래서 우선 이번 포스팅에서는 DispatcherServlet
이 위 사진의 로직들을 코드의 어느 부분에 가지고 있는지만 살펴보자.
그럼 다시 이어서, doDispatch()
메서드의 코드를 보자.
1043번 줄에서 ServletRequest
에 알맞은 Handler
를 가져 온 다음
1050번 줄의 getHandlerAdapter()
를 호출하여 Handler
의 요청 처리 메서드를 호출할 수 있는 HandlerAdapter
를 가져온다.
HandlerMapping
에서 가져온 Handler
로 바로 요청 처리 로직을 수행하면 되는거 아닌가?단순하게 생각해보면, DispatcherServlet
에서 HandlerMapping
을 통해 가져온 Handler
의 요청처리 메서드를 바로 호출하면 될 것 같다.
그런데 불가능하다.😤
왜냐하면 스프링은 Handler
좀 더 쉽게 말해서 Controller
를 만드는 방법을 여러가지 가지고 있고 각 Controller
들의 객체 타입과 요청 처리 메서드가 다 다르기 때문이다.
그래서 위에서 살펴봤던 DispatcherServlet
의 getHandler()
메서드는 아래와 같이 HandlerExecutionChain
이라는 요상한 객체를 반환한다.
그리고 HandlerExecutionChain
이라는 객체내부를 살펴보면
필드로 Object
클래스의 handler
를가지고 있다가 getHandler()
메서드를 통해서 HandlerAdapter
에게 이 handler
객체를 전달한다.
모든 클래스의 부모 클래스인 Object
를 클래스 타입으로 handler
를 가지고 있다.😮
아까도 이야기 했듯이 Handler
를 만드는 방식이 다양하기 때문에 클래스 타입을 Object
로 준것이고, Object
타입인 Handler
에게 적절한 요청 처리 메서드를 주기 위해서 스프링은 아래와 같이 HandlerAdapter
들을 가지는 것이다.
다시 이어서, doDispatch()
로직을 쭉 따라가다보면 아래와 같이 ha.handle()
메서드가 나온다.
ha
는 HandlerAdapter
의 약자이고 handler
의 적절한 요청 처리 메서드를 실행해준다. 그리고 ModelAndView
객체를 반환한다.
Handler
가 다양한 만큼 HandlerAdapter
의 구현체 객체도 얼핏 본것만 5개 정도 되었다. 나중에 추가로 살펴볼 예정이다.Interceptor의 메서드의 이름들인 preHandle()
과 postHandle()
이 보인다.
각각 handler
의 요청처리 메서드가 실행되기 전과 후에 이루어짐을 알 수 있다.
코드를 따라가보니 interceptor
와 관련한 일련의 로직들을 수행 함을 볼 수 있었는데,
실제로 각각 handler
요청 처리 메서드 실행 전후에 1062번 줄과 1074 번 줄에서 interceptor
가 실행되는지는 확인하지 못했다.
무언가 interceptor
와 관련한 일련의 작업들이 발생하는것은 확실하다.
다시 이어서 doDispatch()
코드를 살펴보자.
mappedHandler.applyPostHandle()
까지 끝나게 되면 사진과 같이 processDispatchResult()
메서드를 호출한다.
이 메서드는 파라미터로 ModleAndView
객체를 받아오는데, 파라미터로 보아도 view
와 관련한 처리를 해줌을 알 수 있다.
processDispatchResult()
의 내부 코드를 보면 아래와 같다.
view에 대한 처리를 해주는 코드들을 볼 수 있다.
그러다 실제로 view를 render해주기 위한 render()
메서드를 호출한다.
processDispatchResult()
는 error에 대한 view
를 설정하거나 파라미터인 ModelAndView
가 없는 경우에 대한 처리를 해주고
실제로 view
를 render 하는 메서드는 render()
메서드이다.
위 사진은 DispatcherServlet
의 render()
메서드와 관련한 부분이다.
1375번 줄을 보면 파라미터인 ModelAndView
에 viewName
이 있는 경우에 resolveViewName()
을 호출하여 view
객체를 가져오고,
1383번 줄을 보면 viewName
이 없는 경우에는 파라미터인 ModelAndView
에서 View
객체를 가져옴을 알 수 있다.
그림과 같이 상황에 따라서 resolveViewName()
를 통해서 viewResolver
를 호출하여 view
를 가져오는 것이다.
그리고 DispatcherServlet.render()
메서드는 아래 사진과 같이 view.render()
메서드를 통해 최종적으로 클라이언트에게 view를 랜더링한다.
마지막으로 resolveViewName()
를 살펴보면, viewName
을 통해 알맞은 View
객체를 반환 받아옴을 볼 수 있다.
이상으로 DispatcherServlet
의 요청처리 로직을 살펴보았다.
그림에서 추상적으로 보던 흐름을 실제 코드로 보니 신기하기도 하고 더 잘 이해가 되었다.😊
살펴본 코드들 이외에도 DispatcherServlet
과 관련한 메서드들은 상당히 많기 때문에 필요하거나 궁금해지면 추후에 더 살펴봐야겠다.
다른분들의 DispatcherServlet
의 요청처리 흐름에 대한 이해에 조금이나마 도움이 되면 좋겠다😊
다음 포스팅 부터는 HandlerMapping, HandlerAdapter, DiContainer
등에 대해서도 코드를 살펴볼 예정이다!
좋네요