[Spring] Spring MVC는 어떻게 요청에 응답할까? (Dispatcher Servlet을 중심으로)
0. 들어가며 🏃🏻♂️
스프링 MVC를 사용하면 클라이언트에 요청에 따라 적절한 응답을 만들어낼 수 있습니다. 그 과정을 대략적으로나마 알고있었지만 이를 좀 더 명확하게 정리하는 시간을 가지고자 Dispatcher Servlet을 중심으로 스프링이 어떤 방식을 사용해 요청에 응답하는지 알아보도록 하겠습니다.
1. Spring MVC Cycle을 Dispatcher Servlet과 함께 살펴보기 😀
위 그림은 Spring MVC 구조입니다. 실제로는 Dispatcher Servlet에 요청이 들어가기 전 필터를 거치는데 이 부분은 생략되어 있습니다. 처음 볼때는 한눈에 잘 들어오지 않는 구조라 구조의 중심이 되는 Dispatcher Servlet 내부를 살펴보며 한단계 한단계 살펴보도록 하겠습니다.
0. 우선 따로 등록한 서블릿이 없다면, 클라이언트가 HTTP 요청을 보냈을 때 이 요청을 가장 먼저 맞이하는 것이 바로 Dispatcher Servlet입니다. Front Controller인 Dispatcher Servlet이 들어온 요청들을 전체적인 프로세스로 관장하는 것이죠.
이제부터 Dispatcher Servlet의 내부 코드를 살펴볼 것인데, 살펴볼 코드는 전부 Dispatcher Servlet의 doDispatch 메서드입니다. 해당 메서드가 바로 요청이 들어왔을때 불리는 메서드죠.
1. Dispatcher Servlet은 요청 정보를 통해 해당 요청을 처리해줄 핸들러(컨트롤러)를 조회합니다. 아래 코드는 Dispatcher Servlet의 doDispatch 메서드의 초반부입니다. handler를 for문을 통해 하나하나 뒤져가며 조회하는 행위를 함을 알 수 있습니다.
// 1번 과정 : 현재 요청에 맞는 핸들러 검색
// 현재 요청에 맞는 핸들러를 검색
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 1번 과정의 getHandler 메소드
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
2. 이제 앞서 찾은 핸들러를 가지고 핸들러 어댑터를 찾는 과정을 수행합니다. 핸들러 어댑터라는 이름에서 알 수 있듯 여기에는 어댑터 패턴이 적용되어 있습니다. 어댑터를 쓰는 이유는 스프링이 발전해오면서 여러가지 방식으로 컨트롤러를 작성할 수 있게 되었는데 이에 대응하기 위함입니다. 예를 들어 과거에 사용하던 interface 방식의 컨트롤러나 요즘 사용하는 어노테이션 방식의 컨트롤러 등 다양한 상황에 모두 대응해야하는데 이러한 문제를 스프링은 어댑터 패턴을 적용함으로써 해결한 것으로 보입니다.
아래 코드를 살펴보시면 1번 과정과 비슷하게 어댑터를 찾는 모습을 살펴볼 수 있습니다.
// Determine handler adapter for the current request.
// 2번 과정 : 핸들러 어댑터를 찾기
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 2번 과정의 getHandlerAdapter 메서드
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
3, 4, 5. 이제 핸들러 어댑터를 가지고 핸들러를 실행합니다. 핸들러를 실행하게 되면 이제 우리가 작성한 컨트롤러에게 비로소 요청을 처리하도록 위임하게 되고, 그 결과 값으로 ModelAndView를 반환받습니다.
// 3번 과정 handler를 실행한다.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
6, 7, 8. 앞에서 반환받은 ModelAndView를 가지고 dispatcher servlet은 processDispatchResult라는 메서드를 호출하게 됩니다.
// 6, 7, 8번 과정
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
그 후 아래 사진의 빨간 박스로 표시해놓은 render 메서드를 호출하게 됩니다.
render 메서드를 호출하게되면 아래와 같은 로직을 수행하게 되죠.
첫번째 빨간 박스를 보면 resolveViewName이라는 viewResolver를 통해 view를 반환받고,
두번째 빨간 박스 코드에서 볼 수 있듯, view를 render합니다.
여기까지가 Spring MVC가 요청이 들어왔을 때 응답을 만들어내는 MVC cycle이었습니다.
정리해보면 핸들러 조회 → 핸들러 어댑터 조회 → 핸들러 어댑터 실행 → 핸들러 실행 → ModelAndView 반환 → ViewResolver 호출 → View 반환 → 뷰 렌더링의 순서대로 진행하며 Spring MVC이 요청에 맞는 응답을 만들어내는 것을 알 수 있습니다.
2. 나가며 💨
기술이 어떻게 동작하는지 모르면 마법같다라고 느끼곤 하는데, 이렇게 내부를 살펴보면서 마법을 지식으로 바꿔나가는 과정은 항상 뿌듯한 것 같습니다. 스프링이 요청을 응답으로 만들어내는 마법이 코드로 만들어낸 기술임을 알게된 계기가 된 것 같아 기쁘고, 최근 관심이 생긴 디자인 패턴도 잘 적용된 코드라고 생각하여 천천히 더 생각해보며 확실하게 나의 것으로 만들어나가면 좋을 것 같습니다.
3. 레퍼런스
https://studyandwrite.tistory.com/461
https://mangkyu.tistory.com/18
김영한 스프링 MVC 1편