인프런/스프링 MVC 1편

6)스프링 MVC (구조이해),핸들러 매핑과 핸들러 어댑터,뷰 리졸버

backend dev 2023. 1. 18.

Spring MVC (구조 이해)

이전 시간에 직접만든 MVC 프레임워크랑 실제 스프링 MVC를 비교해보자.

Spring MVC 구조

FrontController -> Dispatcher Servlet

직접 MVC 프레임워크를 만들때 프론트 컨트롤러는 서블릿으로 만들었다.

서블릿의 urlPatterns(어떤 url을 받을것인지)로 * 기호를 이용하여 원하는 http요청들을 다 받아냈고,

요청에 대해 핸들러를 조회해서 찾아오고, 핸들러 처리할 어댑터도 조회해오고 하면서 진행했다. 

Spring MVC에서는 Dispatcher Servlet으로 이름만 다르고 하는일은 동일하다.

(사전적의미 Dispatch -> 보내다(파견보내다) ,Dispatcher 보내고,배치하는것을 관리하는사람 

즉 Dispatcher Servlet은 들어온 http요청에 대한 처리를 위해 맞는 핸들러 어댑터로 핸들러를 보내고 그런 서블릿)

직접 만든 MVC프레임워크과 springMVC의 이름차이

핸들러매핑정보를 저장하기위한 handlerMappingMap -> HandlerMapping이라고 스프링MVC에 존재

viewResolver를 메소드를 만들어서 구현했었는데 스프링에서는 ViewResolver라고 인터페이스로 만들어놨다.

View또한 인터페이스로 만들어놨다 ( 인터페이스로 만들어놨다 == 확장성을 위함)

 DispatcherServlet 살펴보기

컨트롤 + n으로 검색해서 들어가보면

엄청긴 코드가 나를 맞이한다 

그 중 중요코드에 대해 알아보자.

위의 DispatcherServlet 코드를 보면 FrameworkServlet을 상속받는걸 볼 수있다. 

FrameworkServlet는 HttpServletBean을 상속받고 HttpServletBean은 HttpServlet을 상속받는다.

 

DispatcherServlet는 @WebServlet이라는 어노테이션이 없지만,

스프링부트가 내장톰캣을 띄우면서 DispatcherServlet를 서블릿으로 등록시키면서 톰캣을 띄운다.

그래서 DispatcherServlet가 서블릿으로 자동 등록된다. 그리고 모든 경로에 대해서 매핑한다.

(더 자세한 경로가 우선순위가 높다. 그래서 url패턴을 직접 등록한 서블릿도 동작한다(새로등록한 서블릿이 우선순위가 더 높다.))

 DispatcherServlet 흐름

이전시간까지 직접 서블릿을 만들때 HttpServlet을 상속받으면서 service() 메소드를 오버라이드했고

거기다가 비즈니스 로직을 적어주었다. 

 

Spring MVC는 FrameworkServlet에서 service()를 오버라이드해두었고 service()를 시작으로 여러메서드가 호출되면서

DispatcherServlet .doDispatch()가 호출된다고한다.

(FrameworkServlet안에 service()가 오버라이드 되어있으므로 그걸 상속받은 DispatcherServlet 는 쓰기만하면됨)

 

DispatcherServlet .doDispatch()

