Backend/Spring

Spring MVC의 구조(DispatcherServlet, HandlerMapping, HandlerAdapter)

olsohee 2024. 1. 3. 15:23

DispatcherServlet

public class DispatcherServlet extends FrameworkServlet {}
  • DispatcherServlet은 표현 계층 전면에서 HTTP 프로토콜을 통해 들어오는 모든 요청을 가장 먼저 받아 공통 작업을 처리하고, 적합한 컨트롤러에게 요청을 위임해주는 프론트 컨트롤러이다. 즉, 클라이언트로부터 어떠한 요청이 오면 톰캣과 같은 서블릿 컨테이너가 요청을 받는다. 그리고 이 모든 요청을 프론트 컨트롤러인 디스패처 서블릿에게 보낸다. 그러면 디스패처 서블릿은 공통적인 작업을 처리한 후에 해당 요청을 처리해야 하는 컨트롤러를 찾아서 요청을 위임한다.
  • DispatcherServlet의 상속 관계는 다음과 같다. 즉, DispatcherServlet도 결국에는 간접적으로 HttpServlet을 상속받기 때문에 서블릿으로 등록되어 동작한다.
public class DispatcherServlet extends FrameworkServlet {}

public abstract class FrameworkServlet extends HttpServletBean {}

public abstract class HttpServletBean extends HttpServlet {}
  • 그렇다면 DispatcherServlet에는 @WebServlet도 붙어있지 않은데 어떻게 서블릿으로 등록될까? 스프링 부트는 DispatcherServlet을 서블릿으로 자동으로 등록하고, 모든 경로(urlPatterns="/")에 대해서 매핑한다.
  • 따라서 어떤 경로를 호출하든 DispatcherServlet이 호출된다.
  • 참고로 더 자세한 경로의 우선순위가 높다. 따라서 개발자가 별도로 만들고 @WebServlet를 통해 등록한 서블릿도 정상적으로 서블릿으로 등록되어 동작한다.

DispatcherServlet의 동작 흐름

  1. HTTP 요청이 들어오면 WAS는 요청마다 HttpServletRequest, HttpServletResponse 객체를 생성하고, 모든 요청에 대해 매핑된 DispatcherServlet의 service() 메소드를 호출한다. 그리고 service() 메소드의 인자로 HttpServletRequest, HttpServletResponse 객체를 전달한다. (스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해두었다. 즉, 정확히 말하면 FrameworkServlet.service() 메소드가 호출된다.)
  2. FrameworkServlet.service()를 시작으로 여러 메소드가 호출되다가 DispatcherServlet의 doDispatch() 메소드가 호출된다.
  3. DispatcherServlet.doDispatch()에서는 다음과 같은 과정이 일어난다.
    1. HTTP 요청을 HandlerMapping에 위임해서 해당 요청을 처리할 Handler(Controller)를 조회한다.
    2. 찾은 Hander를 실행할 수 있는 HandlerAdapter를 조회한다.
    3. 찾은 HandlerAdapter를 통해 Handler의 메소드를 실행한다.
    4. HandlerAdapter는 Handler에서 반환한 정보를 ModelAndView로 변환해서 반환한다.
    5. DispatcherServlet은 View 이름을 ViewResolver에게 전달하고, ViewResolver는 뷰의 논리 이름을 물리 이름으로 바꾸고 렌더링 역할을 하는 View 객체를 반환한다.
    6. DispatcherServlet은 View에게 Model을 전달하고 화면 표시를 요청한다.
    7. 최종적으로 DispatcherServlet은 View 결과(HttpServletResponse)를 클라이언트에게 반환한다.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    ModelAndView mv = null;

    // 핸들러 조회(핸들러 매핑)
    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null) {
        noHandlerFound(processedRequest, response);
        return;
    }

    // 핸들러 어댑터 조회(핸들러를 처리할 수 있는 어댑터 조회)
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

    // 핸들러 어댑터 실행 -> 핸들러 어댑터를 통해 핸들러 실행 -> ModelAndView 반환 
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {

    // 뷰 렌더링 호출
    render(mv, request, response);
}

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

    View view;
    // 뷰 리졸버를 통해서 뷰 찾기
    String viewName = mv.getViewName(); 
    
    // View 반환
    view = resolveViewName(viewName, mv.getModelInternal(), locale, request);

    // 뷰 렌더링
    view.render(mv.getModelInternal(), request, response);
}

