Spring

코드로 살펴보는 DispatcherServlet과 Aware (1)

천방지축 개발노트 2024. 9. 26. 00:52
DispatcherServlet의 역할과 계층 구조

클라이언트로부터 웹 요청이 들어왔을 때 WAS는 Spring Container가 가진 Object한테 요청에 대한 처리를 위임한다. 그리고 Spring에서 이 역할을 하는 것이 정확히는 DispatcherServlet인데, 위 행동을 하기 위해 어떤 Object가 어떤 요청 처리를 담당하고 있는지 알 수 있는 '매핑 정보'를 필요로 한다.

즉 Spring 애플리케이션이 실행되면 DispatcherServlet이 빈으로 등록될 때 매핑 정보를 주입해 줘야 하는데, 어떻게 이게 가능한 걸까? 상속과 구현 계층 구조를 확인해 보자.

DispatcherServlet 계층 구조
펼쳐보는 DispatcherServlet의 상속/구현 계층 구조

 

Aware로 시작하는 DispatcherServlet

내부 소스를 보면 가장 안쪽에 Aware이라는 마커 인터페이스(Marker Interface)가 있다. 일반적으로 마커 인터페이스 자체는 비어있기에 아무런 기능도 하지 않는 껍데기이지만, 이를 확장하여 특정 기능을 수행하는 다양한 하위 인터페이스가 존재하고 이를 사용하도록 설계되어 있다.

Aware를 구현하는 다양한 인터페이스들
Aware를 구현하는 다양한 인터페이스들

그리고 Spring에서 Aware라는 이름이 postfix하게 붙은 인터페이스들은 컨테이너가 자동으로 특정한 정보를 주입하도록 약속되어 있다. 다시 말해, Aware 인터페이스 자체와 그와 함께 제공되는 다른 인터페이스들은 "이 객체가 특정 객체를 필요로 한다"라는 일종의 신호인 것이다. 이 중에서 DispatcherServlet은 ApplicationContextAware 인터페이스를 구현하고 있다.

ApplicationContextAware를 구현하는 DispatcherServlet
ApplicationContextAware를 구현하여 사용하는 DispatcherServlet

ApplicationContextAware를 구현한 클래스는 setApplicationContext라는 라이프사이클 메소드를 구현하도록 되어있는데, 클래스가 Spring에 빈으로 등록될 때 Container가 해당 메소드를 호출하면서 자기 자신인 현재의 ApplicationContext를 주입한다는 것을 코드와 주석으로 확인할 수 있다. 그리고 궁극적으로 DispatcherServlet이 애플리케이션 컨텍스트를 주입받는 이유는 Spring MVC의 핵심 구성요소인 HandlerMappingHandlerAdapter라는 빈 등을 사용하기 위해서이다.

Spring MVC에서 HTTP 요청을 처리하는 핵심 메소드인 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.
 */
@SuppressWarnings("deprecation")
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request; // 실제로 처리할 HTTP 요청을 저장
    HandlerExecutionChain mappedHandler = null; // 요청을 처리할 handler를 저장하는 변수
    boolean multipartRequestParsed = false; // multipart 요청 여부 확인
    // 비동기 처리를 위한 객체
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

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

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

            // 현재 request를 처리할 수 있는 적합한 Controller를 검색. 없으면 404.
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // 현재 request를 처리할 수 있는 handlerAdapter를 검색
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
            ...
            // handler 호출 전 필요한 작업 수행
            if (!mappedHandler.applyPreHandle(processedRequest, response))
                return;
            // 실제로 핸들러를 호출(실행)
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
            // 비동기 처리가 시작됐는지 확인
            if (asyncManager.isConcurrentHandlingStarted())
                return;
            ...
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        ...
        // 결과를 클라이언트에게 전달
        processDispatchResult(processedRequest, response, ....);
    ...
}

DispatcherServlet은 HandlerMapping을 통해 HTTP 요청에 맞는 Controller(Handler)를 찾고, HandlerAdapter를 사용해 해당 Handler를 실행한다. 즉, 웹 요청 처리를 위해 필요한 빈들을 애플리케이션 컨텍스트를 통해 전달받아 이를 참고해서 웹 요청과 응답 처리의 흐름을 제어하는 것이다.

추가적으로 (특정 애노테이션을 추가하지 않은 일반적인 경우에) Controller에서 String을 반환하면, Spring MVC는 해당 값은 View 이름으로 간주한다. 따라서 마지막 processDispatchResult() 메소드 동작시 내부적으로 뷰 리졸버(ViewResolver)에 의해 JSP와 같은 View 파일을 찾아 결과를 렌더링한다.

 


다음 글에서는 Aware와 함께 상위에 위치하고 있는

또 다른 인터페이스인 Servlet에 대해 얘기해보겠습니다.💬