스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술을 공부하고 정리하는 포스트입니다.


스프링 MVC 구조 이해

직접 만든 MVC 프레임 워크와 비교

  • 프론트 컨트롤러(Front Controller) 패턴의 MVC 프레임워크와 스프링 MVC의 내부구조는 동일
    • FrontController -> DispatcherServlet
    • handlerMappingMap -> HandlerMapping
    • MyHandlerAdapter -> HandlerAdapter
    • ModelView -> ModelAndView
    • ViewResolver -> viewResolver
    • MyView -> View



스프링 MVC 전체 구조

설명

  • 스프링 MVC도 앞서 직접 만든 것과 동일하게 프론트 컨트롤러(Front Controller) 패턴으로 구현
  • 프론트 컨트롤러 역할을 하는 것이 바로 Dispatcher Servlet -> Dispatcher Servlet이 스프링 MVC의 핵심

DispatcherServlet

설명

  • 부모 클래스를 따라가면 결국 HttpServlet을 상속받아서 사용하는 서블릿!
    • DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet
  • 스프링 부트는 DispatcherServlet을 서블릿으로 자동 등록하면서 모든 경로에 대해 매핑
    • 이렇게 모든 경로에 대해 매핑된 다음 지정하는 url 경로에 대해 매핑됨 -> 자세한 경로의 우선순위가 높음!
  • 서블릿이 호출되면 HttpServlet의 service()가 호출
    • FrameworkServlet에서 service()를 Override 해두었기 때문에 이를 통해 서블릿이 시작 -> 이를 통해 DispatcherServlet.doDispatch()가 호출되며 우리의 비즈니스 로직이 시작
    • 결국, 스프링 MVC의 핵심 동작은 DispatcherServlet.doDispatch()에 모두 기술

DispatcherServlet.doDispatch()

  • DispatcherServlet.doDispatch() 핵심 동작 코드
    • 예외 처리 / 인터셉터 기능은 제외한 간단한 로직만 정리
protected void doDispatch(HttpServletRequest request, HttpServletResponseresponse) throws Exception {
  HttpServletRequest processedRequest = request;
  HandlerExecutionChain mappedHandler = null;
  ModelAndView mv = null;

  // 1. 핸들러 조회
  mappedHandler = getHandler(processedRequest);
  if (mappedHandler == null) {
    noHandlerFound(processedRequest, response);
    return;
  }

  // 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
  HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

  // 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
  mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
  /* 아래 정의된 함수를 호출 */
  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();

  // 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
  view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
  // 8. 뷰 렌더링
  view.render(mv.getModelInternal(), request, response);
}


  1. 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회
  2. 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터 조회
  3. 핸들러 어댑터 실행 : 핸들러 어댑터를 실행
  4. 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행
  5. ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환
  6. viewResolver 호출 : 뷰 리졸버를 찾고 실행
    • JSP의 경우 : InternalResourceViewResolver가 자동 등록, 사용
  7. View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환
    • JSP의 경우 : InternalResourceView(JstlView)를 반환, 내부에 forward() 로직이 있음
  8. 뷰 렌더링 : 뷰를 통해서 뷰를 렌더링

인터페이스

  • 인터페이스 살펴보기
    • 대부분의 기능을 확장 가능한 인터페이스로 제공
    • 스프링 MVC의 강점은 DispatcherServlet 코드 변경 없이 원하는 기능을 변경하거나 확장 가능한 것
    • 인터페이스만 구현해서 DispatcherServlet에 등록하면 나만의 컨트롤러 생성 가능 (실제로는 만들 일이 거의 없음)
  • 주요 인터페이스
    • 핸들러 매핑 : org.springframework.web.servlet.HandlerMapping
    • 핸들러 어댑터 : org.springframework.web.servlet.HandlerAdapter
    • 뷰 리졸버 : org.springframework.web.servlet.viewResolver
    • 뷰 : org.springframework.web.servlet.View

정리

  • 스프링 MVC는 복잡해서 내부 핵심 구조만 알고 있어도 충분
    • 직접 기능을 확장하거나 나만의 컨트롤러를 만들 일은 거의 없음
    • 이미 수많은 기능이 개발되어 있기 때문에 필요한 기능은 대부분 존재
  • 스프링 MVC의 핵심 구조를 알아야 향후 문제가 발생했을 때 어떤 부분에서 발생했는지 파악이 가능

핸들러 매핑 / 핸들러 어댑터 / 뷰 리졸버