HandlerMapping, HandlerAdapter

스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현해두었다. 따라서 개발자가 직접 핸들러 매핑과 핸들러 어댑터를 만드는 일은 거의 없다. 스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터는 다음과 같다. 우선순위가 높은 핸들러 매핑과 핸들러 어댑터부터 조건 검사를 실행하면서 핸들러와 핸들러 어댑터를 찾는다.

 

스프링에는 다음과 같은 우선순위로 핸들러 매핑이 등록되어 있다. 핸들러 매핑은 요청 url을 통해 적절한 핸들러(= 컨트롤러 = 스프링 빈 객체)를 찾는다.

  1. RequestMappingHandlerMapping: 어노테이션 기반의 핸들러들 중에서 해당 url과 매핑된 핸들러를 찾는다. 즉, @Controller가 클래스 레벨에 붙어있는 빈 중에서 해당 url과 매핑된 핸들러를 찾는다. 
  2. BeanNameUrlHandlerMapping: 스프링 빈 이름으로 핸들러를 찾는다. 예를 들어, http://localhost:8080/hello라는 url을 호출하면, hello라는 이름의 스프링 빈을 찾는다.

스프링에는 다음과 같은 우선순위로 핸들러 어댑터가 등록되어 있다.

  1. RequestMappingHandlerAdapter: 어노테이션 기반의 핸들러를 처리한다.
  2. HttpRequestHandlerAdapter: HttpRequestHandler 인터페이스를 구현한 핸들러를 처리한다.
  3. SimpleControllerHandlerAdapter: Controller 인터페이스를 구현한 핸들러를 처리한다.

예제1. BeanNameUrlHandlerMapping, SimpleControllerHandlerAdapter

@Component("/springmvc/old-controller")
public class OldController implements Controller {
	
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
    	System.out.println("OldController.handleRequest");
        return null;
    }	
}
  1. @Component("/springmvc/old-controller"): 컨트롤러가 /springmvc/old-controller라는 이름의 스프링 빈으로 등록된다.
  2. http://localhost:8080/springmvc/old-controller url을 호출한다.
  3. 핸들러 조회
    • 스프링에 등록된 핸들러 매핑들을 우선순위대로 실행해서 적절한 핸들러를 찾는다.
    • 이 경우 BeanNameUrlHandlerMapping 핸들러 매핑을 통해 /springmvc/old-controller라는 이름의 스프링 빈인 OldController가 반환된다.
  4. 핸들러 어댑터 조회
    • 스프링에 등록된 핸들러 어댑터들을 우선순위대로 supports()를 호출해서 위에서 찾은 핸들러를 지원하는 핸들러 어댑터인지 확인하며, 핸들러 어댑터를 찾는다.
    • 이 경우 Controller 인터페이스를 지원하는 SimpleControllerHandlerAdapter가 그 대상이 된다.
  5. 핸들러 어댑터 실행
    • DispatcherServlet이 조회한 핸들러 어댑터를 실행하면서 핸들러 정보도 함께 넘겨준다.
    • 핸들러 어댑터(SimpleControllerHandlerAdapter)는 핸들러(OldController)를 내부에서 실행하고, 그 결과를 반환한다.

예제2. BeanNameUrlHandlerMapping, HttpRequestHandlerAdapter

@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler {
	
    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    	System.out.println("MyHttpRequestHandler.handleRequest");
        return null;
    }	
}
  1. @Component("/springmvc/request-handler"): 컨트롤러가 /springmvc/request-handler라는 이름의 스프링 빈으로 등록된다.
  2. http://localhost:8080/springmvc/request-handler url을 호출한다.
  3. 핸들러 조회
    • 스프링에 등록된 핸들러 매핑들을 우선순위대로 실행해서 적절한 핸들러를 찾는다.
    • 이 경우 BeanNameUrlHandlerMapping 핸들러 매핑을 통해 /springmvc/request-handler라는 이름의 스프링 빈인 MyHttpRequestHandler가 반환된다.
  4. 핸들러 어댑터 조회
    • 스프링에 등록된 핸들러 어댑터들을 우선순위대로 supports()를 호출해서 위에서 찾은 핸들러를 지원하는 핸들러 어댑터인지 확인하며, 핸들러 어댑터를 찾는다.
    • 이 경우 HttpRequestHandler 인터페이스를 지원하는 HttpRequestHandlerAdapter가 그 대상이 된다.
  5. 핸들러 어댑터 실행
    • DispatcherServlet이 조회한 핸들러 어댑터를 실행하면서 핸들러 정보도 함께 넘겨준다.
    • 핸들러 어댑터(HttpRequestHandlerAdapter)는 핸들러(MyHttpRequestHandler)를 내부에서 실행하고, 그 결과를 반환한다.

