Backend/Spring Boot

코드 분석을 통해 알아보는 스프링 MVC 동작 과정

sangwonYoon 2023. 10. 13. 17:20

이번 포스팅에서는 클라이언트의 HTTP 요청을 받아 컨트롤러가 요청을 처리하고, 그 결과로 HTML 응답이 반환되어 화면이 그려지는 사이에 스프링 MVC 프레임워크가 어떻게 동작하는지 코드를 분석하며 중요한 로직 위주로 알아볼 것이다.

모든 코드를 line by line으로 설명하면 글이 너무 길어지고 산만해지기 때문에 주요 흐름을 제외한 코드들을 상당 부분 생략했다.

 


 

1. 클라이언트로부터 HTTP 요청을 받아 DispatcherServlet 클래스의 doService() 메소드가 호출된 후, doService() 메소드 내부에서 doDispatch() 메소드가 호출된다.

DispatcherServlet 클래스는 HTTP 요청을 가장 먼저 받아, 적절한 컨트롤러에게 넘겨주는 역할을 하는 클래스이다.

DispatcherServlet의 역할을 Front Controller라고 부르며, Front Controller 패턴에 대해서는 다음 포스팅에서 자세히 다룰 예정이다.

 

// DispatcherServlet 클래스의 멤버 변수 handlerMappings

    /** List of HandlerMappings used by this servlet. */
    @Nullable
    private List<HandlerMapping> handlerMappings;

여기서 handler는 컨트롤러라고 생각하면 된다. 즉, handlerMappings는 간단하게 어떤 HTTP 요청에는 어떤 핸들러를 사용해야 하는지 매핑해놓은 멤버 변수라고 생각할 수 있다.

 

// DispatcherServlet 클래스의 멤버 변수 handlerAdapters
    
    /** List of HandlerAdapters used by this servlet. */
    @Nullable
    private List<HandlerAdapter> handlerAdapters;

handlerAdapters는 DispatcherServlet이 handler를 동작시키기 위해 중간 매개체로 사용하는 HandlerAdapter 인스턴스를 모아놓은 멤버 변수이다. 자세히 어떻게 동작하는지 뒤에서 설명하겠다.

 

// DispatcherServlet 클래스의 doService() 메소드

    /**
     * Exposes the DispatcherServlet-specific request attributes and delegates to {@link #doDispatch}
     * for the actual dispatching.
     */
    @Override
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {

HTTP 요청이 들어오면, DispatcherServlet의 doService() 메소드가 호출된다.

 

// DispatcherServlet 클래스의 doDispatch() 메소드

    /**
     * Process the actual dispatching to the handler.
     * <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
     * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
     * to find the first that supports the handler class.
     * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
     * themselves to decide which methods are acceptable.
     * @param request current HTTP request
     * @param response current HTTP response
     * @throws Exception in case of any kind of processing failure
     */
    @SuppressWarnings("deprecation")
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

doService()의 내부에서 doDispatch() 메소드를 호출한다.

 

2. HTTP 요청에 맞는 핸들러를 찾아온다.

// DispatcherServlet 클래스의 doDispatch() 메소드 중 일부

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

doDispatch() 내부에서 getHandler() 메소드를 통해 현재 HTTP 요청에 맞는 핸들러를 가져온다.

 

// DispatcherServlet 클래스의 getHandler() 메소드 중 일부

    /**
     * Return the HandlerExecutionChain for this request.
     * <p>Tries all handler mappings in order.
     * @param request current HTTP request
     * @return the HandlerExecutionChain, or {@code null} if no handler could be found
     */
    @Nullable
    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;
    }

DispatcherServlet 클래스의 getHandler() 메소드는 handlerMappings에 담긴 HandlerMapping 인스턴스에 현재 HTTP 요청을 넣어보면서 HandlerMapping 인터페이스의 getHandler() 메소드가 null이 아닌 값을 반환할 때까지 반복하여 HTTP 요청에 대한 핸들러를 찾는다.

 

// HandlerMapping 인터페이스의 getHandler() 추상 메소드

    /**
     * Return a handler and any interceptors for this request. The choice may be made
     * on request URL, session state, or any factor the implementing class chooses.
     * <p>The returned HandlerExecutionChain contains a handler Object, rather than
     * even a tag interface, so that handlers are not constrained in any way.
     * For example, a HandlerAdapter could be written to allow another framework's
     * handler objects to be used.
     * <p>Returns {@code null} if no match was found. This is not an error.
     * The DispatcherServlet will query all registered HandlerMapping beans to find
     * a match, and only decide there is an error if none can find a handler.
     * @param request current HTTP request
     * @return a HandlerExecutionChain instance containing handler object and
     * any interceptors, or {@code null} if no mapping found
     * @throws Exception if there is an internal error
     */
    @Nullable
    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

HandlerMapping 인터페이스의 getHandler() 메소드는 구현체마다 구현 방식이 서로 다르지만, 공통적으로 request(HTTP 요청)에 맞는 handler를 반환하는 역할을 한다.

 

3. 핸들러에 맞는 어댑터를 찾아온다.

// DispatcherServlet 클래스의 doDispatch() 메소드 중 일부

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