DispatcherServlet의 핵심인 DispatcherServlet .doDispatch()를 알아보자.

 최대한 간단히 설명하기 위해 예외처리, 인터셉터 기능은 제외했다.

    protected void doDispatch(HttpServletRequest request, HttpServletResponse
        response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        ModelAndView mv = null;
		// 1. 핸들러 조회
        mappedHandler = getHandler(processedRequest); //request를 이용해서 핸들러 찾아오는부분
        if (mappedHandler == null) { //핸들러가없을때 처리하는부분
            noHandlerFound(processedRequest, response);
            return;
        }
		// 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); //핸들러를 주고 핸들러 어댑터를 조회하는 부분
		// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); //핸들러 어댑터를 통해 핸들러 실행하고 ModelAndView를 반환받는 부분
        processDispatchResult(processedRequest, response, mappedHandler, mv,
            dispatchException);
    }
    private void processDispatchResult(HttpServletRequest request,
        HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView
        mv, Exception exception) throws Exception {// 뷰 렌더링 호출
        render(mv, request, response); //뷰 렌더링 호출하는부분
    }
    protected void render(ModelAndView mv, HttpServletRequest request,
        HttpServletResponse response) throws Exception {
        View view;
        String viewName = mv.getViewName(); //ModelAndView에서 뷰 논리이름가져오고
		// 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
        view = resolveViewName(viewName, mv.getModelInternal(), locale, request); //뷰리졸버를 통해 뷰를 찾고,View를 반환
		// 8. 뷰 렌더링
        view.render(mv.getModelInternal(), request, response); //뷰 렌더링
    }

이전에 구현한 MVC프레임워크의 동작방식과 아주 유사하다. 

1. HTTP 요청이 들어오면 Dispatcher Servlet에서 요청URL에 매핑된 핸들러(컨트롤러)를 조회한다.

 

2. Dispatcher Servlet는 조회된 핸들러를 처리할 수 있는 핸들러 어댑터를 조회한다.

 

3. Dispatcher Servlet가 핸들러 어댑터를 실행한다

(핸들러 어댑터 구현체는 핸들러어댑터 인터페이스를 구현해야하고 , 핸들러어댑터 인터페이스에는 해당 핸들러를 처리할수있는지 체크하는 supports와 핸들러를 호출해 작접을 처리하는 handle() 메소드가 있다. 우리가 만든 MVC 프레임워크에서 했던거처럼)

 

4.핸들러 어댑터는 ModelAndView를 반환해야하므로, 핸들러가 반환한 정보를 ModelAndView로 변환해서 반환해준다.

 

5. Dispatcher Servlet는 viewResolver를 찾고(뷰마다 viewResolver구현체가 다르다)

,반환받은 ModelAndView과 viewResolver를 이용해서 View 객체를 반환받는다. (ModelAndView에 뷰의 논리이름이 있음)

 

6. Dispatcher Servlet는 반환받은 View객체를 이용해 뷰를 렌더링 한다.

 

HandlerMapping 인터페이스를 구현한 구현체들이다.

Http요청에 맞게 핸들러를 매핑하는 핸들러매핑 구현체도 보이고, 종류별에 맞게 핸들러를 매핑한 핸들러매핑 구현체들이 보인다.

 

핸들러 어댑터는 당연히 처리할 핸들러에 따라 만들어져야하므로 다양한 구현체가 존재할것이다.

뷰리졸버도 jsp용,타임리프용 등 템플릿엔진에 따라 여러가지 구현체들이 존재한다.

뷰 또한 jsp용 뷰, 타임리프용 뷰 등 다 나눠진다.


핸들러 매핑과 핸들러 어댑터

핸들러 매핑과 핸들러 어댑터가 어떤 것들이 어떻게 사용되는지 알아보자.

 

지금은 전혀 사용하지 않지만, 과거에 주로 사용했던 스프링이 제공하는 간단한 컨트롤러로 핸들러 매핑과
어댑터를 이해해보자.

(과거에는 컨트롤러 인터페이스를 가지고 컨트롤러를 구현했다) 요즘에 사용하는 @Controller와는 다름

 

@Component("/springmvc/old-controller") //스프링빈 이름을 url패턴으로 해둔다.
public class OldController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
        return null;
    }
}

잘 동작되는걸 확인했다.

빈이름으로 url매핑했는데 잘 동작한다. 어떻게 동작하는것일까?

http://localhost:8080/springmvc/old-controller를 호출해서 http요청이 들어오면

 

