본문 바로가기

Develop/SPRING FRAMEWORK

Sprinv MVC 기본 구조 알아보기

1. 스프링 MVC 전체 구조

스프링 MVC의 전체 구조는 위 그림과 같다.

기본적으로 프론트 컨트롤러 패턴으로 구현되어 있다. 이때 프론트 컨트롤러는 바로 Dispatcher Servlet이다. 이놈이 핵심이다.

 

이러한 Dispatcher Servlet도 부모 클래스에서 HttpServlet을 상속 받아서 사용한다. 스프링 부트가 모든 경로(urlpatterns="/")에 대해서 Dispatcher Servlet을 매핑한다. 따라서 어떤 경로로 들어온 요청이든 모두 Dispatcher Servlet을 먼저 거치게 된다. 물론 더 자세한 경로가 우선순위가 높기 때문에 별도의 서블릿을 등록하더라도 잘 동작한다.

2. 동작 순서

  1. 핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
  2. 핸들러 어댑터 조회: 해당 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
  3. 핸들러 어댑터 실행: 해당 핸들러 어댑터를 실행한다.
  4. 핸들러 실행: 핸들러 어댑터가 실제 핸들러(컨트롤러)를 실행한다.
  5. ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환하여 반환한다.
  6. viewResolver 호출: 뷰 리졸버를 찾아서 실행한다.
  7. View 반환: 뷰 리졸버가 뷰의 논리 이름을 물리 이름으로 바꾼 뒤 렌더링 역할을 담당하는 뷰 객체를 반환한다.
  8. 뷰 렌더링: 뷰 객체를 통해서 실제 뷰를 렌더링 한다.

스프링 MVC는 이러한 기능을 대부분 인터페이스로 제공한다. 따라서 해당 인터페이스를 잘 구현해서 Dispatcher Servlet에 등록만 해준다면 Dispatcher Servlet 코드의 변경 없이 원하는 기능을 변경하거나 확장할 수 있다.

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

HandlerMapping (핸들러 매핑)
    0 : RequestMappingHandlerMapping
    1 : BeanNameUrlHandlerMapping

HandlerAdapter (핸들러 어댑터)
    0 : RequestMappingHandlerAdapter
    1 : HttpRequestHandlerAdapter
    2 : SimpleControllerHandlerAdapter

 

먼저 HandlerMapping을 순서대로 실행하여 핸들러를 찾는다. 그리고 HandlerAdapter의 supports()를 순서대로 호출하여 핸들러 어댑터를 찾는다. 그러고 나서 핸들러 어댑터를 실행하면서 앞서 조회한 핸들러 정보를 함께 넘겨주면 핸들러 어댑터는 내부에서 해당 핸들러를 실행하고 그 결과를 반환하게 된다.

 

1) Controller 인터페이스

public interface Controller {
	
    ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
@Component("/springmvc/old-controller")
public class OldController implements Controller {

  @Override
  public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
    System.out.println("OldController.handleRequest");
    return null;
}

 

Controller 인터페이스를 사용하는 다음과 같은 예제가 있다. (@Controller와는 전혀 다르다)

이때 OldController는 컴포넌트 스캔을 통해 /springmvc/old-controller라는 이름의 스프링 빈으로 등록된다.

 

먼저 핸들러 매핑에서 해당 컨트롤러를 찾을 수 있어야 한다. 따라서 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping가 실행에 성공하고 핸들러인 OldController를 반환한다. 그러고 나서 Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾아서 실행해야 한다. 이때 DispatcherServlet은 SimpleControllerHandlerAdapter를 조회한다.  SimpleControllerHandlerAdapter는 내부에서 OldController을 실행하고 그 결과를 반환한다.

 

2) HttpRequestHandler

public interface HttpRequestHandler {

	void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
@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는 컴포넌트 스캔을 통해 /springmvc/request-handler라는 이름의 스프링 빈으로 등록된다.

 

빈 이름으로 핸들러를 조회하는 BeanNameUrlHandlerMapping가 실행에 성공하고 MyHttpRequestHandler를 반환한다. 그러고 나서 HttpRequestHandler 인터페이스를 지원하는 핸들러 어댑터를 찾는다. DispatcherServlet은 HttpRequestHandlerAdapter를 조회한다. HttpRequestHandlerAdapter는 내부에서 MyHttpRequestHandler를 실행하고 그 결과를 반환한다.

3. 뷰 리졸버

@Component("/springmvc/old-controller")
public class OldController implements Controller {

  @Override
  public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    return new ModelAndView("new-form");
}

 

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

스프링 부트는 InternalResourceViewResolver라는 뷰 리졸버를 자동으로 등록하는데 이때 applcation.properties에 등록한 prefix, suffix 정보를 사용한다. (물론 전체 경로를 주어도 동작한다)

 

스프링 부트가 자동으로 등록하는 ViewResolver
    0 : BeanNameViewResolver (빈 이름으로 뷰를 찾아서 반환)
    1 : InternalResourceViewResolver (JSP를 처리할 수 있는 뷰를 반환)

 

먼저 핸들러 어댑터를 통해서 new-form이라는 뷰의 논리 이름을 얻는다. DispatcherServlet은 해당 이름을 가지고 viewResolver를 순서대로 호출한다. 이때 new-form이라는 이름의 스프링 빈으로 등록된 뷰가 없기 때문에 InternalResourceViewResolver가 호출된다. 해당 뷰 리졸버는 InternalResourceView를 반환한다. (InternalResourceView는 JSP와 같이 forward()를 호출하여 처리할 수 있는 경우에 사용된다) 그러고 나서 view.render()가 호출되고 내부적으로 InternalResourceView가 forward()를 호출한다. (만약 JSP를 제외한 나머지 뷰 템플릿을 사용하는 경우에는 forward() 과정 없이 바로 렌더링 된다)