개요

  • 스프링 MVC의 구조에서 핵심적인 역할을 하는 것은 크게 3가지
    • 핸들러 매핑 : url에 해당하는 핸들러 조회
    • 핸들러 어댑터 : 핸들러를 처리할 수 있는 핸들러 어댑터(실제 핸들러 호출)
    • 뷰 리졸버 : 논리 이름을 파일의 실제 물리 주소로 변환
  • 핸들러 매핑 / 핸들러 어댑터가 스프링 MVC에서 어떻게 동작하는지 이해하는게 중요!
  • 지금은 사용하지 않는 인터페이스를 통해 내부 동작 흐름을 쉽게 이해 가능

핸들러 매핑 / 핸들러 어댑터 동작 이해 - Controller 인터페이스를 활용

  • 지금은 사용하지 않는 Controller라는 인터페이스를 상속한 OldController가 호출되는 과정을 알아보자


  • 해당 핸들러(컨트롤러)가 호출되려면 2가지가 필요
    • HandlerMapping(핸들러 매핑) : 해당 url 경로를 처리할 수 있는 핸들러 매핑을 찾고 해당하는 핸들러 반환
      ex) 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요
    • HandlerAdapter(핸들러 어댑터) : 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터 필요
      eX) Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행
  • HandlerMapping / HandlerAdapter
    • 스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현.
/* HandlerMapping */
0 = RequestMappingHandlerMapping // @RequestMapping의 핸들러 매핑
1 = BeanNameUrlHandlerMapping // 스프링 빈(Bean)의 이름으로 핸들러를 매칭
/* HandlerAdapter */
0 = RequestMappingHandlerAdapter // @RequestMapping의 핸들러 어댑터
1 = HttpRequestsHandlerAdapter // HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter // Controller 인터페이스 처리
  • 동작순서
    • 핸들러 매핑 찾기 & 핸들러 반환
      • HandlerMapping을 순서대로 실행 -> 실행할 수 있는 핸들러 매핑을 탐색
      • 이 경우 빈(Bean) 이름으로 핸들러를 찾아야 하기 때문에 BeanNameUrlHandlerMapping을 찾음 (우선순위 1에 해당)
      • 찾은 핸들러인 OldController를 반환
    • 핸들러 어댑터 조회
      • HandlerAdapter의 supports()를 순서대로 호출하며 처리할 수 있는 핸들러 어댑터를 탐색
      • 이 경우 SimpleControllerHandlerAdapter가 Controller 인터페이스를 지원함으로 선택됨 (우선순위 2에 해당)
    • 핸들러 어댑터 실행
      • 위에서 찾은 핸들러 어댑터인 SimpleControllerHandlerAdapter를 통해 실제 OldController 실행
      • ModelAndView 객체를 반환

뷰 리졸버

  • 논리 뷰 이름을 통해서 실제 물리 주소로 매핑해주는 역할
  • 스프링 부트가 자동 등록하는 뷰 리졸버

  • 스프링 부트는 JSP 사용을 위해 InternalResourceViewResolver라는 뷰 리졸버를 자동으로 등록
  • application.properties에 prefix / suffix 설정 정보를 사용해서 등록해야 함
    • spring.mvc.view.prefix : /WEB-INF/views
    • spring.mvc.view.suffix : .jsp
0 = BeanNameViewResolver // 빈 이름으로 뷰를 찾아서 반환
1 = InternalResourceViewResolver // JSP를 처리할 수 있는 뷰를 반환
  • 동작 순서
    • 핸들러 어댑터 호출
      • new-form이라는 논리 뷰 이름을 호출
    • ViewResolver 호출
      • new-form이라는 뷰 이름으로 viewResolver를 순서대로 호출
      • 이 경우 스프링 빈이 없기 때문에 InternalResourceViewResolver가 호출
    • InternalResourceViewResolver
      • InternalResourceView를 반환
    • InternalResourceView
      • InternalResourceView는 JSP처럼 forward()를 호출해서 처리할 수 있는 경우에 사용
    • view.render()
      • view.render()가 호출되고 InternalResourceViewforward()를 사용해 JSP를 실행

스프링 MVC 시작하기

  • 스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작 -> 유연하고 실용적

@RequestMapping

  • 매우 실용적인 컨트롤러

  • @RequestMapping
    • RequestMappingHandlerMapping
    • RequestMappingHandlerAdapter
  • 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMappingRequestMappingHandlerAdapter
    • 애노테이션 기반의 컨트롤러를 지원하는 핸들러 매핑과 어댑터
    • 실무에서 99.9%가 이 방식의 컨트롤러를 사용


  • 클래스 레벨에 @RequestMapping으로 공통 Path를 지정
  • @RequestMapping을 편리하게 사용하기 위한 @GetMapping() / @PostMapping()을 사용
  • @RequestParam을 이용해서 요청 변수를 바로 받음
  • ModelAndView 객체를 직접 반환하는 것이 아니라 viewName만으로 처리해서 사용성이 좋아짐