핸들러를 찾아온 이후, getHandlerAdapter() 메소드를 통해 핸들러를 처리할 수 있는 핸들러 어댑터를 찾아온다.

 

// DispatcherServlet 클래스의 getHandlerAdapter() 메소드
    
    /**
     * Return the HandlerAdapter for this handler object.
     * @param handler the handler object to find an adapter for
     * @throws ServletException if no HandlerAdapter can be found for the handler. This is a fatal error.
     */
    protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
        if (this.handlerAdapters != null) {
            for (HandlerAdapter adapter : this.handlerAdapters) {
                if (adapter.supports(handler)) {
                    return adapter;
                }
            }
        }

getHandlerAdapter() 메소드에서는 위에서 핸들러를 찾는 방식과 유사하게 handlerAdapters에 담긴 HandlerAdapter에 핸들러를 넣어보면서 이 핸들러 어댑터가 핸들러를 처리할 수 있는지 확인한다.

 

4. 어댑터 패턴을 통해 컨트롤러가 HTTP 요청을 처리한다.

// DispatcherServlet 클래스의 doDispatch() 메소드 중 일부
    
    // Actually invoke the handler.
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

찾아온 핸들러 어댑터를 통해 핸들러를 처리하도록 handle() 메소드를 호출한다.

 

5. HTTP 요청을 처리한 후 반환된 결과값을 어댑터에서 ModelAndView 객체에 담아서 반환한다.

// HandlerAdapter 인터페이스의 handle() 추상 메소드

    /**
     * Use the given handler to handle this request.
     * The workflow that is required may vary widely.
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler the handler to use. This object must have previously been passed
     * to the {@code supports} method of this interface, which must have
     * returned {@code true}.
     * @return a ModelAndView object with the name of the view and the required
     * model data, or {@code null} if the request has been handled directly
     * @throws Exception in case of errors
     */
    @Nullable
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

함수 설명에서 살펴볼 수 있듯이, HandlerAdapter 인터페이스의 handle() 메소드는 주어진 핸들러를 사용하여 요청을 처리한 뒤, ModelAndView 객체를 리턴받는 메소드이다.

 

ModelAndView 클래스는 View(화면)에 대한 정보와 화면에 전달할 정보인 Model을 갖는 클래스이다.

// ModelAndView 클래스의 멤버 변수 view

    /** View instance or view name String. */
    @Nullable
    private Object view;

멤버 변수 view에는 View의 이름 정보인 viewName이 존재하거나 View 객체 자체가 들어있다. 

 

// ModelAndView 클래스의 멤버 변수 model

    /** Model Map. */
    @Nullable
    private ModelMap model;

멤버 변수 model에는 화면에 전달할 데이터가 담겨있다. MVC 패턴의 Model이 하는 역할과 같다고 생각하면 된다.

 

// 컨트롤러 예시

@Controller
public class ResponseViewController {

    @RequestMapping("/response-view-v1")
    public ModelAndView responseViewV1(){
        ModelAndView mav = new ModelAndView("response/hello").addObject("data", "hello!");

        return mav;
    }
}

예를 들어, 위와 같은 컨트롤러(핸들러)를 핸들러 어댑터가 handle() 메소드를 통해 처리한다면, ModelAndView 객체인 mav가 반환될 것이다.

DispatcherServlet이 핸들러를 핸들러 어댑터를 통해 처리하는 이유는 다음 포스팅에서 자세하게 알아보도록 하겠다.

 

6. ModelAndView 객체에서 View 객체를 얻은 뒤, Model 정보를 활용해 화면을 렌더링한다.

// DispatcherServlet 클래스의 doDispatch() 메소드 중 일부

    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

핸들러가 요청을 처리한 뒤 반환한 ModelAndView 인스턴스를 processDispatchResult() 메소드의 인자로 넣어 호출한다.

 

// DispatcherServlet 클래스의 processDispatchResult() 메소드 중 일부

    // Did the handler return a view to render?
    if (mv != null && !mv.wasCleared()) {
        render(mv, request, response);

processDispatchResult() 메소드는 DispatcherServlet 클래스의 render() 메소드를 호출하여 ModelAndView 인스턴스에 담긴 View에 대한 정보와 Model 정보를 가지고 화면을 렌더링한다.

 

// DispatcherServlet 클래스의 render() 메소드 중 일부
    
    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();

DispatcherServlet 클래스의 render() 메소드에서는 먼저 View 인스턴스에 viewName이 있을 경우 viewName을 갖고 View 객체를 생성하는 resolveViewName() 메소드를 통해 View 객체를 얻고, View 객체가 존재할 경우 View 객체를 가져온다.

 

// DispatcherServlet 클래스의 render() 메소드 중 일부

    view.render(mv.getModelInternal(), request, response);

최종적으로 View 클래스의 render() 메소드를 호출하여 View 객체의 화면 정보와 Model 정보를 활용해 화면을 렌더링한다.

 

다음 포스팅에서는 스프링 MVC 구조에서 살펴볼 수 있는 Front Controller 패턴과 Adapter 패턴에 대해 알아볼 예정이다.

'Backend > Spring Boot' 카테고리의 다른 글

Spring MVC의 Front Controller 패턴과 Adapter 패턴  (2) 2023.12.18