INFLEARN/스프링 MVC 1편

[스프링 MVC 1] 5. 스프링 MVC - 구조 이해

0298 2021. 10. 27. 00:08

5. 스프링 MVC - 구조 이해

 

[강의 정리] 김영한님 :: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

 

 

1. 스프링 MVC 전체 구조

 

# Spring MVC 구조

  • 지금까지 직접 구현해본 프레임워크와 스프링 MVC를 비교하면 거의 유사
  • FrontController -> DispatcherServlet
  • ViewResolver와 View는 인터페이스로 제공됨 

출처 : 김영한님 강의 자료

 

 

# DispatcherServlet 구조 살펴보기

org.springframework.web.servlet.DispatcherServlet
  • 스프링 MVC도 프론트 컨트롤러 패턴으로 구현
  • 즉, 디스패처 서블릿(Dispatcher Servlet)이 스프링 MVC의 프론트 컨트롤러

 

  • DispatcherServlet도 부모 클래스에서 HttpServlet을 상속 받아서 사용하고, 서블릿으로 동작
  • 스프링 부트는 DispatcherServlet을 서블릿으로 자동으로 등록하고 모든 경로(urlPatterns="/")에 대해서 매핑 (자세한 경로가 우선순위가 더 높음)

 

# doDispatch()

  • 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출 (스프링 MVC는 FrameworkServlet에서 오버라이드 해둠)
  • FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서 DispaterServlet.doDispatch()호출

(1) 핸들러 조회

  • 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러) 조회

(2) 핸들러 어댑터 조회

  • 핸들러를 실행할 수 있는 핸들러 어댑터 조회

(3) 핸들러 어댑터 실행

(4) 핸들러 어댑터를 통해 (실제) 핸들러 실행

(5) ModelAndView 반환

  • 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환

(6) 뷰 리졸버 통해서 뷰 찾기

  • ex) JSP : InternalResourceViewResolver가 자동 등록되고 사용됨

(7) View 반환

  • 뷰 리졸버 : 논리 이름 -> 물리 이름, 렌더링 역할을 담당하는 뷰 객체를 반환
  • ex) JSP : InternalResourceView(JstlView)를 반환
  • 내부에 forward()로직이 있음

(8) 뷰 렌더링

  • 뷰를 통해서 뷰를 렌더링

 

 

# 주요 인터페이스

  • Spring MVC의 강점은 DispatcherServlet 코드의 변경 없이 원하는 기능을 변경하거나 확장 가능 (물론 쉬운 일은 아님)
핸들러 매핑 : org.springframework.web.servlet.HandlerMapping
핸들러 어댑터 : org.springframework.web.servlet.HandlerAdapter
뷰 리졸버 : org.springframework.web.servlet.ViewResolver
뷰 : org.springframework.web.servlet.View

 

 

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

 

# Controller 인터페이스

  • 과거 버젼의 스프링 컨트롤러 (
  • +) Controller 인터페이스는 @Controller 애노테이션과 전혀 다름

# 컨트롤러는 어떻게 호출되는가

  • OldController
  • @Component : /springmvc/old-controller 라는 이름으로 스프링 빈으로 등록 (빈의 이름으로 URL 매핑)
  • 컨트롤러가 호출되려면 2가지가 필요 (핸들러 매핑 / 핸들러 어댑터)

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

 

# 스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터 (OldController)

  • HandlerMapping과 HandlerAdapter를 순서대로 찾음 (실제로는 더 많음..)
  • HandlerMapping 
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping 에서
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러 찾기
  • HandlerAdapter
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스

 

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

  • HandlerMapping을 순서대로 실행해서 핸들러 찾기
  • 빈 이름으로 핸들러 찾아야하므로, BeanNameUrlHandlerMapping 실행에 성공, 핸들러인 OldController 반환

(2) 핸들러 어댑터 조회

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

 

(3) 핸들러 어댑터 실행

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

 

# HttpRequestHandler

  • 서블릿과 가장 유사한 형태의 핸들러

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

  • HandlerMapping을 순서대로 실행해서 핸들러 찾기
  • 빈 이름으로 핸들러 찾아야하므로, BeanNameUrlHandlerMapping 실행에 성공, 핸들러인 MyHttpRequestHandler 반환

(2) 핸들러 어댑터 조회

  • HandlerAdapter의 supports()를 순서대로 호출
  • HttpRequestHandlerAdapter가 HttpRequestHandler 인터페이스를 지원하므로 대상이 됨

 

(3) 핸들러 어댑터 실행

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

 

