티스토리 뷰

반응형

이번에는 SpringBoot의 실행 과정을 소스 코드로 직접 살펴보려고 합니다. 지난번에는 빈들을 찾아서 객체로 만들고 후처리를 해주는 refresh 메소드를 자세히 살펴보았는데, 마지막으로 DispatcherServlet(디스패처 서블릿) 동작 과정을 코드로 살펴보려고 합니다. 많은 디자인 패턴이 적용되어 있고, 요청을 처리하는 프론트 컨트롤러이자 핵심 클래스인만큼 열심히 살펴보도록 하겠습니다.

아래의 내용은 SpringBoot 2.6.3를 기준으로 작성되었습니다.

 

 

 

 

1. DispatcherServlet(디스패처 서블릿) 동작 과정


[ DispatcherServlet(디스패처 서블릿) 동작 과정 ]

디스패처 서블릿은 모든 요청을 가장 먼저 받는 프론트 컨트롤러이다. 디스패처 서블릿의 동작 과정을 이해하기 위해서는 디스패처 서블릿을 알고 있어야 하는데, 관련 개념이 부족하면 이 글을 참고하도록 하자.

디스패처 서블릿을 이해하기 위해 가장 먼저 디스패처 서블릿의 계층 구조부터 살펴보도록 하자.

 

 

위의 여기서 우리가 주목해야 할 부분은 크게 다음과 같다.

  • HttpServlet
    • Http 서블릿을 구현하기 위한 J2EE 스펙의 추상 클래스
    • 특정 HTTP 메소드를 지원하기 위해서는 doX 메소드를 오버라이딩해야 함(템플릿 메소드 패턴)
    • doPatch는 지원하지 않음(아래에서 살펴볼 예정)
  • HttpServletBean
    • HttpServlet을 Spring이 구현한 추상 클래스
    • 스프링이 모든 유형의 서블릿 구현을 위해 정의한 공통 클래스
  • FrameworkServlet
    • 스프링 웹 프레임워크의 기반이 되는 서블릿
    • doX 메소드를 오버라이딩하고 있으며, doX 요청들을 공통된 요청 처리 메소드인 processRequest로 전달함
    • processRequest에서 실제 요청 핸들링은 추상 메소드 doService로 위임됨(템플릿 메소드 패턴)
  • DispatcherServlet
    • 컨트롤러로 요청을 전달하는 중앙 집중형 프론트 컨트롤러(서블릿 구현체)
    • 실제 요청을 처리하는 doService를 구현하고 있음

 

 

 

디스패처 서블릿이 요청을 받아서 컨트롤러로 위임하는 과정은 크게 다음과 같은데, 해당 과정들을 자세히 살펴보도록 하자.

  1. 서블리 요청/응답을 HTTP 서블릿 요청/응답으로 변환
  2. Http Method에 따른 처리 작업 진행
  3. 요청에 대한 공통 처리 작업 진행
  4. 컨트롤러로 요청을 위임
    1. 요청에 패핑되는 HandlerExecutionChain 조회
    2. 요청을 처리할 HandlerAdapter 조회
    3. HandlerAdapter를 통해 컨트롤러 메소드 호출(HandlerExecutionChain 처리)

 

 

 

 

1. 서블리 요청/응답을 HTTP 서블릿 요청/응답으로 변환

HTTP 요청은 등록된 필터들을 거쳐 디스패처 서블릿이 처리하게 되는데, 가장 먼저 요청을 받는 부분은 부모 클래스인 HttpServlet에 구현된 service 메소드이다.

public abstract class HttpServlet extends GenericServlet {

    ...

    @Override
    public void service(ServletRequest req, ServletResponse res)
        throws ServletException, IOException {

        HttpServletRequest  request;
        HttpServletResponse response;

        try {
            request = (HttpServletRequest) req;
            response = (HttpServletResponse) res;
        } catch (ClassCastException e) {
            throw new ServletException(lStrings.getString("http.non_http"));
        }
        service(request, response);
    }

    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
        ...
    }
    
    ...
}

 

 

service에서는 먼저 Servlet 관련 Request/Response 객체를 Http 관련 Request/Response로 캐스팅해준다. 캐스팅 시에 에러가 발생하면 HTTP 요청이 아니므로 에러를 던진다.

 

 

 

2. Http Method에 따른 처리 작업 진행

