스프링 MVC 전체 구조
- 직접 만든 MVC 프레임워크와 스프링 MVC를 비교해보자.
직접 만든 MVC 프레임워크 구조
SpringMVC 구조
직접 만든 프레임워크 스프링 MVC 비교
직접 만든 프레임워크 | 스프링 MVC |
---|
FrontController | DispatcherServlet |
handlerMappingMap | HandlerMapping |
MyHandlerAdapter | HandlerAdapter |
ModelView | ModelAndView |
viewResolver | ViewResolver |
MyView | View |
DispatcherServlet
- 스프링 MVC도 프론트 컨트롤러 패턴으로 구현되어 있다.
- 스프링 MVC의 프론트 컨트롤러가 바로 디스패처 서블릿(DispatcherServlet)이다.
- 그리고 이 디스패처 서블릿이 바로 스프링 MVC의 핵심이다.
DispatcherServlet 서블릿 등록
- DispatcherServlet 도 부모 클래스에서 HttpServlet 을 상속 받아서 사용하고, 서블릿으로 동작한다.
- DispatcherServlet → FrameworkServlet → HttpServletBean → HttpServlet
- 스프링 부트는 DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든 경로에 대해서 매핑한다.
요청 흐름
- 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출된다.
- 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해두었다.
- FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서
- DispatcherServlet.doDispatch()가 호출된다.
DispatcherServlet의 핵심
- 지금부터 DispatcherServlet 의 핵심인 doDispatch() 코드를 분석해보자.
- 최대한 간단히 설명하기 위해 예외 처리, 인터셉터 기능은 제외했다.
스프링 MVC 구조
스프링의 동작 순서
- 핸들러 조회
- 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
- 핸들러 어댑터 조회
- 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
- 핸들러 어댑터 실행
- 핸들러 실행
- ModelAndView 반환
- 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.
- viewResolver 호출
- 뷰 리졸버를 찾고 실행한다.
- JSP의 경우에는 InternalResourceViewResolver 가 자동 등록되고, 사용된다.
- View 반환
- 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
- JSP의 경우에는 InternalResourceView(JstlView) 를 반환하는데, 내부에 forward() 로직이 있다.
- 뷰 렌더링
스프링 MVC의 인터페이스
- 스프링 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가 이미 웹 애플리케이션을 만들 때 필요한 대부분의 기능들을 미리 만들어 뒀기 때문이다.
- 그래도 핵심 동작 원리를 알아야지 향후 문제가 발생했을 떄 어떤 부분에서 문제가 발생했는지 쉽게 파악하고 해결할 수 있다.
핸들러 매핑과 핸들러 어댑터
- 지금은 전혀 사용하지 않지만, 과거에 주로 사용했던 스프링이 제공하는 간단한 컨트롤러로 핸들러 매핑과 어댑터를 이해해보자.
Controller 인터페이스
- 과거에 사용했던 스프링 컨트롤러 인터페이스다.
- 스프링도 처음에는 이런 딱딱한 형식의 컨트롤러를 제공했다.
- 참고
- Controller 인터페이스는 @Controller 애노테이션과는 전혀 다르다.
Controller 인터페이스 구현해보기
- Controller 인터페이스를 간단하게 구현해보자.
- @Component
- 이 컨트롤러는
/springmvc/old-controller
라는 이름의 스프링 빈으로 등록되었다.
- 빈의 이름으로 URL을 매핑할 것이다.
OldController는 어떻게 호출될 수 있을까?
- OldController가 호출되려면 2가지가 필요하다.
- HandlerMapping(핸들러 매핑)
- 핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 한다.
- 예시
- 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
- HandlerAdapter(핸들러 어댑터)
- 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
- 예시
- Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.
- 스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현해두었다.
- 개발자가 직접 핸들러 매핑과 핸들러 어댑터를 만드는 일은 거의 없다.
- 스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터
- 핸들러 매핑도, 핸들러 어댑터도 모두 순서대로 찾고 만약 없으면 다음 순서로 넘어간다.
- 실제로는 더 많지만, 중요한 부분 위주로 설명하기 위해 일부 생략하였다.
- HandlerMapping
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
- HandlerAdapter
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리
- 핸들러 매핑도, 핸들러 어댑터도 모두 순서대로 찾고 만약 없으면 다음 순서로 넘어간다.
- 핸들러 매핑으로 핸들러 조회
- HandlerMapping을 순서대로 실행해서, 핸들러를 찾는다.
- 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping가 실행에 성공하고,
핸들러인 OldController를 반환한다.- 핸들러 어댑터 조회
- HandlerAdapter의 supports()를 순서대로 호출한다.
- SimpleControllerHandlerAdapter가 Controller 인터페이스를 지원하므로 대상이 된다.
- 핸들러 어댑터 실행
- 디스패처 서블릿이 조회한 SimpleControllerHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨준다.
- SimpleControllerHandlerAdapter는 핸들러인 OldController를 내부에서 실행하고, 그 결과를 반환한다.
HttpRequestHandler 인터페이스
HttpRequestHandler 인터페이스 구현해보기
- 핸들러 매핑으로 핸들러 조회
- HandlerMapping을 순서대로 실행해서, 핸들러를 찾는다.
- 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping가 실행에 성공하고,
핸들러인 MyHttpRequestHandler를 반환한다.
- 핸들러 어댑터 조회
- HandlerAdapter의 supports()를 순서대로 호출한다.
- HttpRequestHandlerAdapter가 HttpRequestHandler 인터페이스를 지원하므로 대상이 된다.
- 핸들러 어댑터 실행
- 디스패처 서블릿이 조회한 HttpRequestHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨준다.
- HttpRequestHandlerAdapter는 핸들러인 MyHttpRequestHandler를 내부에서 실행하고, 그 결과를 반환한다.
@RequestMapping
- 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter이다.
- @RequestMapping 의 앞글자를 따서 만들어졌다.
- 이것이 바로 지금 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터이다.
- 실무에서는 99.9% 이 방식의 컨트롤러를 사용한다.
뷰 리졸버
OldController 수정
- 뷰를 사용할 수 있도록
return null;
대신에 return new ModelAndView("new-form");
을 적용하자. - 서버를 실행해서
http://localhost:8080/springmvc/old-controller
에 접속해보자.- 웹 브라우저에는 Whitelabel Error Page라고 출력된다.
- 콘솔에서는 OldController.handleRequest라고 출력된다.
- 실행해보면 컨트롤러를 정상 호출되지만, Whitelabel Error Page 오류가 발생한다.
application.properties 수정
- application.properties에 다음 코드를 추가하자.
뷰 리졸버 - InternalResourceViewResolver
- 스프링 부트는 InternalResourceViewResolver라는 뷰 리졸버를 자동으로 등록한다.
- 이 때, application.properties에 등록한
spring.mvc.view.prefix
와 spring.mvc.view.suffix
설정 정보를 사용해서 등록한다.
- 참고로 권장하지는 않지만 설정 없이 다음과 같이 전체 경로를 주어도 동작하기는 한다.
return new ModelAndView("/WEB-INF/views/new-form.jsp");
스프링 부트가 자동 등록하는 뷰 리졸버
- 스프링 부트가 자동 등록하는 뷰 리졸버
- 실제로는 더 많지만, 중요한 부분 위주로 설명하기 위해 일부 생략
- ViewResolver
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능 에 사용)
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
- 핸들러 어댑터 호출
- 핸들러 어댑터를 통해 new-form이라는 논리 뷰 이름을 획득한다.
- ViewResolver 호출
- new-form이라는 뷰 이름으로 viewResolver를 순서대로 호출한다.
- BeanNameViewResolver는 new-form이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다.
- InternalResourceViewResolver가 호출된다.
- InternalResourceViewResolver
- 이 뷰 리졸버는 InternalResourceView를 반환한다.
- 뷰 - InternalResourceView
- InternalResourceView는 JSP처럼 forward()를 호출해서 처리할 수 있는 경우에 사용한다.
- view.render()
- view.render()가 호출되고 InternalResourceView는 forward()를 사용해서 JSP를 실행한다.
- 참고
- InternalResourceViewResolver는 만약 JSTL 라이브러리가 있으면 InternalResourceView를 상속받은 JstlView를 반환한다.
- JstlView는 JSTL 태그 사용시 약간의 부가 기능이 추가된다.
- 다른 뷰는 실제 뷰를 렌더링하지만, JSP의 경우 forward()통해서 해당 JSP로 이동(실행)해야 렌더링이 된다.
- JSP를 제외한 나머지 뷰 템플릿들은 forward() 과정 없이 바로 렌더링 된다.
- Thymeleaf 뷰 템플릿을 사용하면 ThymeleafViewResolver 를 등록해야 한다.
- 최근에는 라이브러리만 추가하면 스프링 부트가 이런 작업도 모두 자동화해준다.
스프링 MVC - 시작하기
- 스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작해서, 매우 유연하고 실용적이다.
- 과거에는 자바 언어에 애노테이션이 없기도 했고, 스프링도 처음부터 이런 유연한 컨트롤러를 제공한 것은 아니다.
@RequestMapping 컨트롤러
- 스프링이 만든 @RequestMapping 애노테이션을 활용한 매우 유연하고, 실용적인 컨트롤러
- 스프링에서 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping와 RequestMappingHandlerAdapter이다.
- @RequestMapping의 앞글자를 따서 만들어졌다.
- 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 핸들러 매핑과 어댑터이다.
- 실무에서는 99.9% 이 방식의 컨트롤러를 사용한다.
회원 등록 폼
- @Controller
- 스프링이 자동으로 스프링 빈으로 등록한다.
- 내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 된다.
- 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다.
- @RequestMapping
- 요청 정보를 매핑한다.
- 해당 URL이 호출되면 이 메서드가 호출된다.
- 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 된다.
- ModelAndView
- @Controller (클래스 레벨) + @RequestMapping (메서드 레벨) 대신에 다른 방식을 사용해도 된다.
- @Component (클래스 레벨) + @RequestMapping (메서드 레벨)
- @RequestMapping (클래스 레벨) + @RequestMapping (메서드 레벨)
- 스프링 부트 3.0 미만에서만 가능하다.
- 스프링 부트 3.0 (스프링 프레임워크 6.0) 이상부터는 이렇게 사용하려면 스프링 빈으로 직접 등록해줘야 한다.
- 그런데 굳이 이렇게 사용할 일이 있을까 싶다.
회원 저장
회원 목록
스프링 MVC - 컨트롤러 통합
- @RequestMapping을 잘 보면 클래스 단위가 아니라 메서드 단위에 적용된 것을 확인할 수 있다.
- 따라서 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.
조합
- 컨트롤러 클래스를 통합하는 것을 넘어서 조합도 가능하다.
- 해당 컨트롤러에는
/springmvc/v2/members
가 중복이 된다. - 그래서 예제를 보면 클래스 레벨의 @RequestMapping에
/springmvc/v2/members
를 명시가 되어있다. - 따라서 메소드 레벨의 @RequestMapping의 주소는 아래와 같이 매핑된다.
- 회원 등록
/springmvc/v2/members/new-form
- 회원 저장
/springmvc/v2/members/save
- 회원 목록
/springmvc/v2/members
- 추가 주소를 명시하지 않았기 때문에
/springmvc/v2/members
가 된다. /springmvc/v2/members
에 공백 문자를 더한거라고 이해하면 된다.
스프링 MVC - 실용적인 방식
- MVC 프레임워크 만들기를 떠올려 보자.
- v3은 ModelView를 개발자가 직접 생성해서 반환했기 때문에 불편했었다.
- v4를 만들면서 실용적으로 개선하긴 했었다.
- 스프링 MVC는 개발자가 편리하게 개발할 수 있도록 수 많은 편의 기능을 제공한다.
- 실무에서는 지금부터 설명하는 방식을 주로 사용한다.
- Model 파라미터
- save(), members()를 보면 Model을 파라미터로 받는 것을 확인할 수 있다.
- 스프링 MVC도 이런 편의 기능을 제공한다.
- ViewName 직접 반환
- @RequestParam 사용
- 스프링은 HTTP 요청 파라미터를 @RequestParam으로 받을 수 있다.
- @RequestParam(“username”)은 request.getParameter(“username”)와 거의 같은 코드라 생각하면 된다.
- 물론 GET 쿼리 파라미터, POST Form 방식을 모두 지원한다.
- @RequestMapping → @GetMapping, @PostMapping
- @RequestMapping 은 URL만 매칭하는 것이 아니라, HTTP Method도 함께 구분할 수 있다.
- 예를 들어서 URL이 /new-form 이고, HTTP Method가 GET인 경우를 모두 만족하는 매핑을 하려면 다음과 같이 처리하면 된다.
@RequestMapping(value = "/new-form", method = RequestMethod.GET)
- 이것을 @GetMapping, @PostMapping으로 더 편리하게 사용할 수 있다.
- 참고로 Get, Post, Put, Delete, Patch 모두 애노테이션이 준비되어 있다.
출처