출처 : http://www.mungchung.com/xe/spring/102692
Spring MVC 기본 흐름을 나맘대로 정리해봤다.
HandlerAdapter
DispatcherServlet에서 Controller를 찾을 때 어떻게 찾아야 하는지 정의
1. Servlet과 SimpleServletHandlerAdapter
2. HttpRequestHandler와 HttpRequestHandlerAdapter
3. Controller와 SimpleControllerHandlerAdapter (기본)
4. AnnotationMethodHandlerAdapter (기본) - DefaultAnnotationHandlerMapping과 같이 사용됨
HandlerMapping
URL (혹은 요청)과 맵핑된 Controller을 어떻게 찾아야하는지 정의
(http://www.mungchung.com/xe/spring/21278)
1. BeanNameUrlHandlerMapping
2. ControllerBeanNameHandlerMapping
3. ControllerClassNameHandlerMapping
4. SimpleUrlHandlerMapping
5. DefaultAnnotationHandlerMapping
ViewResolver
Controller에서 반환한 ModelAndView를 가지고 View (jsp)를 찾을때 어떻게 찾아야하는지 정의
1. XmlViewResolver (jsp 경로를 xml로 설정함)
2. ResourceBundleViewResolver (jsp경로를 properties로 설정함)
3. UrlBasedViewResolver (view경로로 jsp 찾음)
4. InternalResourceViewResolver
5. VelocityViewResolver / FreeMarkerViewResolver
HandlerAdapter / HanderMapping / ViewResolver 을 보면 몇가지 적용방법들이 있는데
처음엔 뭐가뭔지 잘 모르고 왜이리 종류가 많을까 했는데 보다보니, 결국 이 종류들은 관계(URL과 컨트롤러, 컨트롤러와 View 등)를 맵핑할때
xml 설정으로 찾을래? 아니면 properties 로 설정할래? 아님 어노테이션 설정으로 찾을래? 아님 Bean 이름으로 찾을래? 등을 의미한다.
내용추가 :: 2013.09.25
위의 MVC 기본흐름에 대한 내용이 빈약한것 같아서 filter, HandlerInterceptor 이 추가된 그림을 첨부한다.
(출처 : http://www.cnblogs.com/fangwenyu/archive/2012/10/11/2716665.html)
스프링 MVC는 어떻게 작동하는가?
스프링 MVC 프레임워크는 Model-View-Controller 아키텍쳐를 제공해주고, 쉽고 유연한 개발을 하도록 지원해준다.
스프링 MVC에서 가장 큰 주체는 DispatcherServlet이다.
DispatcherServlet은 Java EE의 Servlet을 래핑한 클래스이다. 컨트롤러의 컨트롤러 같은 느낌으로 개발을 아주 편리하게 하게 해준다. Request를 올바른 처리 핸들러에 위임하는 것부터 Model과 View 처리, Controller 매핑 등.. 다양한 기능을 지원해준다.
이미지 : https://docs.spring.io/spring/docs/3.0.0.M4/reference/html/ch15s02.html
'Front controller' 라고 표현된 DispatcherServlet이 받은 Request를 핸들러에 위임하고, Controller에서 만들어진 model을 response에 알맞게 렌더링 하고, View Template을 렌더링하는 등... 많은 작업을 수행한다.
DispatcherServlet이 어떻게 동작하는지 알아보자
먼저 DispatcherServlet을 등록해야 한다.
DispatcherServlet을 사용하기 위해서 당연히 어딘가에 등록을 해야할 것이다.
요즘엔 잘 사용하지 않지만, web.xml 을 사용했던 시절부터 쫓아가보자. 어플리케이션 배포서술자(Deployment Descriptor) web.xml 이라는 파일에 서버가 알아야할 정보를 서술해 놓았었다. 서블릿, 그리고 리스너와 필터와 같은 컴포넌트, error와 welcome 페이지를 등록할 수 있다.
일반적으로 여기에 DispatcherServlet을 url패턴과 함께 등록하여 모든 url을 dispatcherServlet을 거치도록 설정을 해놓을 것이다. Servlet 3.0 이상에서부터 WebApplicationInitializer 구현 또는 AbstractAnnotationConfigDispatcherServletInitializer 상속으로 web.xml에서 하던 설정을 java config로 설정할 수 있다. java config를 통해 설정하여 classpath 내에 두면, SpringServletContainerInitializer이 서블릿을 초기화할 때 감지한다.
WebApplicationInitializer가 bootstrap되는 메커니즘은 여기에서 확인할 수 있다.
WebApplicationInitializer이든 web.xml이든 어떤 방법을 사용하든 DispatcherServlet을 등록이 되었을 것이다.
DispacherServlet 초기화
웹 컨테이너(supports servlet. tomcat같은)는 사용자의 요청에 대해 request와 response 객체를 생성한다. 그리고 앞에서 살펴본것처럼 배포서술자를 통해 어떤 DispatcherServlet가 처리할지를 알아낸다. 만일 해당 클래스가 한번도 실행된 적이 없다면, 새로 인스턴스를 생성하고, 초기화를 한다.
DispatcherServlet이 생성되고 초기화될때, initStrategies(ApplicationContext context) 메소드를 실행하여 DispatcherServlet에 관련된 빈들을 초기화한다. 앞에서 다양한 기능을 지원해준다고 했는데, 그 다양한 기능을 전략 패턴을 통해 구현했다. 생성된 전략 빈들을 dispatcherServlet에 주입하고, 주입된 전략 빈에 의해 기능이 수행된다. 초기화 메소드에서 아래에서 설명할 전략 인터페이스의 빈이 존재하는지를 찾고, 없으면 default 설정에 따라 빈을 생성하거나 아무 작업이 없을 수도 있다. (전략 인터페이스의 빈 생성은 <mvc:annotation-driven/> 혹은 @EnableMvc 를 통해 웹 컨테이너가 initialize 되는 시점이다.)
그럼 DispatcherServlet이 가지는 기능(전략 인터페이스)를 간단히 알아보자.
MultipartResolver
RFC 1867에 따라 multipart 파일 업로드 요청에 대해 해석하여 변환하는 전략 인터페이스이다.
스프링에서 제공하는 구현체로는 CommonsMultipartResolver (for Apache Commons FileUpload), StandardServletMultipartResolver (for Servlet 3.0+) 2개가 있다.
LocaleResolver
웹 기반의 locale 결정 전략 인터페이스이다. 스프링에서는 Request, Session, Cookie 등을 기반으로 사용한 구현체들이 있다.
AbstractLocaleContextResolver, AbstractLocaleResolver, AcceptHeaderLocaleResolver(“accept-language” 헤더 값), CookieLocaleResolver (쿠키) , FixedLocaleResolver(locale 고정), SessionLocaleResolver(세션)
ThemeResolver
웹 기반의 Theme 결정 전략 인터페이스. <Spring:theme> 태그를 사용한 경우 구현체 ThemeResolver로 Theme를 정의할 수 있다.
HandlerMapping
Request와 핸들러(메소드) 객체를 매핑을 정의하는 인터페이스이다. 일반적으로 사용하는 @RequestMapping 을 이용한 핸들러 매핑 전략이다. 디폴트로 선언되는 빈은 BeanNameUrlHandlerMapping, DefaultAnnotationHandlerMapping 이다.
HandlerAdapter
Spring-MVC의 코어 SPI(Service Provider Interface). Controller의 구현 전략 인터페이스이다. HandlerMapping이 핸들러(메소드)를 찾아주면, HandlerAdpter가 해당 컨트롤러의 핸들러에 전달하기 위한 Adapter라고 보면 된다. HandlerAdapter 인터페이스를 보면 ModelAndView 를 리턴하는 handle 이라는 메소드가 있다. 핸들러를 실행(handle)시켜 ModelAndView를 리턴하게끔 해준다고 생각하면 된다.
스프링의 Controller 종류는 4가지가 있다. Servlet, Controller, HttpRequestHandler 구현체와 @Controller 어노테이션을 사용해서 POJO로 구현하는 방법이다.
각 컨트롤러를 DispatcherServlet에 연결해주는 Adapter가 하나씩 있어야 하므로, handlerAdapter도 총 4개이다. 그 중 SimpleServletHandlerAdapter를 제외한 3개의 핸들러 아답터가 DispatcherServlet의 default 전략으로 설정되어 있다.
HandlerExceptionResolver
Handler 매핑이나, 컨트롤러 실행 도중 발생한 예외를 다루는 인터페이스이다. 디폴트로 AnnotationMethodHandlerExceptionResolver(@ExceptionHandler 처리), ResponseStatusExceptionResolver(발생한 예외를 HTTP status code로 변경), DefaultHandlerExceptionResolver(앞선 exceptionResolver가 처리하지 못한 예외상황을 처리한다. 메소드를 찾지 못해 발생하는 예외는 HTTP 404를 발생시키는 등의 로직 수행) 가 선언된다.
RequestToViewNameTranslator
컨트롤러에서 뷰를 지정하지 않았을 때, Request를 참고하여 내부적으로 뷰 이름을 생성해준다.
ViewResolver
컨트롤러에서 Request를 처리하고 생성하는 결과물에 대한 view 처리 전략 인터페이스이다. 컨트롤러가 return "index.jsp"; 와 같이 뷰 이름을 넘겨주더라도 viewResolver가 ModelAndView 객체로 변경해서 돌려준다. 디폴트는 InternalResourceViewResolver (컨트롤러가 리턴한 뷰 이름으로 실제 뷰 이름을 지정. prefix와 suffix를 조합하는 로직도 여기에 있다) 이다.
FlashMapManager
FlashMap 객체를 retrieve & save 하는 전략 인터페이스이다.
redirect 해야하는 경우에, 특정 데이터를 넘겨야 할 때가 있다. 이때 RedirectAttributes 를 이용하여 데이터를 넘길텐데 2가지 방법이 있다. addAttribute 와 addFlashAttribute 이다. addAttribute는 request 파라미터로 넘겨주는 방식이고, addFlashAttribute 는 FlashMap을 이용해 넘길 데이터를 세션에 저장하고 retrieve 하면 세션에서 바로 삭제한다. flashMap에 담은 내용은 세션에 저장되고 redirect되어 데이터를 받게되면 바로 삭제한다.
위에서 설명한 모든 전략 인터페이스의 처리 과정은 주입된 빈에 의해 이루어진다.
커스터마이징이 필요한 부분은 인터페이스를 구현하여 스프링 빈으로 등록하면 된다. 스프링 빈으로 등록만 해놓으면 DispatcherServlet 이 초기화 시점에 빈 주입이 된다. 디폴트 빈은 <mvc:annotation-driven/> 혹은 @EnableMvc 를 사용할 때 생성된다. 디폴트 빈 설정은 spring-mvc 프로젝트내 DispatcherServlet.properties 에 선언되어 있으니 확인해보면 된다.
Request 처리
본격적으로 DispatcherServlet에서 사용자 요청에 대한 처리가 어떻게 될지 살펴보자.
서버쪽으로 온 요청은 웹 컨테이너에 의해 Request, Response 객체를 생성한다.
그리고 DispatcherServlet을 실행한다. 만약 컨테이너에서 DispatcherServlet의 객체가 없다면(처음 실행한다면), 객체를 생성하고 초기화한다.
유저 Request는 웹 컨테이너(tomcat)의 스레드풀로부터 하나의 스레드를 할당받아 DispatcherServlet의 service() 메소드를 호출한다. ( service() -> processRequest() -> doService() )
doService()에서 doDispatch() 메소드를 호출하게 되고, 실제 핸들러에 위임하게 된다.
코드를 살펴보자.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | 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 { // multipartResolver에 의해 resolve 처리 (request를 파싱하여 file이 있다면 set) processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // 현재 요청에 알맞은 핸들러를 가져온다.. mappedHandler = getHandler(processedRequest); if (mappedHandler == null || mappedHandler.getHandler() == null ) { noHandlerFound(processedRequest, response); return ; } // 위에서 가져온 핸들러를 실행(invoke) 시킬 수 있는 핸들러 아답터를 가져온다. (아래에서 더 자세히 살펴보자) HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // last-modified header 수정. ( last-modified 헤더값으로 브라우저나 proxy 서버에서 캐시를 이용할 수 있음) String method = request.getMethod(); boolean isGet = "GET" .equals(method); if (isGet || "HEAD" .equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (logger.isDebugEnabled()) { logger.debug( "Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); } if ( new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return ; } } // 적용할 interceptor가 있다면, preHandle 적용 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return ; } // 실제 컨트롤러 핸들러(메소드) 실행 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); //비동기 처리라면, 종료. WebAsyncManager에서 별도의 스레드가 처리하게 된다. if (asyncManager.isConcurrentHandlingStarted()) { return ; } //View 에 대한 설정이 없다면, RequestToViewNameTranslator 전략 빈에 의해 view name을 설정한다. applyDefaultViewName(processedRequest, mv); //interceptor postHandle 실행 mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { // 예외 처리 생략 } //핸들러 실행 결과 ModelAndView를 적절한 response 형태로 처리한다. (아래에서 더 자세히 살펴보자) processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } //아래 예외처리 부분 생략 |
위에서 핸들러를 실행시키는 부분을 추가적으로 살펴보자.
핸들러를 실행 하는 원리를 보면 Reflection을 이용한 메소드 invoke 방식이다. 이미 스프링 컨텍스트에 올라와있는 컨트롤러 빈을 이용해서 JVM 내 Metaspace 라는 공간에 메소드가 바이트코드 형태로 로드 되어있는데, arguments를 알맞게 설정하여 파라미터로 넣어주어 실행시키는 것이다.
물론 핸들러 실행시키는 부분은 컨트롤러를 어떻게 생성했느냐에 따라 다를 것이다. 위에서 말한것처럼 @Controller로 만들수도 있고, Controller를 상속해서 만들수도 있다. 각각의 방법에 대해 아답터가 있어야 한다.
모든 전략 빈에 대해서 살펴볼 수는 없으니 가장 흔히 사용하는 RequestMappingHandlerAdapter (@Controller 어노테이션을 사용한 컨트롤러 Adapter) 을 보겠다.
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 32 | public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { //request를 위한 arguments를 셋팅하고, 실제 핸들러를 실행한다! Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); setResponseStatus(webRequest); if (returnValue == null ) { if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { mavContainer.setRequestHandled( true ); return ; } } else if (StringUtils.hasText(getResponseStatusReason())) { mavContainer.setRequestHandled( true ); return ; } mavContainer.setRequestHandled( false ); try { //실행 결과 returnValue를 메소드의 return Type에 알맞게 설정한다. //(HandlerMethodReturnValueHandler 구현체를 통해.. 밑에 설명) this .returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) { if (logger.isTraceEnabled()) { logger.trace(getReturnValueHandlingErrorMessage( "Error handling return value" , returnValue), ex); } throw ex; } } |
각 핸들러는 Return Type이 제각각이다. @ResponseBody를 이용해 POJO 모델 클래스를 return 하면서 deserializing 한 결과를 그대로 내보낼 수도 있고, 템플릿 엔진(Jsp, Thymeleaf 등)을 이용하여 페이지명을 반환할 수도 있다. ModelAndView를 리턴할 수도 있고, ResponseEntity 사용할 수도 있고.. 경우가 많다.
그렇기 때문에 핸들러의 return type에 대해서도 많은 경우가 있고, 각각의 경우에 대한 Adapter가 또 필요하다. 그래서 스프링은 HandlerMethodReturnValueHandler 인터페이스로 각각의 전략들을 구현하도록 했다. 해당 부분은 구현체를 보면 어떻게 변환하는지를 어렵지않게 볼 수 있다. (코드도 짧음)
다음으로 dispatcherServlet이 dispatch한 결과를 어떻게 처리하는지 살펴보자.
processDispatchResult 메소드를 통해 렌더링을 처리한다. (processDispatchResult -> render -> view.render )
ViewResolver는 자신이 처리해서 return 할 뷰를 설정해야 한다. View라는 인터페이스로 또 다양한 화면을 그릴수 있다. XML, JSP, PDF, Redirect 등 다양한 View 형태를 가질 수 있다. 역시 마찬가지로 구현체마다 렌더링을 하는 방식이 다르므로 각각 살펴봐야 한다.
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 32 33 34 35 36 37 38 | protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { //localeResolver를 통하여 locale 설정 Locale locale = this .localeResolver.resolveLocale(request); response.setLocale(locale); View view; if (mv.isReference()) { // 뷰 설정이 레퍼런스라면(String 이라면, viewResolver를 통한 view = resolveViewName(mv.getViewName(), 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(); if (view == null ) { throw new ServletException( "ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'" ); } } // 뷰 (View) 구현체에 위임하여 렌더링을 하도록 한다. if (logger.isDebugEnabled()) { logger.debug( "Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'" ); } try { if (mv.getStatus() != null ) { response.setStatus(mv.getStatus().value()); } //ViewResolver에서 반환할 View 구현체에 의해 렌더링 view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { //예외 생략 } } |
(비동기 처리 과정에 대해서는 나중에 다시 정리를 해야겠다!)
여기서 궁금한점이 생길 수 있다.
url을 가지고 매핑하는 정보는 언제 초기화될까?
dispatcherServlet이 초기화된 시점에 이미 url별로 어떤 핸들러에 매핑을 시킬건지에 대한 정보를 들고 있기 때문에 DispatcherServlet만 따라가서는 어디서 언제 매핑 정보가 생성 되는지 알수가 없다. 매핑 정보 생성시점은 전략빈이 주입되는 시점이다. 일반적으로 많이 사용하는 전략빈으로 RequestMappingHandlerMapping을 살펴보겠다. 핸들러 매핑 정보를 어떻게 가져오는지 보자.
RequestMappingHandlerMapping
InitializingBean 을 구현했기 때문에 스프링 빈 초기화시 afterPropertiesSet() 메서드가 자동 호출된다.
여기서 ApplicationContext 내부의 모든 빈들을 불러와, 핸들러가 맞는지를 확인하여(Controller 또는 RequestMapping을 사용했는지로 여부판단), 핸들러로써 유효한 빈에 대해 detectHandlerMethods(beanName) 메소드를 호출한다.
clazz 안에 선언되어있는 모든 메소드들을 불러와 inspect 메소드를 통해 유효성 검사를 하고, 유효한 메소드들을 mappingRegistry 에 추가한다. mappingRegistry은 실제 request에 해당하는 url을 찾을때 사용된다.
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 32 | protected void detectHandlerMethods( final Object handler) { //get Class Class<?> handlerType = (handler instanceof String ? getApplicationContext().getType((String) handler) : handler.getClass()); final Class<?> userType = ClassUtils.getUserClass(handlerType); //class 안의 모든 메소드들을 순회하며 유효한 Handler만 반환 Map<Method, T> methods = MethodIntrospector.selectMethods(userType, new MethodIntrospector.MetadataLookup<T>() { @Override public T inspect(Method method) { try { return getMappingForMethod(method, userType); } catch (Throwable ex) { throw new IllegalStateException( "Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex); } } }); if (logger.isDebugEnabled()) { logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods); } for (Map.Entry<Method, T> entry : methods.entrySet()) { //invoke(호출) 가능한 메소드를 가져온다. Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType); T mapping = entry.getValue(); //RequestMappingInfo(request의 매핑 정보)가 invoke 가능한 method 와 함께 저장한다. registerHandlerMethod(handler, invocableMethod, mapping); } } |
뭔가 많이 적다보니 집중력도 떨어지고 처음에 살펴보고자 했던게 맞는지 잘 모르겠다.
내용도 산만해진것 같고, 위에 적은 내용 이외에도 MVC의 기능이 아주 많지만.. 설명하기엔 내 능력이 너무나 모자란것 같다.
너무 복잡해진것 같으니.. 간략하게 MVC 프로세스를 간략히 정리해보면
1. 배포서술자에 DispatcherServlet 등록한다.
2. 웹 컨테이너가 뜰때 DispatcherServlet의 전략 빈들이 생성된다.
3. 실제 사용자의 Request가 들어온다.
4. 웹 컨테이너는 DispatcherServlet 객체를 생성하고 초기화 한다. (첫 호출시에만)
5. 웹 컨테이너가 Request, Response 객체를 생성한다.
6. 웹 컨테이너의 쓰레드 풀에서 쓰레드 하나를 할당하여 DispatcherServlet 실행.
7. request uri에 알맞은 핸들러(메소드)를 가져온다. (실제 핸들러를 매핑하는 전략은 DispatcherServlet에 주입된 HandlerMapping 구현체에 의해)
8. 핸들러를 실행시키기 위하여 핸들러 아답터를 가져온다. (핸들러 클래스 타입마다 적용시킬 아답터가 다르므로)
9. 인터셉터의 preHandle 실행
10. 실제 컨트롤러 핸들러(메소드) 실행
11. 인터셉터의 postHandle 실행
12. 개발자가 설정한 View 에 알맞도록 렌더링을 실행하여 결과를 write
13. 웹 컨테이너가 response를 보내주고 유저 스레드 반환
다음 글은 스프링 MVC를 커스터마이징해서 만든걸 올리고 싶은데.. 스칼라 공부를 하느라 언제 할수 있을지 잘 모르겠다.
참고
http://www.baeldung.com/spring-mvc-handler-adapters
'프로그래밍 > Spring & MyBatis' 카테고리의 다른 글
[Spring] Spring Web Application Architecture (0) | 2018.12.19 |
---|---|
[Spring Boot] 외장 톰캣 사용 (WAR 배포) (0) | 2018.12.11 |
[Spring Framework] @Transactional 트랜젝션 관리 (0) | 2018.12.10 |
[Spring Framework]Bean 등록 방법 (0) | 2018.12.10 |
[Spring Boot] 첨부파일 (0) | 2018.12.06 |