그리고 HttpServletRequest 객체를 파라미터로 갖는 service 메소드를 호출하는데, HttpServlet에도 service가 있지만 자식 클래스인 FrameworkServlet에 service가 오버라이딩 되어 있어 자식의 메소드가 호출된다. 해당 로직을 보면 다음과 같다.

public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {

    ...

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

	HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
	if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
	    processRequest(request, response);
	}
	else {
	    super.service(request, response);
	}
	
	...
    }
}

 

 

자식 클래스인 FrameworkServlet의 service는 PATCH 메소드인 경우 바로 processRequest를 호출하고, PATCH 메소드가 아니면 다시 부모 클래스인 HttpServlet의 service를 호출해주고 있다.

이상하게도 PATCH 메소드만 이러한 흐름을 거치는 이유는 PATCH 메소드의 탄생과 관련이 있다. 과거 HTTP 표준에는 PATCH 메소드가 존재하지 않았다. 그러다가 2010년도에 Ruby on Rails가 부분 수정의 필요를 주장하였고, 2010년도에 공식 HTTP 프로토콜로 추가되었다.

이러한 이유로 javax의 HttpServlet에는 doPatch 메소드가 존재하지 않아서 Spring 개발팀은 PATCH 요청을 처리하고자 자체적으로 개발한 FrameworkServlet를 거쳐 PATCH라면 핸들링하고, 그렇지 않은 경우에는 다시 자바 표준 기술인 부모 클래스 HttpServlet이 처리하도록 대응한 것이다. PATCH가 아닌 경우에 처리되는 로직은 다음과 같다.

public abstract class HttpServlet extends GenericServlet {

    ...

    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            ... // lastModifed에 따른 doGet 처리(요약함)
            doGet(req, resp);

        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);

        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);

        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);

        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);

        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);

        } else {
            ... // 에러 처리
        }
    }

    ...
}

 

 

HttpServlet에서는 요청 메소드에 따라 필요한 처리와 doX 메소드를 호출해주고 있다. 그러면 doX 메소드를 오버라이딩하고 있는 자식 클래스인 FrameworkServlet로 다시 요청이 이어지게 된다. PATCH를 제외한 메소드들에 대해 템플릿 메소드 패턴이 적용된 것이다.

오버라이딩된 doX 메소드는 다음과 같이 구현되어 있다.

public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {

    ... 

    @Override
    protected final void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        processRequest(request, response);
    }
    
    @Override
    protected final void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        processRequest(request, response);
    }

    ...
}

 

 

각각의 doX 메소드에서는 Http Method에 맞는 작업을 하는데, doGet에서 lastModifed 관련 처리를 해주는 것 말고는 거의 없다.

그리고 결국 모든 doX 메소드들은 processRequest를 거치게되는데, 해당 과정을 살펴보도록 하자.

 

 

 

3. 요청에 대한 공통 처리 작업 진행

오버라이딩된 doX 메소드는 모두 request를 처리하는 processRequest 메소드를 호출하고 있다.

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
		    throws ServletException, IOException {

    ... // LocaleContextHolder 처리 등 생략

    try {
        doService(request, response);
    } catch (ServletException | IOException ex) {
        failureCause = ex;
        throw ex;
    } catch (Throwable ex) {
        failureCause = ex;
        throw new NestedServletException("Request processing failed", ex);
    } finally {
        ... // 후처리 진행
    }
}

protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
        throws Exception;

 

 

processRequest에서는 request에 대한 공통 작업을 한 후에, 드디어 doService를 호출하고 있다. 앞서 설명한대로 doService는 템플릿 메소드 패턴이 적용된 추상 메소드이므로 자식 클래스인 DispatcherServlet에 가야 해당 코드를 볼 수 있다.

DispatcherServlet의 doService 코드는 다음과 같다.

public class DispatcherServlet extends FrameworkServlet {

    ...

    @Override
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        logRequest(request);

        ... // flash map 등의 처리 진행

        try {
            doDispatch(request, response);
        } finally {
            ... // 후처리 진행
        }
    }

    ...

}

 

 

processRequest와 doService에서는 LocaleContextHolder와 flashMap 등에 대한 공통 처리 작업이 진행되는데, 그렇게 중요하지 않다. 우리가 주목할 부분은 HTTP 요청을 컨트롤러로 위임해주는 doDispatch 이므로, 바로 해당 단계로 넘어가도록 하자.

 

 

 

