[Spring] Spring MVC & DispatcherServlet

leehyunjon·2022년 6월 11일
0

Spring MVC

목록 보기
2/4

Spring MVC

Spring MVC란, Spring에서 사용하는 MVC패턴을 얘기하는 것으로써 MVC패턴은 Model, View, Controller 3가지로 나누어 역할을 분담하고 있는 디자인 패턴이다.
역할을 나누어 처리하기 때문에 클래스간의 결합도가 낮아져 유지보수가 쉽고 좋은 코드가 된다.

Spring MVC 구조

참조 https://velog.io/@neity16/6-%EC%8A%A4%ED%94%84%EB%A7%81-MVC-6-%EC%8A%A4%ED%94%84%EB%A7%81-MVC-%EA%B5%AC%EC%A1%B0-DispatcherServlet-HandlerMapping-HandlerAdapter-viewResolver
  1. Handler Mapping
    • 사용자의 Http요청을 통해 핸들러(컨트롤러)를 찾는다.
  2. Handler Adapter 조회
    • Handler Mapping에서 찾은 핸들러를 처리할 수 있는 핸들러 어댑터를 찾는다.
  3. Handle
    • 찾은 핸들러 어댑터를 통해 핸들러 내부 로직을 실행시키고 그 결과를 ModelAndView에 담아 반환한다.
  4. ViewResolver 호출
    • ViewResolver를 이용해 응답으로 사용할 View를 반환한다.
  5. Render 호출
    • ViewResolver를 통해 찾은 View를 클라이언트에게 반환한다.

Spring MVC는 사용자 요청이 들어오게되면 위의 흐름대로 결과를 반환해주게 된다.
전체적인 흐름에 있어서 가장 중요한 역할을 하는 것은 DispatcherServlet라는 클래스이다.


DispatcherServlet

Servlet기반의 웹 서비스인 경우 Servlet Container에서 요청 URL에 맞는 서비스를 제공하기 위해 URL Mapping과 Servlet을 정의해서 추가해야한다. 그리고 뷰와 모델, 비즈니스 로직이 하나의 서블릿이 관리하면 관심사가 분리되지 않는다. 즉 역할 구분이 되지 않아 유지보수가 어려워진다.

이러한 문제를 해결하기 위해 FrontController패턴으로 하나의 서블릿에서 모든 요청을 받아드리고 각 요청에 맞는 핸들러에게 dispatch해서 역할을 위임시키게 된다.

이런 역할을 하는 것이 Spring의 DispatcherServlet이다.

FrontController 패턴이란 들어오는 요청들을 중앙 집중적으로 처리해서 중복 코드를 제거하는 패턴을 말한다.

DispatcherServlet의 동작 방식

DispatcherServlet은 애플리케이션의 초기화 단계에서 ApplicationContext로 부터 HandlerMapping, HandlerAdapter 등과 같이 기본적략의 빈들을 등록하거나 의존성을 주입받는다.

Servlet Container에 의해 Servlet이 호출되게 되면 FrameworkServlet의 servie()메서드를 실행한다. (FrameworkServlet.service()메서드는 HttpServlet의 오버라이드)

service() 메서드를 호출하게 되면 processRequest()를 거쳐 Dispatcher에서 구현하는 doServie()메서드를 호출하게 된다. doService()는 HttpServletRequest와 HttpServletResponse를 매개변수로 받아와 DispatcherServlet에서 가장 중요한 역할을 하는 doDispatch() 메서드를 호출하게 된다.

doDispatch 주석을 읽어보면 실제로 핸들러에게 요청을 전달하는 처리를 한다.라고 되어있다.
위에서 설명했던 HandlerMapping, Handler Adapter 조회 등의 주요 처리를 이 doDispatch에서 해주게 된다.

HandlerMapping

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

	HandlerExecutionChain mappedHandler = null;
    
    //...
    
    mappedHandler = getHandler(processedRequest);
    
    //...
}

핸들러를 가져오는 getHandler()메서드는 HttpServeletRequest를 매개변수로 받아서 등록되어있는 Handler전략들에게 HttpServletRequest를 전달해서 매칭되는 핸들러를 찾아서 반환해준다.

Get HandlerAdapter

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	//...
    
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    
    //...
}

