MVC 프레임워크 만들기

스프링 MVC 1편 강의 정리

스프링 MVC 전체 구조

DispatcherServlet

  • HttpServlet 상속받아 서블릿으로 동작
  • 스프링 부트에서 DispatcherServlet을 서블릿으로 자동 등록하면서 모든 경로(urlPatterns="/")에 대해 매핑하며 자세한 경로가 우선순위가 더 높음
  • 요청 흐름
    • 서블릿이 호출되면 HttpServlet이 제공하는 service() 가 호출됨
    • 스프링 MVC에서는 DispatcherServlet이 상속받은 FrameworkServlet에서 service()를 재정의함
    • FameworkServlet.service()가 호출되면서 DispatcherServlet.doDispatch()가 호출됨
// doDispatch 간단 버전
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
  HttpServletRequest processedRequest = request;
  HandlerExecutionChain mappedHandler = null;
  ModelAndView mv = null;
  
  // 핸들러 조회
  mappedHandler = getHandler(processedRequest);
  if (mappedHandler == null) {
    noHandleFound(processedRequest, response);
    return;
  }
  
  // 핸들러 어댑터 조회: 핸들러를 처리할 수 있는 어댑터
  HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
  
  // 핸들러 어댑터 실행 -> 핸들러 어댑터 통해 핸들러 실행 -> ModelAndView 반환
  mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
  
  processedDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

private void processedDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception execption) throws Exception {
  // 뷰 렌더링 호출
  render(mv, request, reponse);
}

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
  View view;
  String viewName = mv.getViewName();
  
  // 뷰 리졸버 이용해서 뷰 찾기 -> view 반환
  view = resolveViewName(viewName, mv.getModelinternal(), locale, request);
  
  // 뷰 렌더링
  view.render(mv.getModelInternal(), request, response);
}

SpringMVC 구조

  1. DispatcherServlet에서 매핑된 핸들러에서 핸들러 조회
  2. DispatcherServlet에서 핸들러를 처리할 수 있는 핸들러 어댑터 조회
  3. DispatcherServlet에서 핸들러 어댑처 실행
  4. 핸들러 어댑터에서 핸들러(컨트롤러) 실행
  5. 핸들러 어댑터가 DispatcherServlet에 ModelAndView 반환
  6. DispatcherServlet에서 ViewResolver 실행
  7. ViewResolver가 뷰의 논리 이름을 물리 이름으로 바꾸고 렌더링을 담당하는 뷰 객체 반환
  8. DispatcherServlet이 View의 render(model) 메서드 호출하여 HTML 반환

인터페이스

  • 스프링 MVC의 강점은 인터페이스로 제공되어 DispatcherServlet 코드의 변경없이 원하는 기능을 추가/확장할 수 있다는 점
  • 주요 인터페이스
    • 핸들러 매핑: org.springframework.web.servlet.HandlerMapping
    • 핸들러 어댑터: org.springframework.web.servlet.HandlerAdapter
    • 뷰 리졸버: org.springframework.web.servlet.ViewResolver
    • 뷰: org.springframework.web.servlet.View

Handler mapping & handler adapter

Controller Interface

  • 현재에는 사용하지 않은 과거 버전의 스프링 컨트롤러
// org.springframework.web.servlet.mvc.Controller
@FunctionalInterface
public interface Controller {
  @Nullable
  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 {
    return null;
  }
}
  • 위의 컨트롤러가 호출되기 위해 필요한 조건
    • 핸들러 매핑: 핸들러 매핑에서 위의 컨트롤러를 찾을 수 있어야 함
    • 핸들러 어댑터: 핸들러 매핑에서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 있어야 함
  • 위의 두 조건은 스프링에서 구현이 되어있고 스프링 부트에서는 자동으로 등록
  • 핸들러 매핑은 BeanNameUrlHandlerMapping에 의해 실행
  • 핸들러 어댑터는 SimpleControllerHandlerAdapter에 의해 지원됨

Handler Mapping

  • 1순위
    • RequestMappingHandlerMapping
    • 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
  • 2순위
    • BeanNameUrlHandlerMapping
    • 스프링 빈의 이름으로 핸들러를찾음

Handler Adapter

  • 1순위
    • RequestMappingHandlerAdapter
    • 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
  • 2순위
    • HttpRequestHandlerAdapter
    • HttpRequestHandler 처리
  • 3순위
    • SimpleControllerHandlerAdapter
    • Controller 인터페이스

HttpRequestHandler

  • 서블릿과 가장 유사한 형태의 핸들러
@FunctionalInterface
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 {
  }
}
  • BeanNameUrlHandlerMapping에 의해 핸들러를 찾아 실행됨
  • HttpRequestHandlerAdapterHttpRequestHandler 인터페이스를 지원하므로 핸들러 어댑터를 조회
  • 디스패처 서블릿이 HttpRequestHandlerAdapter를 실행하면서 구현한 핸들러의 결과 반환

View Resolver

  • 기존의 OldController에서 View를 쓸 수 있도록 반환 타입을 ModelAndView로 변경
  • 서버를 띄우면 컨트롤러는 호출되지만 해당 뷰를 찾지 못함
  • application.properties 에 관련 설정 추가
@Component("/springmvc/old-controller")
public class OldController implements Controller {

  @Override
  public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
      throws Exception {
    return new ModelAndView("new-form");
  }
}
# application.properties
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
  • 스프링 부트에서 InternalResourceViewResolver 자동으로 등록
    • spring.mvc.view.prefix, spring.mvc.view.suffix 설정을 사용해서 등록
  • 스프링 부트가 자동 등록하는 뷰 리졸버
    • BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환
    • InternalResourceViewResolver : JSP를 처리할 수 있는 뷰 반환
  • 과정
    • 핸들러 어댑터를 통해 뷰의 논리 이름 확인
    • 뷰 이름으로 viewResolver 순서대로 호출
      • 위의 코드에서는 빈 이름으로 찾을 수 없으므로 InternalResourceViewResolver가 호출됨
    • 뷰 리졸버가 InternalResourceView 반환
    • view.render()가 호출되고 InternalResourceView가 forward()를 사용해서 JSP 실행

@RequestMapping

  • 애노테이션을 활용한 유연하고 실용한 컨트롤러를 쓸 수 있게 함
  • 요청 정보를 매핑하고 해당 URL이 호출되면 메서드가 호출됨
  • 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping & RequestMappingHandlerAdapter로서 각각 애노테이션 기반의 컨트롤러를 지원하는 핸들러 매핑과 어댑터
  • @Controller
    • 스프링이 자동으로 스프링 빈으로 등록
    • 내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 됨
    • 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식
  • RequestMappingHandlerMappingr은 스프링 빈 중에 @RequestMapping또는 @Controller애노테이션이 클래스에 있을 경우 매핑 정보로 인식
  • HTTP method도 매핑 가능 (@RequestMapping(value="/members", method=RequestMethod.GET))
    • @GetMapping, @PostMapping, @DeleteMapping, @PutMapping, @PatchMapping 등의 애노테이션으로 더 간결하게 사용 가능

그외

  • @RequestParam : HTTP 요청 파라미터를 받을 수 있게 해주는 애노테이션
  • Model : view에 데이터를 전달하는 객체