Dispatcher Servlet이 핸들러 매핑에서 방금 만든 이 컨트롤러를 찾을 수 있어야한다.

(스프링 빈의 이름으로 핸들러를 찾는, 그런 핸들러 매핑(구현체)이 필요한것이다.)

핸들러 매핑을 통해서 찾은 핸들러(OldController)를 실행할 수 있는 핸들러 어댑터가 필요할것이다.

(OldController라는 핸들러를 실행시키려면 Controller 인터페이스를 실행 할 수 있는 핸들러 어댑터를 찾고 실행해야한다.)

이미 만들어져있기에 잘 동작한것이다.

방금 만든 OldController가 잘 실행된 이유는 BeanNameUrlHandlerMapping과 SimpleControllerHandlerAdapter가 이미 만들어져있기 때문이다 

 

요즘은 어노테이션 기반인 컨트롤러를 사용하므로 RequestMappingHandlerMapping과 RequestMappingHandlerAdapter를 주로 사용한다.

 

OldController 실행과정

HttpRequestHandler

@FunctionalInterface
public interface HttpRequestHandler {

   /**
    * Process the given request, generating a response.
    * @param request current HTTP request
    * @param response current HTTP response
    * @throws ServletException in case of general errors
    * @throws IOException in case of I/O errors
    */
   void handleRequest(HttpServletRequest request, HttpServletResponse response)
         throws ServletException, IOException;

}

HttpRequestHandler를 보면 서블릿과 아주 유사하다.

handleRequest() 메소드가       HttpServlet를 상속받고 service()를 오버라이드할때 그 service()와 유사하게 생겼다.

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

 

HttpRequestHandler를 만들어서 테스트해보자.

@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler {

    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("MyHttpRequestHandler.handleRequest");
    }
}

HttpRequestHandler를 구현해주는 MyHttpRequestHandler 구현체를 만들었다.

 

HttpRequestHandler는 HttpRequestHandler를 처리해주는 HttpRequestHandlerAdapter는 있지만

 

매핑해주는 HandlerMapping이 없기에 BeanNameUrlHandlerMapping를 이용한다 ( 스프링빈의 이름으로 url를 매핑한다)

 

http://localhost:8080/springmvc/request-handler로 url 호출을 해주면

잘동작하는걸 확인할 수 있다.

 

MyHttpRequestHandler 동작방식

1. HandlerMapping을 순서대로 실행시켜서 핸들러를 찾는다. 위에서 만든 MyHttpRequestHandler는 

스프링 빈이름으로 url를 매핑했으므로 , 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping가 실행에 성공할것이고 핸들러인 MyHttpRequestHandler를 반환해준다.

 

2. HandlerAdapter의 supports 메소드를 순서대로 호출해서 MyHttpRequestHandler를 처리할 수 있는 HandlerAdapter를 찾는다 

public class HttpRequestHandlerAdapter implements HandlerAdapter {

   @Override
   public boolean supports(Object handler) {
      return (handler instanceof HttpRequestHandler);
   }

저 supports 부분에서 걸릴것이다. MyHttpRequestHandler는 HttpRequestHandler를 구현한 구현객체이므로

 

3. Dispatcher Servlet이 조회한 HttpRequestHandlerAdapter를 실행하면서 핸들러 정보를 같이 넘겨준다.

HttpRequestHandlerAdapter는 MyHttpRequestHandler를 내부에서 실행시키고 , 그 결과를 반환해준다

MyHttpRequestHandler 동작 순서

 

 


뷰 리졸버

이번에는 뷰 리졸버에 대해서 자세히 알아보자.

@Component("/springmvc/old-controller") //스프링빈 이름을 url패턴으로 해둔다.
public class OldController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
        return new ModelAndView("new-form");
    }
}

위에서 만든 OldController의 반환값으로 ModelAndView를 만들어 반환한다. ("new-form"이라는 뷰 논리이름을 넣고)

 

이렇게 해두고 스프링부트를 실행해서 매핑된 url로 들어가보면