HandlerMapping을 통해 가져온 핸들러를 getHandlerAdapter()메서드의 매개변수로 주어 HandlerAdapter들의 supports()메서드에 대입하고 부합하는 adapter를 반환해준다.

Handle

요청 Http URL을 통해 HandlerMapping으로 핸들러를 가져오고, 해당 핸들러에 맞는 HandlerAdapter를 받았다면, 핸들러를 실행시켜줄 차례이다.
반환받은 핸들러 어댑터의 handle()메서드를 매핑된 핸들러를 실행시켜 비즈니스 로직을 실행시키고 ModelAndView객체를 반환받는다.

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

	ModelAndView mv = null;
    
	//...
    
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    //...
}

핸들러를 실행시켜주는 핸들러 어댑터의 종류는 여러가지가 있어 handle()메서드의 구현 로직은 어댑터마다 다르다.
주로 RequestMappingHandlerAdapter를 사용하는데, 자세한건 아래에서 설명한다.

ViewResolver

ViewResolver는 논리 뷰 이름을 통해서 실제 물리 주소로 매핑해주는 역할을 한다.

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {

	View view;
		String viewName = mv.getViewName();
		if (viewName != null) {
			// We need to resolve the view name.
			view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
			if (view == null) {
				throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
						"' in servlet with name '" + getServletName() + "'");
			}
		}
		else {
			// No need to lookup: the ModelAndView object contains the actual View object.
			view = mv.getView();
			if (view == null) {
				throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
						"View object in servlet with name '" + getServletName() + "'");
			}
		}
        
        //...
}

handle()에서 받은 ModelAndView객체를 가지고 View를 반환받는다.

참고로 REST API만 개발하는 경우에는 View가 없으니 ViewResolver가 동작하지 않는다.
템플릿 엔진을 사용한 개발 시에만 ViewResolver가 동작한다.

Render

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
	View view;
    
	//...
    
    view.render(mv.getModelInternal(), request, response);
    
    //...
}

이렇게 클라이언트 요청이 들어왔을때 DispatcherServlet의 요청처리 과정을 알아봤다.
그렇다면 요청 Http URL을 통해 비즈니스 로직을 실행시켜주는 역할을 하는 HandlerMapping과 Handler Adapter는 어떻게 요청을 구분하고 핸들러를 찾아주게 되는것일까?


HandlerMapping & Handler Adapter

HandlerMapping

HandlerMapping이란 Http 요청 정보를 이용해서 핸들러(컨트롤러)를 찾아주는 기능을 수행한다.
DispatcherServlet은 등록된 Handler 전략들에게 HttpServletRequest을 전달하여 매칭되는 핸들러를 찾는다.

handler전략에서 자동으로 등록하는 전략 BeanNameUrlHandlerMappingRequestMappingHanlderMapping 두가지가 있다.

Hanlder Mapping 처리 우선순위

  1. RequestMappingHandlerMapping
  2. BeanNameUrlHandlerMapping

Handler 전략

  • BeanNameUrlHandlerMapping
    • Http요청 URL과 빈의 이름을 비교해서 핸들러를 찾는다.

    • 빈 이름에는 ANT패턴이라고 불리는 *, **, ? 을 이용한 패턴을 넣을 수 있다.

    • hello로 시작하면 모두 여기에 매핑된다.

      <bean name="/hello*" class="HelloController"/>

      **는 하나 이상의 경로를 매핑 할 수 있다.

      <bean name="/root/**/sub" class="SubController"/>

      컨트롤러의 개수가 ㅁ낳아지면 URL정보가 XML이나 어노테이션에 분산되어 파악하기 어려워지므로, 복잡한 애플리케이션에서는 잘 사용하지 않는다.

  • RequestMappingHandlerMapping
    • @RequestMapping이라는 어노테이션을 이용해 매핑하는 전략
    • @RequestMapping은 클래스 뿐만 아니라 메서드 단위에도 URL매핑을 할수 있고 method, parameter, header등의 정보도 매핑에 활용할수 있어서 강력하고 편리한 방법으로 현재 가장 많이 사용하는 방법이라고 한다.
  • 그 외
    - ControllerBeanNameHandlerMapping
    - BeanNameUrlHandlerMapping과 유사하지만 빈 이름 앞에 자동으로 '/'이 붙어서 URL에 매핑이되므로 URL형식으로 짓지 않아도 된다.
    - < bean name="hello" class="HelloController" >
    - ControllerClassNameHandlerMapping
    - 빈의 클래스 이름을 URL에 매핑, 클래스 이름이 Controller로 끝날 경우 Controller를 뺀 나머지 이름을 URL에 매핑해준다.
    - SimpleUrlHandlerMapping
    - URL과 컨트롤러 매핑정보를 한곳에 모아놓고 보는 전략
        <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings"> <!-- Properties 타입으로 URL과 빈 이름을 넣어준다 -->
    	<props>
    		<prop key="/hello">helloController</prop>
    		<prop key="/root/**/sub">subController</prop> <!-- ANT 패턴 사용 가능 -->
    	</props>
    </property>