4. 컨트롤러로 요청을 위임

여기서부터 이제 중요하다. 컨트롤러로 요청을 위임하는 doDispatch는 크게 다음의 3가지 단계로 나뉘어 진행된다.

  1. 요청에 패핑되는 HandlerExecutionChain 조회
  2. 요청을 처리할 HandlerAdapter 조회
  3. HandlerAdapter를 통해 컨트롤러 메소드 호출(HandlerExecutionChain 처리)

 

 doDipatch 코드는 다음과 같은데, 마찬가지로 각각의 단계에 대해 세부적으로 살펴보도록 하자.

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);

            // 1. 요청에 패핑되는 HandlerExecutionChain 조회
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // 2. 요청을 처리할 HandlerAdapter 조회
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            ...

            // 3. HandlerAdapter를 통해 컨트롤러 메소드 호출(HandlerExecutionChain 처리)
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            ... // 후처리 진행(인터셉터 등)
        } 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 {
        ... // 후처리 진행
    }
}

 

 

 

1. 요청에 패핑되는 HandlerExecutionChain 조회

doDispatch에서도 우리가 볼 부분은 그렇게 많지 않은데, 가장 먼저 볼 부분은 HandlerExecutionChain을 조회하는 부분이다.

HandlerExecutionChain는 HandlerMethod와 Interceptor들로 구성된다. HandlerMethod에는 매핑되는 컨트롤러의 메소드와 컨트롤러 빈 이름(또는 컨트롤러 빈) 및 빈 팩토리 등이 저장되어 있는데, 각각은 다음의 특징을 지닌다.

  • 매핑되는 컨트롤러의 메소드
    • Reflection 패키지의 Method 객체
    • Method를 호출(Invoke)하기 위해서는 메소드의 주인인 빈 객체를 필요로 함
  • 빈 정보(컨트롤러 빈 이름 또는 컨트롤러 빈)
    • Object 타입으로써 컨트롤러 빈 이름 또는 컨트롤러 빈가 될 수 있음
    • 기본적으로 컨트롤러 빈 이름을 갖고 있으며, 빈 객체 조회를 위해 사용됨
    • HandlerMethod의 createWithResolvedBean를 호출하면 빈 이름이 아닌 실제 빈을 갖는 HandlerMethod 객체를 생성함
  • 빈 팩토리
    • 스프링 부트가 관리하는 빈들을 가지고 있음
    • 컨트롤러의 메소드를 호출하기 위한 빈 객체 조회를 위해 사용됨

 

HandlerMethod에 저장된 Reflection 패키지의 Method를 호출(Invoke)하기 위해서는 메소드의 주인인 객체를 넘겨주어야 한다. 그런데 HandlerMethod는 기본적으로 컨트롤러 빈 이름을 갖고 있으므로, 빈 팩토리를 통해 컨트롤러 객체를 찾아서 Method를 호출 시에 넘겨주도록 빈 팩토리를 가지고 있는 것이다. 컨트롤러 빈 이름만 있는 HandlerMethod 객체의 createWithResolvedBean를 호출하면 빈 이름 대신 실제 빈을 갖는 HandlerMethod 객체가 반환된다.

 

디스패처 서블릿이 HandlerMethod가 아닌 HandlerExecutionChain를 얻는 이유는 공통적인 인터셉터에 대한 처리가 필요하기 때문이다. 이제 HandlerExecutionChain를 찾는 getHandler부터 봐보도록 하자. getHandler 코드는 다음과 같다.

@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;
}

 

 

getHandler에서는 HandlerMapping 목록을 순회하여 HanlerExecutionChain를 찾는다. HTTP 요청인 경우에는 Request 처리를 해주는 RequestMappingHandlerMapping가 HanlerExecutionChain를 찾아준다. RequestMappingHandlerMapping의 getHandler에서는 HTTP 요청을 처리할 수 있는 대상이 있으면 HandlerExecutionChain를 생성해 반환하고 없으면 null을 반환한다.

해당 과정을 자세히 살펴보기 위해 getHandlerInternal를 보도록 하자.

@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    Object handler = getHandlerInternal(request);
    if (handler == null) {
        handler = getDefaultHandler();
    }
    if (handler == null) {
        return null;
    }
    // Bean name or resolved handler?
    ...

    HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

    ... // CORS 및 기타 처리

    }

    return executionChain;
}

 

 