스프링빈의 이름으로 핸들러(컨트롤러)를 찾는 BeanNameUrlHandlerMapping으로 요청된 url에 맞는 OldController를 찾았고, 찾은 OldController를 처리해줄 SimpleControllerHandlerAdapter도 찾아서 SimpleControllerHandlerAdapter가 내부에서 OldController를 실행해줬기 때문에 로그가 잘 출력이 되었다.

 

하지만 응답화면은 에러페이지를 보여주고있다.

 

ModelAndView를 넘겼지만,  

viewResolver를 호출하면서 ModelAndView를 넘기고, View를 반환받고 반환받은 View로 렌더링하는 과정이 없었기에 원하는 화면이 보이지않고있다.  ( ModelAndView에 있는 논리뷰이름을 이용하여 viewResolver에서 물리뷰이름을 만들어서 View객체를 만들어주고, View객체를 반환하고, View객체를 이용해서 렌더링하는 과정)

 

application.properties에 다음 코드를 추가하자.

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

그리고 나서 다시 실행해보면

화면이 잘 보이는걸 확인가능하다.

 

스프링부트는 InternalResourceViewResolver라는 뷰 리졸버를 자동으로 등록하는데 

application.properties에  spring.mvc.view.prefix, spring.mvc.view.suffix 설정정보를 가지고 뷰 리졸버를 등록한다.

 

화면이 어떻게 보이는지에 대한 동작방식은 

 

밑에서 설명한다.

 

뷰 리졸버 동작방식

1. 핸들러 어댑터가 ModelAndView를 반환해주고

 

2. ModelAndView안에 있는 논리뷰이름을 가지고 

등록되어있는 뷰 리졸버를 순서대로 호출하면서 받은 논리뷰이름을 가지고 뷰를 생성하여 반환해줄 수 있는 뷰 리졸버를 찾는다. ( 받은 논리뷰이름을 가지고 뷰를 생성하여 반환해줄 수 있는 뷰 리졸버인지 체크하면서)

 

3. BeanNameViewResolver는 "new-form"이라는 이름의 스프링빈으로 등록된 뷰를 찾아야하는데 없으므로 뷰를 생성할 수 없고,

그러면 그다음인 InternalResourceViewResolve가 호출된다.

(InternalResourceViewResolve가 application.properties에 설정한 prefix, suffix 설정정보를 이용해서 물리뷰 이름을 만들어보고 ("/WEB-INF/views/new-form.jsp") 찾아봤더니 jsp가 존재하는것을 확인할 수 있다. 

InternalResourceViewResolve가 받은 논리뷰이름을 가지고 뷰를 만들 수 있다는것이다.

 

 // InternalResourceViewResolve( jsp같은 내부 자원을 찾을때 쓰는 뷰 리졸브)

 

4.InternalResourceViewResolve가 뷰를 생성해서 반환한다 ( JSP를 처리 할 수 있는)

InternalResourceView 라는 뷰이다. (View의 구현객체, JSP forward() 기능을 가지고있다.)

 

5. Dispatcher Servlet이 받은 뷰를 가지고 render()메소드를 호출하고, InternalResourceView는 forward()를 사용해서 JSP를 실행한다. (jsp가 렌더링 된다.)

 

DispatcherServlet이 논리적인 뷰 이름을 가지고 뷰를 생성하는 방법

 

DispatcherServlet에서는 핸들러 어댑터에서 받은 논리적인 뷰 이름을 가지고 뷰 리졸버 목록을 순회하며 view를 생성 시도한다. 뷰 리졸버가 뷰 생성에 성공하면 해당 뷰를 반환해준다.

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

BeanNameViewResolver는 resolveViewName()이라는 메소드로 뷰를 찾아 반환한다.

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
   ApplicationContext context = obtainApplicationContext();
   if (!context.containsBean(viewName)) {
      // Allow for ViewResolver chaining...
      return null;
   }
   if (!context.isTypeMatch(viewName, View.class)) {
      if (logger.isDebugEnabled()) {
         logger.debug("Found bean named '" + viewName + "' but it does not implement View");
      }
      // Since we're looking into the general ApplicationContext here,
      // let's accept this as a non-match and allow for chaining as well...
      return null;
   }
   return context.getBean(viewName, View.class);
}