<!-- 빈 이름이 위의 value 와 매핑 -->
<bean id="helloController" />
<bean id="subController" />

Handler Adapter

HandlerMapping을 통해 가져온 핸들러를 직접 실행하는 기능을 하는 인터페이스.
HandlerMapping으로 가져온 핸들러를 Handler Adapter들을 돌면서 각 support()메서드에 대입하고 부합하는 어댑터의 handle() 메서드를 통해 핸들러를 실행하고 ModelView를 리턴한다.

스프링부트가 자동 등록하는 핸들러 어댑터는 RequestMappingHandlerAdapter, HttpRequestHandlerAdapter, SimpleControllerHandlerAdapter 가 있다.

Handler Adapter의 우선순위

  1. RequestMappingHandlerAdapter
  2. HttpRequestHandlerAdapter
  3. SimpleControllerHandlerAdapter

Handler Adapter 종류

  • SimpleControllerHandlerAdapter
    • Controller유형의 핸들러를 지원해준다
      public class SimpleController implements Controller{
      	@Override
          public ModelAndView handleRequest(
          HttpServletRequest reqeust,
          HttpServletResponse response) throws Exception{
          	ModelAndView model = new ModelAndView("OldController - form");
              model.addObject("objectName", "objectValue");
              return model;
          }
      }
    1. 빈의 이름으로 핸들러를 찾아야 하므로 BeanNameUrlHanlderMapping으로 핸들러를 찾는다.
    2. Controller를 상속받았으므로 SimpleControllerHandlerAdapter가 해당 핸들러를 지원해서 실행해준다.
  • HttpRequestHandlerAdapter
    - HttpRequestHandlerAdapter 유형의 핸들러를 지원해준다.
    public class HttpRequestHandlerController implements HttpRequestHandler {
    		@Override
    		public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws
    			ServletException,
    			IOException {
    			System.out.println("HttpRequestHandler.handleRequest");
    		}
    }
    1. 빈의 이름으로 핸들러를 찾아야 하므로 BeanNameUrlHandlerMapping으로 핸들러를 찾는다.
    2. HttpRequestHandler를 상속받았기 때문에 HttpRequestHandlerAdapter가 핸들러를 지원해서 HttpRequestHandlerController를 실행한다.
  • RequestMappingHandlerAdapter
    - @RequestMapping 어노테이션이 달린 핸들러 클래스, 메서드를 대상으로 지원한다.
    @RequestMapping("/api/adapter")
    public class RequestMappingController{
    	@GetMapping("/test")
     public void getMethod(){
     	System.out.println("ReqeustMappingHandlerController.getMehotd());
     }
    }
  1. @RequestMapping 어노테이션을 사용했으므로 RequestMappingHandlerMapping을 통해 핸들러를 찾는다.
  2. @RequestMapping 어노테이션을 사용했기 때문에 RequestMappingHandlerAdapter가 핸들러를 지원해서 RequestMappingController를 실행한다.

Reference

https://velog.io/@neity16/6-%EC%8A%A4%ED%94%84%EB%A7%81-MVC-6-%EC%8A%A4%ED%94%84%EB%A7%81-MVC-%EA%B5%AC%EC%A1%B0-DispatcherServlet-HandlerMapping-HandlerAdapter-viewResolver

https://joont92.github.io/spring/HandlerMapping-HandlerAdapter-HandlerInterceptor/

https://velog.io/@jihoson94/DispatcherServlet-in-Spring-MVC

https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/handler-adapter.html

https://6161990src.tistory.com/94

https://www.baeldung.com/spring-mvc-handler-adapters

https://galid1.tistory.com/526

profile
내 꿈은 좋은 개발자

0개의 댓글