# @RequestMapping

  • 가장 우선순위가 높은 핸들러 매핑 : RequestMappingHandlerMapping
  • 가장 우선순위가 높은 핸들러 어댑터 : RequestMappingHandlerAdapter
  • 실제 개발에서는 거의 @RequestMapping만 사용 
  • 애노테이션 기반의 컨트롤러를 지원

 

 

3. 뷰 리졸버

 

# View를 사용할 수 있도록

  • OldController

  • application.properties

 

# 뷰 리졸버 - InternalResourceViewResolver

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

 

# 뷰 리졸버 동작방식 

  • 스프링 부트가 자동 등록하는 뷰 리졸버
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환

(1) 핸들러 어댑터 호출

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

(2) ViewResolver 호출

  • new-form이라는 뷰 이름으로 viewResolver를 순서대로 호출
  • BeanNameViewResolver는 new-form이라는 이름의 스프링 빈으로 등록된 뷰를 찾지만 없음
  • 그래서 그 다음 순서인 InternalResourceViewResolver가 호출

(3) InternalResourceViewResolver

  • InternalResourceView 반환

(4) 뷰 - InternalResourceView

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

(5) view.render()

  • 호출되면 InternalResourceView가 forward()를 사용해서 JSP 실행

 

+) 참고

  • InternalResourceViewResolver는 만약 JSTL라이브러리가 있으면 JstlView를 반환
  • 다른 뷰는 실제 뷰를 렌더링해서 JSP와 달리 forward() 과정 없이 바로 렌더링 됨

 

 

4. 스프링 MVC - 시작하기

 

# @RequestMapping

  • RequestMappingHandler
  • RequestMappingAdapter

 

# SpringMemberFormControllerV1 - 회원 등록 폼

  • @Controller
    • 스프링이 자동으로 스프링 빈으로 등록 (내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 됨)
    • 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식
  • @RequestMapping
    • 요청 정보를 매핑
    • 해당 URL이 호출되면 이 메서드가 호출
    • 애노테이션 기반으로 동작하므로 메서드 이름은 임의로 지어도 상관 없음
  • ModelAndView
    • 모델과 뷰 정보 담아서 반환

 

RequestMappingHandlerMapping은 스프링 빈 중에서 @RequestMapping 또는 @Controller가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식 

(1) @Controller (거의 이 방식만 사용)
(2) @Component @RequestMapping
(3) 컴포넌트 스캔 없이 직접 스프링 빈으로 등록해도 가능

 

 

# SpringMemberSaveControllerV1 - 회원 저장

  • 기존에 만들었던 V3와 유사
  • mv.addObject("member", member) : 스프링이 제공하는 ModelAndView를 통해 Model 데이터를 추가할 때는 addObject() 사용 (이 데이터는 이후 뷰 렌더링 할 때 사용)

 

#  SpringMemberListControllerV1 - 회원 목록

  • 기존에 만들었던 V3와 유사

 

 

5. 스프링 MVC -  컨트롤러 통합

 

# 컨트롤러 통합

  • @RequestMapping은 메서드 단위에 적용되서 앞서 만들었던 회원 등록 폼/회원 저장/회원 목록을 하나로 통합할 수 있음

 

# 조합

  • 중복 제거 가능 (클래스 레벨에 공통적인 부분 뺌)

 

 

6. 스프링 MVC - 실용적인 방식

 

# 매번 ModelView 생성하고 반환하는거 불편

  • 회원 등록 폼

  • 회원 저장

  • 회원 목록

 

# Model 파라미터

  • save(), members() 를 보면 Model을 파라미터로 받음

 

# ViewName 직접 반환

  • 뷰의 논리 이름 반환

 

# @RequestParam 사용

  • 스프링은 HTTP 요청 파라미터를 @RequestParam으로 받을 수 있음
  • GET 쿼리 파라미터, POST Form 방식 모두 지원
  • @RequestParam("username")은 request.getParamter("username")과 거의 같은 코드

 

# @RequestMapping -> @GetMapping, @PostMapping

  • HTTP Method를 구분 짓지 않으면, GET임에도 불구하고 POST를 날려도 넘어갈 수 있음
  • 그래서 URL만 매칭하는 것이 아니라 HTTP Method도 구분 필요 (아래와 같이)
@RequestMapping(value = "/new-form", method = RequestMethod.GET)
  • 하지만, 조금 더 편리하게 아래와 같이 사용 가능 (Get, Post, Put, Delete, Patch 모두 애노테이션 있음)
@GetMapping("/new-form")