HandlerMethod를 찾는 RequestMappingHandlerMapping의 getHandlerInternal는 내부적으로 다시 추상 부모 클래스인 AbstractHandlerMethodMapping의 getHandlerInternal로 위임되는데, 해당 코드를 보면 다음과 같다.

@Override
@Nullable
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    String lookupPath = initLookupPath(request);
    this.mappingRegistry.acquireReadLock();
    try {
        HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
        return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
    } finally {
        this.mappingRegistry.releaseReadLock();
    }
}

 

 

가장 먼저 API URI인 lookupPath를 찾아주고, mappingRegistry에 대해 readLock을 얻은 다음에 HandlerMethod를 찾고 있다.

실제로 조회한 HandlerMethod의 예시를 보면 다음과 같은데, 빈 으름과 빈팩토리, 메소드 객체가 있는 것을 볼 수 있다.

 

 

이러한 HandlerMethod를 찾는 lookupHandlerMethod는 코드로 보지 말고 설명으로만 읽고 넘어가도록 하자.

스프링은 애플리케이션이 초기화 때 모든 컨트롤러를 파싱해서 (요청 정보, 요청 정보를 처리할 대상)을 관리해둔다. 그리고 요청이 들어오면 가장 먼저 URI를 기준으로 매핑되는 후보군들을 찾는다. 만약 동일한 URI에 대해 POST, PUT 메소드가 있으면 2개의 후보군이 찾아진다. 그리고 HTTP Method와 다른 조건들을 통해 완전히 매핑되는지를 검사한다.

매핑 정보는 다음과 같은 RequestMappingInfo 클래스인데, 관련된 자세한 내용은 다른 포스팅에서 살펴보도록 하자.

 

 

 

그러면 다시 이제 요청을 처리할 대상을 찾는 getHandler로 넘어오도록 하자.

@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    Object handler = getHandlerInternal(request);
    if (handler == null) {
        handler = getDefaultHandler();
    }
    if (handler == null) {
        return null;
    }
    // Bean name or resolved handler?
    ...

    HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

    ... // CORS 및 기타 처리

    }

    return executionChain;
}

 

 

위와 같은 과정을 거쳐 찾아진 HandlerMethod는 최종적으로 HandlerExecutionChain이 되어 반환된다. HandlerExecutionChain은 HandlerMethod를 통해 컨트롤러에 요청을 위임하기 전에 처리해야 하는 인터셉터 목록을 갖고 있다. 그래서 관련 정보를 갖고 있는 HandlerExecutionChain를 찾아서 반환해주는 것이다.

그럼 다시 디스패처 서블릿으로 넘어와서 이번에는 HandlerAdapter를 조회하는 로직을 보도록 하자.

 

 

 

2. 요청을 처리할 HandlerAdapter 조회

디스패처 서블릿은 HandlerExecutionChain을 직접 실행하지 않고, HandlerAdapter라는 HandlerAdapter는 어댑터 인터페이스를 통해 실행한다. (실제로 어댑터 패턴이 적용된 것은 아니다.) HandlerExecutionChain를 자체적으로 실행하지 않고 HandlerAdapter를 사용하는 이유는 요청을 처리하는 대상인 컨트롤러에 대해 공통적인 전/후처리를 하기 위함이다.

그러므로 디스패처 서블릿은 요청에 따라 적합한 HandlerAdapter를 찾고 HandlerAdapter를 통해 실제 컨트롤러의 메소드를 호출하는데, doDispatch에서는 getHandlerAdapter를 통해 HandlerAdapter를 조회한다.

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);

            // 1. 요청에 패핑되는 HandlerExecutionChain 조회
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // 2. 요청을 처리할 HandlerAdapter 조회
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            ...

            // 3. HandlerAdapter를 통해 컨트롤러 메소드 호출(HandlerExecutionChain 처리)
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            ... // 후처리 진행(인터셉터 등)
        } 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 {
        ... // 후처리 진행
    }
}

 

 

지금처럼 API 요청인 경우에는 URI에 매핑되는 컨트롤러 빈과 그 메소드 정보를 갖는 HandlerMethod를 호출하기 위한 RequestMappingHandlerAdapter가 찾아진다. 앞서 설명한대로 컨트롤러 메소드를 직접 호출하는 것이 아니라 어댑터를 사용하는 이유는 공통적인 전/후처리 과정이 필요하기 때문인데, 대표적으로 컨트롤러 메소드 호출 전에는 적합한 파라미터를 만들어 넣어주어야 하며(ArgumentResolver), 호출 후에는 메세지 컨버터를 통해 ResponseEntity의 Body를 찾아 Json 직렬화하는 등(ReturnValueHandler)이 필요하다.