InternalResourceViewResolve는 이름이 resolveViewName은 아니지만 buildView라는 메소드로 뷰를 생성해보는것같다.

@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
   InternalResourceView view = (InternalResourceView) super.buildView(viewName);
   if (this.alwaysInclude != null) {
      view.setAlwaysInclude(this.alwaysInclude);
   }
   view.setPreventDispatchLoop(true);
   return view;
}

DispatchServlet은

이렇게 뷰 리졸브들을 순회하면서 뷰 리졸브가 논리적인 뷰 이름을 가지고 , 뷰를 찾아 반환해주거나, 뷰를 생성해서 반환해주면 그 뷰를 받아 렌더링을 진행한다.

 

뷰 리졸브는 핸들러 어댑터의 반환된 값(논리적인 뷰 이름)을 이용해 뷰를 찾아 반환하거나(BeanNameViewResolver처럼), 뷰를 생성해서 반환한다.

DispatcherServlet은 뷰 반환에 성공한 뷰 리졸브에게 뷰를 받아 렌더링한다.

 

개념이 헷갈린다면 DispatcherServlet에서 코드를 살펴보자. 

인터페이스의 구현체를 살펴보고싶을때 단축키 컨트롤+알트+B

Resolver 같은거 등록할때는 수동빈등록할때처럼

@Bean
    ViewResolver myViewResolver() {
        return new MyViewResolver(매개변수);
    }
}

이렇게 스프링빈에 등록해주면 DispatchServlet이 스프링 컨테이너에 등록된 빈 중에 ViewResolver를 찾아 뷰 리졸버 목록을 만드는듯하다.

DispatchServlet.initViewResolvers()

private void initViewResolvers(ApplicationContext context) {
   this.viewResolvers = null;

   if (this.detectAllViewResolvers) {
      // Find all ViewResolvers in the ApplicationContext, including ancestor contexts.
      Map<String, ViewResolver> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
      if (!matchingBeans.isEmpty()) {
         this.viewResolvers = new ArrayList<>(matchingBeans.values());
         // We keep ViewResolvers in sorted order.
         AnnotationAwareOrderComparator.sort(this.viewResolvers);
      }
   }

// Find all ViewResolvers in the ApplicationContext ->ApplicationContext는 BeanFactory의 하위 인터페이스이고, BeanFactory는 스프링컨테이너에 접근 할 수 있다.

즉 스프링컨테이너에 접근해서 빈 중에 ViewResolver를 찾아 뷰 리졸버 목록을 만드는것같다. 

 

DispatchServlet.initHandlerAdapters()

private void initHandlerAdapters(ApplicationContext context) {
   this.handlerAdapters = null;

   if (this.detectAllHandlerAdapters) {
      // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts.
      Map<String, HandlerAdapter> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
      if (!matchingBeans.isEmpty()) {
         this.handlerAdapters = new ArrayList<>(matchingBeans.values());
         // We keep HandlerAdapters in sorted order.
         AnnotationAwareOrderComparator.sort(this.handlerAdapters);
      }
   }

핸들러 어댑터 또한 스프링컨테이너에 등록도니 빈중에 찾아서 핸들러 어댑터 목록을 만드는것 같다.

내가 원하는 커스텀 컨트롤러(핸들러)를 만들고 사용하려면 핸들러매핑,핸들러어댑터,뷰리졸버등을 커스텀해서 만들고 스프링빈으로 등록하면 될것같다. ( 물론 있는거 쓰는것도 바쁠것이다)

댓글