ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring MVC Internals (내부구조)
    Java 2021. 2. 12. 23:39
    반응형

     

    Spring MVC는 Spring Framework의 일부로 Spring Web Layer에서 MVC(Model-View-Controller)를 구현한 Web-Servlet 모듈입니다.

     

    이번 글에서는 클라이언트 요청과 응답 시 Spring MVC가 어떻게 동작하는지를 중점으로 그 구조를 살펴보겠습니다.

     

    • Servlet: Java Web 애플리케이션의 기반
    • DispatcherServlet: SpringMVC의 중심
    • HTTP 요청 처리 과정
    • 요청 처리(handle)
    • Handler 메소드의 아규먼트와 리턴값 처리
    • 뷰 렌더링

    아래에서 SpringWeb MVC가 어떠한 방식으로 동작하는지 이해하기 위해서, 아래와 같이 간단한 hello() 메소드가 @Controller로 annotated된 클래스에 존재한다고 가정하겠습니다 (웹 및 애플리케이션 서버 - Tomcat으로 가정):

    import org.springframework.web.bind.annotation.GetMapping;
    
    @GetMapping("/")
    public String hello() {
        return "login";
    }

     

    Servlet: Java Web 애플리케이션의 기반

     

    브라우저에 http://localhost:8080/를 넣고 엔터를 치면 해당 요청은 웹서버에 보내지고 어떤 일이 발생하는 걸까요?

     

    톰캣은 서블릿 컨테이너(Servlet Container)이기 때문에, 톰캣 웹서버에 전달되는 모든 HTTP 요청은 자바 서블릿에 의해 처리됩니다. 그렇기에 SpringWeb 애플리케이션의 엔트리포인트 역시 하나의 서블릿입니다.

     

    Tomcat Architecture - Image from Author inspired by [1]

     

    서블릿은 자바 웹 애플리케이션의 핵심 컴포넌트로, low-level이며 MVC와 같은 특정 프로그래밍 패턴에 대한 제약을 가하지 않습니다. HTTP 서블릿은 단 하나의 HTTTP 요청을 받을 수 있고 그것을 처리한 후에 응답을 돌려줍니다. 

     

    DispatcherServlet: SpringMVC의 중심

    웹 애플리케이션을 개발하는 개발자의 입장에서 원하는 부분은 아래와 같은 지루하고 기본적인 작업을 추상화하여 단순하게 처리하고 유용한 비지니스 로직 구현에 집중하는 것입니다:

     

    • HTTP 요청을 하나의 특정 프로세싱 방법에 매핑하는 것
    • HTTP 요청과 헤더(headers)를 데이터 전송 객체(data transfer objects, DTOs)나 도메인 객체로 파싱하는 것
    • model-view-controller 간의 연동
    • DTOs나 도메인 객체로부터의 응답 생성

    Request & Response on Servlet - Spring Container - Image from Author inspired by [2]

    Spring의 DispatcherServlet은 정확히 위와 같은 기능들을 제공합니다. SpringWebMVC 프레임웍의 중심에 위치하여, 애플리케이션에 전달되는 모든 요청들을 받습니다. 

     

    DispatcherServlet은 많은 부분 확장 가능하기에 아래와 같은 다양한 tasks를 위해 기존 또는 새로운 애댑터를 플러그하여 사용할 수 있게 합니다:

     

    • 해당 요청을 처리해야하는 클래스나 메소드에 요청을 매핑 (HandlerMapping interface의 구현)
    • 기본 서블릿, 복잡한 MVC 워크플로우, POJO bean 형태의 메소드 등 특정 패턴을 사용하여 하나의 요청을 처리 (HandlerAdapter interface의 구현)
    • 다양한 템플릿 엔진(XML, XSLT 등의 view 기술)을 사용하여 이름을 통해 view를 resolve (ViewResolver interface의 구현)
    • 디폴트 Apache Commons 파일 업로딩 또는 직접 작성한 MultipartResolver를 사용하여 multiparts 요청을 파싱
    • 쿠키, 세션, Accept HTTP 헤더, 또는 다른 방식을 통해 사용자가 예상하는 locale을 LocaleResolver를 통해 resolve

     

    HTTP 요청 처리 과정

    먼저, 간단한 HTTP 요청을 컨트롤러 레이어의 메소드로 보내고, 다시 브라우저/클라이언트에게 보내는 처리를 살펴보겠습니다.

     

    DispatcherServlet은 매우 긴 상속 구조를 가지고 있습니다:

     

    Image from javadoc

     

    HTTP 요청을 처리하는 데에 있어서, 위 구조의 각 요소의 역할은 아래와 같습니다.

     

    GenericServlet

    GenericServelt은 Servlet Specification의 일부로 직접 HTTP에 한정되지 않습니다(ServletRequest 및 ServletResponse가 HTTP에 한정되지 않음). 들어오는 요청을 받고 응답을 처리하는 service()라는메소드를 정의합니다. 

    public abstract class GenericServlet implements Servlet, ServletConfig,
            java.io.Serializable {
            
        // ...
        
        @Override
        public abstract void service(ServletRequest req, ServletResponse res)
                throws ServletException, IOException;
                
        // ...
        
    }

     

    이 메소드는 서버에 대한 매 요청마다 실행됩니다.

     

    HttpServlet

    HttpServlet은 이름에서 알 수 있듯이, HTTP 처리 서블릿의 구현물로 역시 Servlet Specification에 의해 정의됩니다. 

     

    좀 더 구체적으로 살펴보면, HttpServlet은 HTTP 메소드 타입에 따라 요청을 구분하는 service() 메소드 구현물을 가진 추상클래스(abstract)입니다:

     

    public abstract class HttpServlet extends GenericServlet {
        // ...
        
        @Override
        public void service(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
            
            String method = req.getMethod();
    
            if (method.equals(METHOD_GET)) {
            	
                // ...
                
                doGet(req, resp);
                
                // ...
                
            } else if (method.equals(METHOD_POST)) {
                doPost(req, resp);
       	        // ...         
            }
        }
    }

     

    HttpServletBean

    HttpServletBean은 위 위계구조에서 처음 Spring을 인식하는 클래스로 web.xml 또는 WebApplicationInitializer로부터 받은 서블릿 init-param 값들을 사용하여 bean 프로퍼티에 삽입합니다. 

     

    public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
    
        // ...
        
        @Override
        public final void init() throws ServletException {
            
            // Set bean properties from init parameters.
            PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
            
            if (!pvs.isEmpty()) {
                try {
                    BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                    
                    // ...
       
                    bw.setPropertyValues(pvs, true);   
                }
                // ...
            }
            
            // Let subclasses do whatever initialization they like.
            initServletBean();
        }
        
        // ...
    }

     

    FrameworkServlet

    FrameworkServlet은 ApplicationContextAware 인터페이스를 implements하여 서블릿 기능과 웹 애플리케이션 컨텍스트를 연결합니다. 하지만 직접 웹 애플리케이션 컨텍스트를 생성할 수도 있습니다.

     

    위에서 언급된 것과 같이, HttpServletBean superclass는 bean 프로퍼티로 init-params을 삽입하며, context class 이름은 서블릿의 contextClass init-param을 통해 설정되고, 이 클래스의 인스턴스가 애플리케이션 컨텍스트로 생성되게 됩니다. 따로 명시되지 않으면 디폴트인 XmlWebApplicationContext 클래스가 사용되고, SpringBoot에서는 디폴트로 AnnotationConfigWebApplicationContext 클래스를 사용합니다. 

     

    doGet, doPost 등 HttpServlet.service() 구현에서 route된 요청들을 실행하는 메소드는 FrameworkServlet 내에서 processRequest를 실행합니다. 그리고 processRequest 메소드는 doService를 호출하며, doService() 메소드는 FramewokrServlet에서는 추상메소드로 존재하며 서브 클래스가 그 메소드를 구현하게 됩니다. 

    public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
        // ...
        
        @Override
        protected final void doGet(HttpServletRequest request, HttpServletResponse response)
    	        throws ServletException, IOException {
    
            processRequest(request, response);
        }
        
        // ...
        
        protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            // ...
            
            try {
                doService(request, response);
            }
            
            // ...
            
        }
        
        // ...
        
        protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
                throws Exception;
        // ...
    }

     

    DispatcherServlet

    DispatcherServlet은 FrameworkServlet을 상속하여 Spring MVC가 사용자에게 제공하는 최종 형태의 API를 이룹니다.

     

    doService() - 요청 처리를 일원화, 요청에 추가 정보를 더함

    위의 FrameworkServlet에서 추상메소드였던 doService()를 구현하여, doGet, doPost 등의 다양한 타입의 요청을 doService 메소드로 일원화하여 처리합니다. 

     

    public class DispatcherServlet extends FrameworkServlet {
        // ...
        
        @Override
        protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
            // ...
            // Make framework objects available to handlers and view objects.
            request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
            request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
            request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
            request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
    
            // ...
            
            try {
                doDispatch(request, response);
            }
            
            // ...
        }
    }
       

     

     

    DispatcherServlet은 위(코드 상의 request.setAttribute 류)와 같이, doService() 메소드 이후의 프로세싱 파이프라인 상에서 유용하게 사용하는 객체들을 더해줍니다.  

     

    또한 doService() 메소드 내에서 아래와 같이 input 및 output Flash Map Manager(output의 경우 Flash Map)를 준비해줍니다. 이러한 Flash Map은 기본적으로 한 요청에서 연속되는 다른 하나의 요청으로 인자를 넘겨주는 패턴으로 리다이렉트와 같은 경우 매우 유용합니다.

     

    if (this.flashMapManager != null) {
        FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
        if (inputFlashMap != null) {
            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
        }
        request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
        request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
    }

    그리고, doDispatch() 메소드를 실행하여 요청 dispatching을 진행합니다.

     

    요청을 dispatching

    doDispatch() 메소드의 주요 목적은 요청에 알맞는 handler를 찾아 request/response 인자를 넘겨주는 것입니다. 그러한 handler는 기본적으로 객체이며 특정 인터페이스 제한이 되지 않습니다. 이러한 사실은 Spring은 이 handler와 연동되기 위해서 이 handler와 어떻게 연동하면 되는지 알고 있는 어댑터를 찾아야 한다는 점을 알려줍니다. 

     

    요청에 알맞는 handler를 찾기 위해서, Spring은 HandlerMapping 인터페이스를 구현하고 있는 구현체를 사용합니다. 그러한 구현체는 다양한 목적에 따라 여러가지가 존재합니다.

     

    SimpleUrlHandlerMapping은 하나의 요청을 요청의 URL을 확인하여 특정 프로세싱 bean에 연결해 줍니다. 예로, java.util.Properties 인스턴스를 통해 아래와 같은 매핑을 삽입하여 설정할 수 있습니다:

     

    /welcome.html=ticketController
    /show.html=ticketController

     

    아마도 가장 많이 사용되는 handler 매핑은 RequestMappingHandlerMapping으로 하나의 요청을 @Controller 클래스의 @RequestMapping으로 annotated된 메소드에 연결시켜 줍니다. 맨 처음에서 본 hello() 메소드가 바로 이것입니다.

     

    @GetMapping, @PostMapping 등의 어노테이션은 동일한 메타-어노테이션인 @RequestMapping에서 기반합니다.

     

    이 doDispatch() 메소드는 다른 HTTP-특정 tasks도 처리해줍니다:

     

    • 리소스가 변경되지 않은 경우 GET 요청에 대한 short-circuiting
    • 상응하는 요청에 대한 multipart resolver 적용
    • 만약 handler가 비동기로 처리하려하는 경우 요청에 대한 short-circuiting

     

    요청 처리

    Spring이 요청에 대한 handler와 그 handler에 대한 어댑터를 결정했다면, 마침내 그 요청을 처리합니다. HandlerAdapter.handle() 메소드는 아래와 같은 메소드 구조를 가집니다:

     

    public interface HandlerAdapter {
        // ...
        
        @Nullable
        ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
        // ...   
    }

    위 코드 상에서 알 수 있듯이 handler는 다음과 같은 형태로 요청을 처리할 수 있습니다:

     

    • 스스로 응답(response) 객체에 데이터를 쓰고 null을 리턴
    • DispatcherServlet을 통해 렌더링 되는 ModelAndView 객체를 리턴

    handler에는 다양한 타입이 존재하며, 아래에는 그 중 하나인 SimpleControllerHandlerAdapter의  handler 메소드가 Spring MVC 컨트롤러 인스턴스를 어떻게 처리하는지 알 수 있습니다. 

     

    public class SimpleControllerHandlerAdapter implements HandlerAdapter {
        // ...
        
        @Override
        @Nullable
        public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
                throws Exception {
    
            return ((Controller) handler).handleRequest(request, response);
        }
        // ...
    }

     

    두 번째 예는 SimpleServletHandlerAdapter로 일반적인 Servlet을 요청 handler로 adapt합니다:

    public class SimpleServletHandlerAdapter implements HandlerAdapter {
        // ...
        
        @Override
        @Nullable
        public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
                throws Exception {
    
            ((Servlet) handler).service(request, response);
            return null;
    	}
        // ...
    }

    서블릿은 ModelAndView에 대한 어떤 것도 알지 못하기 때문에 단순히 요청 자체만을 처리하고 응답 객체에 그 결과를 넣어줍니다. 그러므로 위의 메소드는 ModelAndView 대신 null을 리턴합니다.

     

    맨 처음의 샘플에서, 컨트롤러는 @RequestMapping 어노테이션이 추가된 POJO로 handler는 HandlerMethod 인스턴스로 wrapped된 해당 클래스의 메소드입니다. 이러한 handler 타입을 adapt하기 위해서, Spring은 RequestMappingHandlerAdapter 클래스를 사용합니다.

     

    Handler 메소드의 아규먼트와 리턴값 처리

    컨트롤러 메소드는 주로 도메인 객체, path 파라미터 등의 다양한 데이터를 받고 리턴하며 HttpServletRequest와 HttpServletResponse 아규먼트를 사용하는 일은 드뭅니다.

     

    또한, 컨트롤러 메소드에서 ModelAndView 인스턴스를 리턴하기보다는 뷰이름이나, ResponseEntity 또는 json 응답으로 변형되는 POJO을 주로 리턴합니다. 

     

    RequestMappingHandlerAdapter는 메소드 아규먼트가 HttpServletRequest로부터 resolve되도록 하며, 메소드 리턴값으로부터 ModelAndView 객체를 생성합니다. 이와 같은 변형은 아래와 같은 RequestMappingHandlerAdapter 코드에 잘 나타나 있습니다:

     

    public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
    		implements BeanFactoryAware, InitializingBean {
        // ...
        
        @Nullable
        protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
                HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
            try {
                // ...
                
                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);
            }
            // ...
        }
    }

     

    위에서 argumentResolvers 객체는 다양한 HandlerMethodArgumentResolver 인스턴스로 이루어져 있습니다. 이러한 Argument Resolver의 종류는 30가지가 넘는데, 요청의 여러 형태의 정보를 추출하여 메소드 아규먼트로 넘겨주는 기능을 합니다. 예로, URL path 변수, 요청 body 파라미터, 요청 헤더, 쿠키, 세션 데이터 등이 존재합니다.

     

    returnValueHandlers 객체는 다양한 HandlerMethodReturnValueHandler로 이루어져 있습니다. Adapter가 원하는 ModelAndView 객체를 생성하기 위해 이러한 다양한 ValueHandler가 다양한 값들을 ModelAndView 객체로 만들어 줍니다. 예로, 샘플의 hello() 메소드가 String "login"을 리턴하면 ViewNameMethodReturnValueHandler가 그 값을 처리합니다. 

     

    뷰 렌더링

    이제 Spring은 HTTP 요청을 처리하고 ModelAndView 객체를 받아서 사용자가 브라우저를 통해 볼 HTML 페이지를 렌더링해야 합니다. 렌더링은 받은 ModelAndView 객체 안에 포함된 선택된 view를 통해 진행됩니다. 

     

    HTTP 프로토콜을 통해 전달될 수 있는 데이터 형태(JSON 객체, XML 등)는 모두 렌더링될 수 있습니다 (뒤의 REST 관련 섹션).

     

    DispatcherServlet의 render() 메소드는 처음에 LocaleResolver 인스턴스를 사용하여 응답  locale을 설정합니다. 

     

    렌더링이 진행되는 동안, ModelAndView 객체는 직접 선택된 뷰의 레퍼런스를 가지고 있거나, 단순히 뷰이름을 가지고 있거나 또는 디폴트 뷰에 의존하기에 아무것도 없을 수 있습니다.

     

    샘플의 hello() 메소드는 원하는 뷰를 String 형태의 뷰이름으로 명시하였기에 viewResolvers 리스트를 통해 순회하여 resolveViewName 메소드를 통해 뷰이름으로 뷰를 찾아 리턴합니다:

     

    public class DispatcherServlet extends FrameworkServlet {
        // ...
        
        protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
            // Determine locale for request and apply it to the response.
            Locale locale =
                    (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
            response.setLocale(locale);
    
            View view;
            String viewName = mv.getViewName();
            if (viewName != null) {
                // We need to resolve the view name.
                view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
                // ...
            }
            // ...
            
            try {
                if (mv.getStatus() != null) {
                    response.setStatus(mv.getStatus().value());
                }
                view.render(mv.getModelInternal(), request, response);
            }
            // ...
        }
        // ...
        
        @Nullable
        protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
                Locale locale, HttpServletRequest request) throws Exception {
    
            if (this.viewResolvers != null) {
                for (ViewResolver viewResolver : this.viewResolvers) {
                    View view = viewResolver.resolveViewName(viewName, locale);
                    if (view != null) {
                        return view;
                    }
                }
            }
            return null;
        }
        // ...
    }
            

     

    viewResolvers는 ViewResolver 인스턴스 리스트입니다. ViewResolver는 뷰를 찾기 위해 어디를 살펴보아야 하는지 알고있고 해당되는 뷰 인스턴스를 제공합니다. 

     

    View의 render() 메소드를 실행하여 HTML 페이지를 사용자 브라우저에 송신하고, 마침내 Spring은 요청 처리를 마무리합니다.

     

    Reference

    [1] Professional Apache Tomcat 5

    [2] Servlet Container and Spring Framework

    [3] https://stackify.com/spring-mvc/

    [4] https://web.archive.org/web/20171218173656/http://www.novocode.com/doc/servlet-essentials/chapter1.html

    [5] https://www.sciencedirect.com/science/article/pii/S1877050917329800

     

    반응형
Kaden Sungbin Cho