관리 메뉴

나구리의 개발공부기록

스프링 MVC - 구조이해, 스프링 MVC 전체 구조, 핸들러 매핑과 핸들러 어댑터, 뷰 리졸버 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

스프링 MVC - 구조이해, 스프링 MVC 전체 구조, 핸들러 매핑과 핸들러 어댑터, 뷰 리졸버

소소한나구리 2024. 2. 29. 18:50

  출처 : 인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/Gmptq


1. 스프링 MVC 전체 구조

1) 스프링 MVC

(1) 직접 작성한 MVC 프레임워크 구조와 스프링 MVC 구조

  • 구조 둘의 구조가 동일함

좌) 직접 작성한 MVC프레임워크 / 우) SpringMVC

 

(2) 직접만든 MVC 프레임워크 -> 스프링 MVC 1:1 비교

  • FrontController -> DispatcherServlet
  • handlerMappingMap -> HandlerMapping(인터페이스): 핸들러 매핑
  • MyHandlerAdapter -> HandlerAdapter(인터페이스): 핸들러 어댑터
  • ModelView -> ModelAndView
  • viewResolver -> ViewResolver(인터페이스): 뷰 리졸버
  • MyView -> View(인터페이스): 뷰
  • 굵은 표시는 주요 인터페이스 목록

(2) 동작 순서 설명

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

(3) 인터페이스 살펴보기

  • 스프링 MVC의 큰 강점은 DispatcherServlet 코드의 변경 없이 원하는 기능을 변경하거나 확장이 가능하도록 인터페이스들을 제공함
  • 해당 인터페이스들을 구현해서 DispatcherServlet에 등록하면 자신만의 컨트롤러를 만들 수도 있음

2) DispatcherServlet 구조 살펴보기

  • 스프링 MVC도 프론트 컨트롤러 패턴으로 구현되어있으며 스프링 MVC의 프론트 컨트롤러가 바로 디스패처 서블릿(DispatcherServlet)임
  • 이 디스패처 서블릿이 스프링 MVC의 핵심

(1) DispatcherServlet 등록

  • 부모클래스에서 HttpServlet을 상속받아서 사용하며 서블릿으로 동작
  • DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet
  • 스프링 부트는 DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든경로(urlPatterns = "/")에 대해서 매핑함
  • 참고로 더 자세한 경로가 우선순위가 높으므로 기존에 등록한 서블릿도 함께 동작함

(2) 요청 흐름

  • 서블릿 호출 -> HttpServlet이 제공하는 service()가 호출 됨
  • 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service를 오버라이딩 해두었음
  • FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서 DispatcherServlet.doDispatch()가 호출 됨

(3) DispatcherServlet의 핵심 메서드인 doDispatch() 코드 분석

  • 너무 구현 코드가 많으므로 예외처리, 인터셉터 기능은 제외하고 분석
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {    
    // 1. 핸들러 조회
    mappedHandler = this.getHandler(processedRequest);
    if (mappedHandler == null) {
        this.noHandlerFound(processedRequest, response);
        return;
    }
    
    // 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
    
    // 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);    
}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, 기타 파라미터들...) throws Exception{
    // 뷰 렌더링 호출
    this.render(mv, request, response);    
}

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

 

(4) 정리

  • 스프링 MVC는 코드 분량도 매우 많고 복잡해서 내부구조를 파악하는 것이 쉽지 않음
  • 이미 전세계의 수많은 개발자들이 요구사항에 맞추어 기능을 확장해왔기 때문에 대부분의 기능은 이미 구현이 되어있어서 기능을 직접 확장하거나 나만의 컨트롤러를 만드는일은 없으므로 만들어진 기능을 잘 활용하는 것에 중점을 두면 됨
  • 핵심 동작방식을 알아두는 이유는 향후 문제가 발생했을 때 어떤 부분에서 문제가 발생했는지 파악하고 해결할 때 쉽게 접근이 가능하고 만약 확장포인트가 필요하다면 어떤 부분을 확장해야 할지 감을 잡기 수월하기 때문임

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

1) 핸들러 매핑과 해들러 어댑터 이해