애노테이션 기반의 컨트롤러(@Controller, @RequestMapping)

앞서 살펴본 바와 같이 우선순위가 가장 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping과 RequestMappingHandlerAdapter이다. 이는 지금 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 핸들러 매핑과 핸들러 어댑터이다. 

 

애노테이션 기반의 컨트롤러를 사용하려면 @Controller와 @RequestMapping을 사용하면 된다.

@Controller
public class MainController {

	@RequestMapping("/hello")
	public void process() {
		// "/hello" url로 요청하면 해당 메소드가 실행된다.
	}
}
  • @Controller
    • @Controller 내부에는 @Component가 있다. 즉, 해당 클래스가 스프링 빈으로 등록된다.
    • RequestMappingHandlerMapping은 @Controller가 클래스 레벨에 붙어있는 빈 중에서 요청 url과 매핑된 빈을 찾는다.
      • 즉, "/hello"라는 url 요청이 들어왔을 때 RequestMappingHandlerMapping이 적절한 핸들러(MainController)를 찾는다.
      • 그리고 RequestMappingHandlerAdapter가 이 핸들러를 처리한다.
  • @RequestMapping
    • 해당 url이 호출되면 @RequestMapping이 붙은 메서드가 호출된다.

큰 흐름 정리

@Controller, @RequestMapping 등의 어노테이션은 스프링을 사용하면서 자주 사용된다. 따라서 그 흐름을 잊고 습관적으로 사용하게 되는데, 지금까지 정리한 내용을 바탕으로 그 흐름을 대략적으로 이해해보자.

  1. HTTP 요청이 들어오면 프론트 컨트롤러 역할을 하는 DispatcherServlet이 호출된다. 이 DispatcherServlet도 서블릿이다 (HttpServlet을 상속받는다). 스프링은 DispatcherServlet을 서블릿으로 자동 등록한다. 그리고 모든 경로(urlPatterns="/")에 대해 매핑한다. 따라서 HTTP 요청이 들어오면 항상 DispatcherServlet이 호출되는 것이다.
  2. DispatcherServlet이 호출되면서 HttpServlet이 제공하는 service() 메소드가 호출된다. DispatcherServlet의 부모인 FrameworkServlet가 service() 메소드가 오버라이드한다. 따라서 FrameworkServlet.service() 메소드가 호출된다.
  3. FrameworkServlet.service()를 시작으로 여러 메소드가 호출되다가 DispatcherServlet.doDispatch() 메소드가 호출된다. DispatcherServlet.doDispatch()에서는 다음과 같은 과정이 일어난다.
    1. 핸들러 매핑을 통해 핸들러를 조회한다.
      •  스프링에 등록된 핸들러 매핑들을 우선순위대로 실행한다. 
      • RequestMappingHandlerMapping은 어노테이션 기반의 핸들러들 중 적절한 핸들러를 찾는다. 따라서 요청 url로 매핑된 @Controller 어노테이션이 붙은 핸들러(컨트롤러)를 찾아서 반환한다. 이때 @Controller 내부에는 @Component가 있기 때문에 해당 객체는 스프링 컨테이너에 보관되어 있다. 따라서 이때 스프링 컨테이너에 보관된 빈이 조회되는 것이다.
    2.  조회한 핸들러를 처리할 수 있는 핸들러 어댑터를 조회한다.
      1. 스프링에 등록된 핸들러 어댑터를 우선순위대로 실행한다. 
      2. RequestMappingHandlerAdapter는 어노테이션 기반의 핸들러를 처리한다. 따라서 RequestMappingHandlerAdapter이 조회된다.
    3. 핸들러 어댑터를 통해 핸들러를 실행한다.
      • 위에서 조회한 핸들러 어댑터(RequestMappingHandlerAdapter)가 핸들러(@Controller가 붙은 빈 객체)를 실행한다. 

Reference