이러한 이유로 스프링은 요청의 종류에 따라 적절한 HandlerAdapter를 구현해두었으며, HandlerAdapter를 통해 전처리를 하고 컨트롤러로 실제 요청을 위임한 다음에 후처리를 하도록 하였다.

 

 

 

3. HandlerAdapter를 통해 컨트롤러 메소드 호출(HandlerExecutionChain 처리)

적합한 HandlerAdapter가 HandlerExecutionChain를 모두 찾았으면 이제 핸들러 어댑터를 통해 요청을 처리할 차례이다. AbstractHandlerMethodAdapter를 보면 handle 메소드가 다음과 같이 구현되어 있다.

@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {

   return handleInternal(request, response, (HandlerMethod) handler);
}

@Nullable
   protected abstract ModelAndView handleInternal(HttpServletRequest request,
      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception;

 

 

요청의 종류에 따라 HandlerAdapter 구현체가 달라지고, 그에 따라 전/후처리가 달라지므로 세부 구현을 구체 클래스로 위임하는 템플릿 메소드 패턴이 또 사용된 것이다. 우리는 대표적으로 API 요청을 처리하는 RequestMappingHandlerAdapter를 기준으로 살펴보도록 하자.

RequestMappingHandlerAdapter의 handleInternal은 실제로 요청을 위임하는 invokeHandlerMethod를 호출한다.

@Override
protected ModelAndView handleInternal(HttpServletRequest request,
      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

   ModelAndView mav;
   checkRequest(request);

   // Execute invokeHandlerMethod in synchronized block if required.
   if (this.synchronizeOnSession) {
      ... // 생략
   } else {
      // No synchronization on session demanded at all...
      mav = invokeHandlerMethod(request, response, handlerMethod);
   }

   ...
      
   return mav;
}

 

 

여기서 InvokeHandlerMethod가 컨트롤러로 요청을 위임하는 곳이므로 해당 코드를 살펴보도록 하자.

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
      HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

   ServletWebRequest webRequest = new ServletWebRequest(request, response);
   try {
      WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
      ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

      ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
      if (this.argumentResolvers != null) {
         invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
      }
      if (this.returnValueHandlers != null) {
         invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
      }
      
      ...

      invocableMethod.invokeAndHandle(webRequest, mavContainer);
      
      ...

      return getModelAndView(mavContainer, modelFactory, webRequest);
   } finally {
      webRequest.requestCompleted();
   }
}

 

 

여기서 먼저 주목할 부분은 HandlerMethod가 ServletInvocableHandlerMethod로 재탄생한다는 것이다.

앞서 설명하였듯 HandlerAdapter를 사용하는 이유는 요청에 대한 공통적인 전/후처리를 하기 위함이라고 하였는데, 이러한 작업에는 대표적으로 컨트롤러의 파라미터를 처리하는 ArgumentResolver와 반환값을 처리하는 ReturnValueHandler가 있다.

즉, ServletInvocableHandlerMethod로 다시 만드는 이유는 HandlerMethod와 함께 argumentResolver나 returnValueHandlers 등을 추가해 공통 전/후 처리를 하기 위함이다. 세팅이 끝나면 ServletInvocableHandlerMethod의 invokeAndHandle로 이어진다.

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
      Object... providedArgs) throws Exception {

   Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
   setResponseStatus(webRequest);

   ... 

   try {
      this.returnValueHandlers.handleReturnValue(
            returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
   }
   catch (Exception ex) {
      ...
      throw ex;
   }
}

 

 

그리고는 바로 부모 클래스인 InvocableHandlerMethod의 invokeForRequest로 이어진다.

@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
      Object... providedArgs) throws Exception {

   Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
   if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
   }
   return doInvoke(args);
}

 

 

invokeForRequest에서는 먼저 메소드 호출을 위해 필요한 인자값을 처리한다. @RequestHeader, @CookieValue 및 @PathVariable 등도 모두 스프링이 만들어둔 ArgumentResolver에 의해 처리가 되는데, 이러한 인자값을 만드는 작업이 getMethodArgumentValues 내에서 처리가 된다. 그리고 doInvoke에서 만들어진 인자값을 통해 컨트롤러의 메소드를 호출한다.