(1) Controller 인터페이스 - 과거버전 스프링 컨트롤러

  • 지금은 전혀 사용하지 않지만 과거에는 주로 사용하였으며 스프링도 처음에는 이런 딱딱한 형식의 컨트롤러를 제공하였음
  • 참고로 @Controller(애노테이션)과는 전혀 다름
package org.springframework.web.servlet.mvc;

public interface Controller {
    ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

 

(2) OldController

  • web패키지 하위에 springmvc.old 패키지를 생성하여 Controller 인터페이스를 구현하는 클래스를 작성
  • @Component: 이 컨트롤러는 /springmvc/old-controller라는 이름의 스프링 빈으로 등록되며 빈의 이름으로 URL을 매핑함
  • 작성 후 애플리케이션을 실행하여 localhost:8080/springmvc/old-controller에 접속하여 로그에 해당 출력문이 출력되면 성공임
package hello.servlet.web.springmvc.old;

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

 

(3) 해당 컨트롤러가 호출될 때 필요한 항목

  • HandlerMapping(핸들러 매핑) : 핸들러 매핑에서 해당 컨트롤러를 찾을 수 있어야 함
    예를 들어 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요함
  • HandlerAdapter(핸들러 어댑터) : 핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요함
    예를 들어 Controller 인터페이스를 실행 할 수 있는 핸들러 어댑터를 찾고 실행 해야 함

(4-1) 스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터(실제로는 더 많지만 중요한 부분만 작성)

  • 스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터 대부분을 구현해두었기 때문에 개발자가 직접 만드는 일은 거의 없음
  • 실제로는 더 많지만 중요한 부분 위주로 설명하기위해 일부 생략
  • 핸들러 매핑, 핸들러 어댑터 모두 순서대로 찾고 만약 없으면 다음 순서로 넘어감

(4-2) HandlerMapping

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

 

(4-3) HandlerAdapter

0 = RequestMappingHandlerAdapter: 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리

 

(5-1) 핸들러 매핑으로 핸들러 조회

  1. HandlerMapping을 순서대로 실행해서 핸들러를 찾음
  2. 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아 주는 BeanNameUrlHandlerMapping이 실행되고 핸들러인 OldController를 반환

(5-2) 핸들러 어댑터 조회

  1. HandlerAdapter의 supports()를 순서대로 호출
  2. SimpleControllerHandlerAdapter가 Controller 인터페이스를 지원하므로 대상이 됨

(5-3) 핸들러 어댑터 실행

  1. 디스패처 서블릿이 조회한 SimpleControllerHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨줌
  2. SimpleControllerHandlerAdapter는 핸들러인 OldController를 내부에서 실행하고 그 결과를 반환

(6) 정리 - OldController 매핑, 어댑터

  • OldController를 실행하며 사용 된 객체는 아래와 같음
HandlerMapping = BeanNameUrlHandlerMapping
HandlerAdapter = SimpleControllerHandlerAdapter

2) HttpRequestHandler

(1) HttpRequestHandler 인터페이스

  • Controller인터페이스가 아닌 다른 핸들러이며 서블릿과 가장 유사한 형태의 핸들러임(컨트롤러)
package org.springframework.web;

public interface HttpRequestHandler {
    void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

 

(2) MyHttpRequestHandler구현

  • 작성 후 localhost:8080/springmvc/request-handler로 접속하여 출력문이 나오면 정상임
  • OldController(Controller 인터페이스)와 동일하지만 빈 이름으로 핸들러를 찾으면 MyHttpRequestHandler가 반환됨
package hello.servlet.web.springmvc.old;

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

 

(3-1) 핸들러 매핑으로 핸들러 조회

  1. HandlerMapping을 순서대로 실행해서 핸들러를 찾음
  2. 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아 주는 BeanNameUrlHandlerMapping이 실행되고 핸들러인 MyHttpRequestHandler를 반환

(3-2) 핸들러 어댑터 조회

  1. HandlerAdapter의 supports()를 순서대로 호출함
  2. HttpRequestHandlerAdapter가 HttpRequestHandler 인터페이스를 지원하므로 대상이 되어 실행이 됨

(3-3) 핸들러 어댑터 실행

