스프링 공식 문서 뿌수기(10), DispatcherServlet - handler 실습

2023. 5. 6. 14:34자바/스프링

728x90
반응형

지난 포스팅에서 HandlerMapping부터 handler에 값을 전달하는 것까지 대충 눈으로 확인해봤다. 그런데, 솔직히 이해가 잘 안된다. 그래서 이번 포스팅에서는 코드로 한번 작성해보면서 이를 확인해보려고 한다.


DispatcherServlet handler

지난 포스팅에서 요약했던 내용을 한번 들고와 보자.

 

서블릿은 요청에 대해서 응답을 처리해주는 역할을 한다. 서블릿 컨테이너(혹은 웹 컨테이너)는 여러 개의 서블릿을 가지고 있어서 요청이 들어왔을 때 이에 맞는 서블릿을 동작시킨다. 여러 개의 서블릿 중에서 스프링에서 사용하는 서블릿은 dispatcherServlet이다.
 
가장 먼저 외부에서 요청이 들어오면 dispatcherServlet은 그 요청에 맞는 handler를 매핑한다. 기본적으로 등록된 handlerMapping은 BeanNameUrlHandlerMapping과 RequestMappingHandlerMapping이다. BeanNameUrlHandlerMapping은 빈에 등록된 핸들러 이름을 보고 매핑을 시켜준다. RequestMappingHandlerMapping은 @RequestMapping 어노테이션을 사용해 등록한 핸들러의 url 주소를 보고 매핑해준다.
 
HandlerMapping을 통해 찾은 handler가 있다면 이제 dispatcherServlet에 등록된 adapter들에게 mapping된 handler를 다룰 수 있는지 묻는다. 만약 다룰 수 있는 adapter가 있면 그 adapter에게 handler를 전달해서 그 handler의 동작을 수행한다. HttpRequestHandler를 구현한 handler의 경우에는 HttpRequestHandlerAdapter를 adapter로 사용한다. Controller를 구현한 handler의 경우에는 SimpleControllerHandlerAdapter를 adapter로 사용한다. 그리고 @RequestMapping 어노테이션을 사용해 등록한 handler는 RequestMappingHandlerAdapter를 adapter로 사용한다.
만약 HandlerMapping을 통해 handler가 없다면 servlet에서 지정한 예외 페이지와 json이 전달된다.
 
이후 요청에 대해서 handler가 동작한 뒤 adapter에서 적절하게 처리해서 ModelAndView를 리턴하면 dispatcherServlet에서 이를 받아서 응답 처리를 한다.

 

오... 정말 모르겠는 걸~

 

지난 포스팅에서 정리했던 내용을 그림으로 그려보면 다음과 같다.

왼쪽의 서블릿 컨테이너로부터 request 요청을 받으면 dispatcherServlet은 handlerMapping을 한 뒤 적절한 handler를 찾고, 이를 adapter에 전달해서 처리를 하는 식으로 동작한다. 구글링을 해봐도 이에 대한 그림이 없어서... 직접 그려봤다. ㅠㅠ

 

만약 위의 그림에 나오는 용어들이 기억 안난다면 한번 이전 포스팅 자료와 함께 보면 좋을 것 같다.

 

https://konghana01.tistory.com/611

 

DispatcherServlet 코드

 

 

가장 먼저 DispatcherServlet 코드를 한번 살펴보자

 