(getMethodArgumentValues에서 ArgumentResolver를 이용해 인자값을 처리하는 과정에는 컴포지트 패턴이 적용되어 있는데, 후처리하는 과정에서도 동일하게 사용되므로 이따가 살펴보도록 하자.)

 

doInvoke는 부모 클래스인 InvocableHandlerMethod에 다음과 같이 구현되어 있다.

@Nullable
protected Object doInvoke(Object... args) throws Exception {
   Method method = getBridgedMethod();
   try {
      if (KotlinDetector.isSuspendingFunction(method)) {
         return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
      }
      return method.invoke(getBean(), args);
   } catch (IllegalArgumentException ex) {
      ... // IllegalArgumentException 처리
   } catch (InvocationTargetException ex) {
      ... // Unwrap for HandlerExceptionResolvers ...
   }
}

 

 

가장 먼저 요청을 처리할 컨트롤러의 메소드 객체(Java의 리플렉션 Method)를 꺼내온다. 그리고 Method 객체의 invoke를 통해서(Reflection을 사용해서) 실제 컨트롤러로 위임을 해준다.

컨트롤러에서 성공적으로 작업을 처리한 후에 ResponseEntity를 반환했다면 invokeAndHandle의 returnValue로 해당 객체가 온다.

private HandlerMethodReturnValueHandlerComposite returnValueHandlers;

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {

    Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);

    ...

    try {
        this.returnValueHandlers.handleReturnValue(
            returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
        } catch (Exception ex) {
            ...
        }
    }
}

 

 

그 다음에는 응답에 대한 후처리를 할 차례인데, 후처리는 returnValueHandlers를 통해 처리된다.

ArgumentResolver로 요청을 전처리하는 과정과 ReturnValueHandler로 후처리하는 과정 모두에는 컴포지트 패턴이 적용되어 있다. 요청을 처리하기 위한 인터페이스로 HandlerMethodArgumentResolver가 있다면, 응답을 처리하기 위한 인터페이스로는 HandlerMethodReturnValueHandler가 있다.

public interface HandlerMethodReturnValueHandler {

boolean supportsReturnType(MethodParameter returnType);

void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
         ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;

}

 

 

응답에 따라 다양한 형태로 처리하기 위해서 이를 리스트로 갖고 있으며 supportsReturnType으로 처리 가능한 구현체인지를 판별해야 한다. 스프링은 HandlerMethodReturnValueHandler 인터페이스 목록을 갖고 있는 컴포지트 객체인 HandlerMethodReturnValueHandlerComposite를 만들어두고 HandlerMethodReturnValueHandler를 구현받도록 하여 컴포지트 패턴을 적용하고 있다.

public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {

   private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<>();

   @Override
   public boolean supportsReturnType(MethodParameter returnType) {
      return getReturnValueHandler(returnType) != null;
   }

   @Nullable
   private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) {
      for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
         if (handler.supportsReturnType(returnType)) {
            return handler;
         }
      }
      return null;
   }

   @Override
   public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
         ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

      HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
      if (handler == null) {
         throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
      }
      handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
   }

   ...

}

 

 

오버라이딩된 supportsReturnType 메소드의 경우에는 리스트를 순회하여 처리 가능한 핸들러가 있을 경우에 true를 반환하게 하였으며, handleReturnValue의 경우에는 처리 가능한 핸들러를 찾아서 해당 핸들러의 handleReturnValue 호출을 해주고 있다. 아주 적절하게 컴포지트 패턴을 적용해서 문제를 해결함을 확인할 수 있다.

ResponseEntity 객체를 반환한 경우에는 컴포지트 객체가 갖는 HandlerMethodReturnValueHandler 구현체 중에서 HttpEntityMethodProcessor가 사용된다. HttpEntityMethodProcessor 내부에서는 Response를 set해주고, 응답 가능한 MediaType인지 검사한 후에 적절한 MessageConverter를 선택해 응답을 처리하고 결과를 반환한다.

 

 

 

 

2. DispatcherServlet(디스패처 서블릿)의 초기화 과정


마지막으로 디스패처 서블릿의 초기화 과정 중 일부만 살펴보도록 하자.

 