  1.  디스패처 서블릿이 조회한 HttpRequestHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨줌
  2. 핸들러인 MyHttpRequestHandler를 내부에서 실행하고 그 결과를 반환함

(4) 정리 - MyHttpRequestHandler 핸들러 매핑, 어댑터

  • MyHttpRequestHandler를 실행하며 사용 된 객체
HandlerMapping = BeanNameUrlHandlerMapping
HandlerAdapter = HttpRequestHandlerAdapter

 

(5) @RequestMapping

  • 뒤에서 더 설명이 나오지만 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping과 RequestMappingHandlerAdapter임
  • @RequestMapping의 앞글자를 따져 만들어졌는데 이것이 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터이며 실무에서 99.9% 이 방식의 컨트롤러를 사용함 (MVC 시작하기에서 자세히 다룸)

3. 뷰 리졸버

1) 뷰 리졸버 이해

(1) OldController 수정 - View 조회를 할 수 있도록 변경

  • View를 사용할 수 있도록 return new ModelAndView("/new-form")을 추가
  • 애플리케이션을 실행 후 localhost:8080/springmvc/old-controller에 접속해보면 웹브라우저는 에러페이지가 출력되지만 콘솔에는 정상적으로 출력문이 확인됨
  • 즉, 컨트롤러는 정상 호출되지만 Whitelabel Error Page 오류가 발생하는데, 뷰 리졸버 기능이 필요함
package hello.servlet.web.springmvc.old;

@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 new ModelAndView("new-form");	// 추가
    }
}

 

 

(2) application.properties 수정

  • 아래의 설정 추가 후 다시 애플리케이션을 실행하고 해당 경로로 들어가보면 등록 폼이 정상적으로 출력되는 것을 확인할 수 있음
  • 물론 저장 기능을 개발하지 않았으므로 현재는 폼만 출력되고 더 진행하면 오류가 발생함
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

 

(3) 뷰 리졸버 - InternalResourceViewResolver

  • 스프링 부트는 InternalResourceViewResolver라는 뷰 리졸버를 자동으로 등록함
  • 이때 application.properties에 등록한 spring.mvc.view.prefix와 spring.mvc.view.suffix 설정 정보를 사용해서 등록함

** 참고

  • 권장하지는 않지만 설정 없이 OldController의 반환값인 ModelAndView에 전체경로를 주어도 동작하기는 함
  • return new ModelAndView("/WEB-INF/views/new-form.jsp");

2) 뷰 리졸버 동작 방식

(1) 스프링 부트가 자동으로 등록하는 뷰 리졸버(실제로는 더 많음)

1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.

 

(2) 핸들러 어댑터 호출

  • 핸들러 어댑터를 통해 new-form이라는 논리 뷰 이름을 획득

(3) View Resolver 호출

  • 획득한 논리뷰 이름으로 viewResolver를 순서대로 호출
  • BeanNameViewResolver는 new-form이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야하는데 없음
  • InternalResourceViewResolver가 호출됨

(4) InternalResourceViewResolver

  • InternalResourceView를 반환

(5) 뷰 - InternalResourceView

  • InternalResourceView는 JSP처럼 forward()를 호출해서 처리할 수 있는 경우에 사용함

(6) view.render()

  • view.render()가 호출되고 InternalResourceView는 forward()를 사용해서 JSP를 실행

** 참고

  • 만약 JSTL 라이브러리가 있으면 InternalResourceView를 상속받은 JstlView를 반환하는데 JstlView는 JSTL태그 사용시 약간의 부가 기능이 추가 된 것임
  • 다른 뷰 템플릿은 실제 뷰를 렌더링(forward() 과정 없이 바로 렌더링) 하지만 JSP의 경우 forward()를 통해서 해당 JSP로 이동(실행)해야 렌더링이 됨
  • Thymeleaf 뷰 템플릿을 사용하면 ThymeleafViewResolver를 등록해야하는데 최근에는 라이브러리만 추가하면 스프링 부트가 모두 자동으로 작업해줌