DispatcherServlet 내부에는 doDispatch라는 메서드가 존재한다. 그리고 그 메서드 내부에는 다음과 같은 코드가 등장한다.

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = HttpMethod.GET.matches(method);
				if (isGet || HttpMethod.HEAD.matches(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

 

초반부에 보면 아래와 같은 코드가 등장한다.

주석으로 설명이 나와있듯이 request를 전달해서 getHandler를 하는 과정이 등장한다. 여기서 요청을 전달하고 이에 대응하는 handler를 매핑해주는 과정을 거친다.

 

그리고 이 메서드에 들어가보면

HandlerMapping에 대해서 getHandler를 하는 코드가 등장하는데, 이게 바로 위에서 살펴봤던 HandlerMapping을 하는 과정이다. 앞서 살펴본 BeanNameUrlHandlerMapping이나 RequestMappingHandlerMapping 모두 이 HanlderMapping을 구현하고 있어서 이와 같이 request에 대응하는 handler를 찾을 수 있다.

 

그리고 handler를 찾을 수 없다면 noHandlerFound라는 메서드가 수행되는데

 

해당 메서드는 우리가 이전 포스팅에서 봤던 대로 잘못된 요청에 대해 화이트라벨이나 404 json 파일을 전달할 때 사용된다. 이때 우리가 작성한 handler에 매핑되지 않기 때문에 우리가 따로 예외 처리를 하지 않아도 예외 처리가 되는 것을 알 수 있다.

 

그리고 매핑되는 handler를 찾았다면 아래의 코드가 수행된다.

 

곧바로 getHandlerAdapter라는 메서드를 통해서 현재 request에 대해 사용할 수 있는 adapter를 찾는다.

해당 메서드는 이전 포스팅에서 언급했던 대로 adapter에게 handler를 전달해서 '너 이 handler를 다룰 수 있어?'를 묻는다. supports 메서드가 true라면 adapter가 handler를 다룰 수 있다고 판단해서 해당 adapter를 반환한다.

 

 

이후 반환된 adapter에 대해 request, response, handler를 전달해서 요청에 대한 응답을 처리하도록 한 뒤에 dispatcherServlet에서 결과를 처리한 뒤에 동작을 마무리하는 것을 알 수 있다. (뒤에 몇 가지 동작들이 더 있지만 이는 postHandle과 관련된 내용이기에 나중에 다루도록 하겠다.) 

 

지난 포스팅에서는 공식문서로만 확인을 해서 좀 이해하기 어려웠는데 이제 이해가 되기 시작한다.

 

이번에는 handler를 직접 만들어보면서 어떻게 동작하는지 알아보도록 하겠다.

 

 

HttpRequestHandler

 

 

가장 먼저 HttpRequestHandler를 한번 만들어보자!

 

 

우선 HttpRequestHandler를 구현한 CustomHttpRequestHandler를 만들었다. Component 어노테이션을 사용해 pom-michutda라는 이름으로 해당 핸들러를 빈을 등록했다. 

 

그리고 /pom-michutda라는 url로 접속을 하게 되면!

 

 

아래와 같이 핸들러에서 response에 설정한 값이 잘 반영된 것을 알 수 있다. 

 

아래와 같이 위의 코드에 대해서 동작 과정을 정리해볼 수 있을 것 같다.

진한 검은색으로 칠해진 부분이 동작한다.

1. 클라이언트가 /pom-michutda url로 접속한다.

2. 서블릿 컨테이너에서 디스패처 서블릿이 핸들러 매핑을 한다. 이때 BeanNameUrlHandlerMapping를 사용 빈 이름이 '/pom-michutda'인 CustomHttpRequestHandler로 매핑된다. 

3. CustomHttpRequestHandler는 HttpRequestHandler를 구현하고 있기에 이를 동작하기 위해서 HttpRequestHandlerAdapter를 통해 Handler와의 접점을 만든다.

4. 이후 CustomHttpRequestHandler가 동작한다.

 

이렇게 그림을 그리고 보니 조금 명확하게 어떻게 동작하는지 알 수 있을 것 같다.

 

 

Controller

 

자, Controller를 한번 만들어보자.

 

 

Controller도 마찬가지로 handlerRequest 메서드가 존재하지만 return 값이 ModelAndView이다. 따라서 모델과 뷰를 동시에 설정해줄 수 있다. 그래서 이번에는 특별히 html 파일도 만들었다.

 

jkm.html


그리고 마찬가지로 component로 등록한 빈의 이름을 url로 검색하면!

 

 

크으~~ 잘 나오는 것을 확인할 수 있다.  이 또한 동작 과정을 한번 간략하게 서술해보자.

 

1. 클라이언트가 /joong-kkeok-ma 접속한다.

2. 서블릿 컨테이너에서 디스패처 서블릿이 핸들러 매핑을 한다. 이때 BeanNameUrlHandlerMapping를 사용 빈 이름이 '/joong-kkeok-ma'인 CustomController 매핑된다. 

3. CustomController는 Controller를 구현하고 있기에 이를 동작하기 위해서 SimpleControllerHandlerAdapter를 통해 Handler와의 접점을 만든다.

4. 이후 CustomController가 동작한다.

 

 

 

RequestHandler

 

마지막으로 @RequestMapping 어노테이션으로 등록한 Handler는 어떤 식으로 동작하는지 확인해보자. (물론 여태 이걸 사용해왔지만 말이다~)

 

이번 장바구니 미션에서 작성한 관리자 페이지이다.

 

 

@RestController 어노테이션과 @RequestMapping 어노테이션을 사용하고 있다. 클라이언트가 /admin url로 접속하면 displayItemList 메서드(handler)가 수행될 텐데, 이 메서드는 @ModelAttribute를 사용하고 있기에 admin 페이지로 넘어갈 것이다.

 

그리고 admin 페이지는 쪼매 길어서 생략하도록 하겠다. 아무튼 /admin 페이지로 접속해보자.

 

하나

 

짠!

 

페이지가 올바르게 로드되는 것을 확인할 수 있다. 

 

이 또한 동작 과정을 살펴보자

1. 클라이언트가 /admin으로 접속한다.

2. 서블릿 컨테이너에서 디스패처 서블릿이 핸들러 매핑을 한다. 이때 RequestMappingHandlerMapping을 사용 RequestMapping이 '/admin'인 AdminController의 displayItemList 매핑된다. 

3. AdminController @RequestMapping을 사용하고 있기에 이를 동작하기 위해서 RequestMappingHandlerAdapter를 통해 Handler와의 접점을 만든다.

4. 이후 displayItemList가 동작한다.

 

여기까지이다. 사실 RequestMappingHanlderAdapter는 다른 HandlerAdapter보다 훨씬 더 많은 동작을 수행한다. 위에서도 보면 알 수 있듯이 다양한 request를 처리한다. 그리고 다양한 방식으로 response를 처리한다. Http methods도 다양하고, 요청을 처리할 때 데이터를 받는 방식도 다양하고, 데이터를 정제해서 응답하는 것도 다양하다. 그러니 당연히 내용이 많아질 수 밖에 없다. 뭐 구체적으로 이는 어떤 동작들을 수행하는지 차차 알아보도록 하겠다.

 

코드를 들춰보며 나름 재미는 있었다. dispatcherServlet에서 처리할 때 어떤 식으로 하는지, 이번 step 2에서 등장한 Resolver가 무엇인지, Interceptor는 무엇인지 등등에 대해서 아직 공부해야할 내용이 많다. 일단 며칠 후에 발표를 해야하는 내가 맡은 스터디 주제가 MVC config이니 다음 포스팅에서는 이를 한번 다뤄보도록 하겠다.

 

 

근데... @RequestMapping 어노테이션을 사용하면 Controller나 HttpRequestHandler를 따로 구현해서 사용하지 않아도 handler를 만들 수 있는데, 왜 이걸 분리해둔거지??

728x90
반응형