[ DispatcherServlet(디스패처 서블릿)의 초기화 과정 ]

앞서 설명하였듯 디스패처 서블릿은 J2EE 스펙의 HttpServlet 클래스를 확장한 서블릿 기반의 기술이다. 그러므로 디스패처 서블릿 역시 일반적인 서블릿의 라이프사이클을 따르게 되는데 서블릿의 라이프사이클은 다음과 같다.

 

  • 초기화: 요청이 들어오면 서블릿이 웹 컨테이너에 등록되어 있는지 확인하고, 없으면 초기화를 진행함 
  • 요청 처리: 요청이 들어오면 각각의 HTTP 메소드에 맞게 요청을 처리함 
  • 소멸: 웹 컨테이너가 서블릿에 종료 요청을 하여 종료 시에 처리해야하는 작업들을 처리함

 

 

클라이언트로부터 요청이 오면 웹 컨테이너는 먼저 서블릿이 초기화 되었는지를 확인하고, 초기화되지 않았다면 init() 메소드를 호출해 초기화를 진행한다. init() 메소드는 첫 요청이 왔을 때 한번만 실행되기 때문에 서블릿의 쓰레드에서 공통적으로 필요로 하는 작업들이 진행된다. 그 작업들 중에는 디스패처 서블릿이 컨트롤러로 요청을 위임하고 받은 결과를 처리하기 위한 도구들을 준비하는 과정이 있다. 디스패처 서블릿은 요청을 처리하기 위해 다음과 같은 도구들을 필요로 한다.

  • Multipart 파일 업로드를 위한 MultipartResolver
  • Locale을 결정하기 위한 LocaleResolver
  • 요청을 처리할 컨트롤러를 찾기 위한 HandlerMapping
  • 요청을 컨트롤러로 위임하기 위한 HandlerAdapter
  • 뷰를 반환하기 위한 ViewResolver
  • 기타 등등

 

 

스프링은 Lazy-Init 전략을 사용해 애플리케이션을 빠르게 구동하도록 하고 있어서, 첫 요청이 와서 서블릿 초기화가 진행될 때 애플리케이션 컨텍스트로부터 해당 빈을 찾아서 설정(Set)해준다. 그리고 이는 스프링의 첫 요청을 느리게 만드는 원인이다. 디스패처 서블릿의 초기화 화 로직은 다음과 같이 구현되어 있다. (부모 클래스 부분은 생략한 것이다.)

@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

 

 

위의 코드에서 도구들을 초기화하는 메소드 이름이 initStrategies인 이유는 전략 패턴이 적용되었기 때문이다. 대표적으로 뷰를 반환하기 위한 도구인 ViewResolver를 중심으로 살펴보도록 하자. ViewResolver에는 전략 패턴이 적용되었으므로 인터페이스이다. ViewResolver 외에 다른 모든 도구들도 전략 패턴을 적용하므로 인터페이스를 갖고 있고, 인터페이스 타입으로 선언되어 있다.

public interface ViewResolver {

    @Nullable
    View resolveViewName(String viewName, Locale locale) throws Exception;

}

 

 

스프링은 기본적으로 ContentNegotiatingViewResolver, BeanNameViewResolver, InternalResourceViewResolver를 빈으로 등록해둔다. 그리고 개발자가 추가한 Thymeleaf나 Mustache와 같은 템플릿 엔진을 위한 ViewResolver도 추가될 수 있다. (스프링 부트에서는 해당 의존성을 추가하면 자동으로 등록된다.) 결국 실제로 어떤 구현체가 사용될지는 애플리케이션 실행 후에야 알 수 있다. 그래서 스프링은 유연하게 도구들을 사용할 수 있도록 전략 패턴을 적용하였으며 여러 ViewResolver가 동작 가능하도록 List로 ViewResolver를 가지고 있다.

 

그런데 스프링은 ViewResolver에 추가적으로 컴포지트 클래스인 ViewResolverComposite를 만들어 컴포지트 패턴까지 적용하고 있는데, 그 이유는 WebMvcConfigurer와 관련이 있다. 스프링에서는 인터셉터나 CORS 처리 등 웹 기능을 확장하기 위해서 WebMvcConfigurer 인터페이스를 구현한 설정 클래스를 만들어준다. 그리고 여기서도 ViewResolver를 직접 등록해줄 수 있는데, 여기서 등록된 빈들은 CompositeViewResolver에 등록이 된다. 예를 들어 다음과 같은 설정 클래스를 추가했다고 하자.

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.viewResolver(new MyViewResolver());
    }

    static class MyViewResolver implements ViewResolver {
        @Override
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            return null;
        }
    }

    @Bean
    public MangKyuViewResolver mangKyuViewResolver() {
        return new MangKyuViewResolver();
    }

    static class MangKyuViewResolver implements ViewResolver {
        @Override
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            return null;
        }
    }
}

 

 

MyViewResolver는 WebMvcConfigurer를 통해 등록되었으므로 ViewResolverComposite에 추가가 되고, MangKyuViewResolver는 직접 빈을 등록해준 것이므로 List 안에 등록이 된다. 최종적으로 디스패처 서블릿의 List<ViewResolver>는 다음과 같이 구성 된다.

  • ContentNegotiatingViewResolver
  • BeanNameViewResolver
  • MangKyuViewResolver
  • ViewResolverComposite
    • MyViewResolver
  • InternalResourceViewResolver

 

 

첫 요청이 느린 문제를 해결하는 방법은 스프링 애플리케이션이 실행된 후에 아무런 API를 호출해 서블릿 초기화를 시키면 된다. 존재하지 않는 URI라 할지라도 디스패처 서블릿은 첫 요청을 받아들이기 위해 초기화 과정을 진행한다.

개발자라면 애플리케이션이 실행된 후에 1회 초기화 과정을 자동화하기를 원할 수 있는데 관련 내용은 이 포스팅에서 참고하도록 하자.

 

 

 

 

 

 

위의 내용들은 개인적으로 공부를 하면서 작성을 한 내용이라 충분히 틀리거나 잘못된 내용들이 있을 수 있습니다. 혹시 수정 또는 추가할 내용들을 발견하셨다면 댓글 남겨주세요! 반영해서 수정하도록 하겠습니다:)

 

 

 

 

 

 

 

관련 포스팅

  1. SpringBoot 소스 코드 분석하기, SpringBoot의 장점과 특징 - (1)
  2. SpringBoot 소스 코드 분석하기, 애플리케이션 컨텍스트(Application Context)와 빈팩토리(BeanFactory) - (2)
  3. SpringBoot 소스 코드 분석하기, @SpringBootApplication 어노테이션의 속성과 구성요소 - (3)
  4. SpringBoot 소스 코드 분석하기, SpringBootApplication의 생성과 초기화 과정  - (4)
  5. SpringBoot 소스 코드 분석하기, SpringBootApplication의 run 메소드와 실행과정  - (5)
  6. SpringBoot 소스 코드 분석하기, 애플리케이션 컨텍스트(ApplicationContext)의 refreshContext 동작 과정 - (6)
  7. SpringBoot 소스 코드 분석하기, DispatcherServlet(디스패처 서블릿) 동작 과정 - (7)

 

 

 

반응형
댓글
댓글쓰기 폼
  • Skills 좋은 글 감사합니다!
    코드 부분 테마(?)가 너무 예뻐서 그런데 혹시 어떤거 사용중인지 알 수 있을까요?
    2022.03.03 18:16 신고
  • 망나니개발자 오 코드부분 괜찮나요...?? 너무 진한 것 같아서 최근에도 다른 밝은 계열로 옮겨야하나 고민되네요ㅜㅜ
    참고로 위에 적용한건 rainbow 테마입니다ㅎㅎ
    2022.03.03 20:53 신고
  • Skills 제가 보기엔 이쁜 것 같습니다 ㅋㅋ!!
    저도 tistory 블로그를 사용중인데 플러그인에 rainbow 테마가 없네요 ㅠㅠ
    플러그인으로 사용중이신게 아니신가요?
    괜찮으시다면 rainbow 테마 적용하는 방법을 알고싶습니다!!
    답변 감사합니다 좋은 하루 되세요~!!
    2022.03.04 14:34 신고
  • 망나니개발자 말씀해주신대로 티스토리에서 제공되는 것으로는 사용이 불가능하고, 별도로 highlight.js를 스킨 편집에서 넣어주셔야 합니다!
    https://highlightjs.org/
    2022.03.04 19:23 신고
반응형
공지사항
Total
2,673,127
Today
2,902
Yesterday
5,427
TAG
more
«   2022/07   »
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            
글 보관함