스프링 MVC가 제공하는 편리한 기능들을 공부하기 이전에
이전까지 학습했던 MVC 프레임워크와 스프링 MVC에 대한 비교를 먼저 해보자
1. 원래 구현했던 자체 MVC 구조
https://progyun.tistory.com/190
[스프링 웹 MVC 1편] 15. MVC 프레임워크 제작해보기 - v5
지금까지 우리는 V1~V4까지 MVC 패턴을 직접 개선해가면서 코드를 작성해보았다 정리하자면 다음과 같은 점진적 개선과정을 거쳤다 v1: 프론트 컨트롤러 도입을 통해 공통처리를 가능하게 했다 이
progyun.tistory.com
스프링 MVC는 우리가 구현했던 V5 코드와 상당히 유사한 구조를 갖고 있다
직접 만든 컨트롤러와 비교했을때 구조는 똑같으나 이름이 다른 차이가 있다
FrontController -> DispatcherServlet
프론트 컨트롤러는 URL 호출에 따라 호출해야할 핸들러를 호출한다
handlerMappingMap -> HandlerMapping
HandlerMappingMap에서 호출 가능한 핸들러 목록을 갖는다
(호출 가능한 핸들러 목록이 Map의 형태로 담겨있다고 이해함)
MyHandlerAdapter -> HandlerAdapter
핸들러 어댑터의 존재로 다양한 핸들러를 처리할 수 있다
(지원하는 핸들러, supports()가 참인 경우, 해당 컨트롤러를 호출하여 처리, handle())
handler는 컨트롤러의 포괄적 의미임을 기억하자
이 위 두 항목은 V4에서 추가했다.
ModelView -> ModelAndView
이전에 구현했던 MVC 버전 3(V3)에서 ControllerV3 인터페이스의 process 메서드의 반환형으로 갖는다
이때 컨트롤러에서 모델, 뷰를 이용하여 객체를 생성해, 모델 데이터를 넣어주고
객체 내에 논리 뷰 값을 집어 넣어 Return 해주어야한다.
뷰 네임과 모델을 필드로 갖는다
V3를 참고하자
viewResolver -> ViewResolver
물리뷰를 반환했던 V2와는 다르게 V3에서는 논리이름만 반환해도 prefix와 suffix를 처리해주는
viewResolver가 있었다
V3를 참고하자
MyView -> View
viewResolver 이전에 논리 이름을 담는 객체이다
render 메서드가 구현되어 있으며 RequestDispatcher의 .forward 메서드를 통해 jsp가 렌더링하도록 이동시킨다.
V2 버전을 참고하자
DispatcherServlet의 구조를 살펴보자
스프링 MVC도 이전에 구현했던 MVC 패턴과 동일하게 프론트 컨트롤러 패턴으로 구현되어 있다.
스프링 MVC에서 프론트컨트롤러가 바로 이 DispatcherServlet에 해당한다.
- DispatcherServlet도 부모 클래스에서 HttpServletRequest, Response를 상속 받아서 사용하고 서블릿으로 동작
스프링 부트는 DispatcherServlet을 서블릿으로 자동 등록하면서 모든 경로에 대해서 매핑한다(모든 경로 = urlPatterns = "/")
-> 참고로 다 자세한 경로가 우선순위가 높기 때문에 우리가 기존에 등록한 서블릿들도 (urlPatterns = "/" 하위에 있는것들) 등록된다.
요청 흐름은 다음과 같다.
1) Servlet이 호출되면 HttpServlet이 제공하는 service()가 호출된다.
2) 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드해두었다.
3) FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서
DispatcherServlet.doDispatch()가 호출된다
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
1. Handler를 조회한다
2. 핸들러 어댑터를 조회한다 - 핸들러를 처리할 수 있는 어댑터 (which means, 컨트롤러를 호출할 수 있는 어댑터)
3. 핸들러 어댑터 실행(handle 메서드 실행) -> 4. 핸들러 어댑터를 통해 핸들러(컨트롤러) 실행 (handle 메서드 내부에서 Controller 호출) -> 5. ModelAndView 반환
다음은 V5 버전의 어댑터 구조에서 가져온 코드이다.
package hello.servlet.web.frontcontroller.v5.adapter;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelView modelView = new ModelView(viewName);
modelView.setModel(model);
// model은 어차피 넘어가면 컨트롤러에서 모델에 필요한 데이터 담는다.
return modelView;
}
private static Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator().forEachRemaining(
paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
package hello.servlet.web.frontcontroller.v5;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
// 이전 코드
//private Map<String, ControllerV4> controllerMap = new HashMap<>()
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5(){
// 1. Mapping 정보 주입
initHandlerMappingMap();
// 2. HandlerAdapter 정보 주입
initHandlerAdapters();
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/", new MemberListControllerV3());
// V4 컨트롤러 추가
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/", new MemberListControllerV4());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. handlerMappingMap 꺼내기
Object handler = getHandler(request);
if(handler == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter handlerAdapter = getHandlerAdapter(handler); // 핸들러 어댑터 조회
ModelView mv = handlerAdapter.handle(request, response, handler); // 핸들러 어댑터 실행 (handle 메서드 호출)
String viewName = mv.getViewName();
MyView myView = viewResolver(viewName);
myView.render(mv.getModel(), request, response);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter handlerAdapter : handlerAdapters) { // 핸들러 다 뒤짐
if(handlerAdapter.supports(handler)){ // 서포트 호출 -> V3 핸들러 처리 가능?
return handlerAdapter; // 가능하면 그 어댑터를 반환
}
}
throw new IllegalArgumentException("handler adapter not found");
}
private Object getHandler(HttpServletRequest request){
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
이해를 돕기 위해 이전에 V5 프론트 컨트롤러와 어댑터를 구현하면서 작성한 코드를 추가했다.
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
집중해야 할 부분은 render가 호출된다는 부분이다 -> 뷰 렌더링을 호출한다.
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}
render에서는 ModelAndView에서 view의 논리이름을 꺼내서 뷰 리졸버를 통해 뷰를 찾고, View를 반환한다.
이후 View의 render를 호출할때 모델과 request, response 객체를 넘겨서 뷰를 렌더링하는 과정을 거친다.
코드 길이가 긴데, 어떤 메서드를 호출했는지 작성했는데 추후에 복습하게 된다면 기술한 메서드 중심으로 코드 흐름을 이해하면 되겠다.
이해가 안된다면 V3의 프론트 컨트롤러를 봐가면서 이해하자, 구조는 이와 유사하다.
정리하면, 스프링 MVC의 실행 흐름은 다음과 같다 (V5 프론트 컨트롤러에 작성된 코드를 기준으로 설명해보겠다)
private final Map<String, Object> handlerMappingMap = new HashMap<>(); // 핸들러 목록
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>(); // 어댑터 목록
1) 핸들러 조회 : 핸들러 매핑을 통해 URL에 매핑된 컨트롤러(핸들러)를 조회한다.
private void initHandlerMappingMap() { // 매핑 정보 등록 과정
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/", new MemberListControllerV3());
// V4 컨트롤러 추가
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/", new MemberListControllerV4());
}
Object handler = getHandler(request); // 매핑된 핸들러 조회 및 예외처리 404
if(handler == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
2) 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
MyHandlerAdapter handlerAdapter = getHandlerAdapter(handler); // 핸들러 어댑터 조회
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter handlerAdapter : handlerAdapters) { // 핸들러 다 뒤짐 from handlerAdapters
if(handlerAdapter.supports(handler)){ // 서포트 호출 -> V3 핸들러 처리 가능?
return handlerAdapter; // 가능하면 그 어댑터를 반환
}
}
throw new IllegalArgumentException("handler adapter not found");
}
private void initHandlerAdapters() { // 미리 등록된 핸들러 어댑터 목록에서 조회 (여기서 반복 돌림)
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
요런식으로 supports 메서드를 통해 넘어온 handler가 해당 Controller의 타입과 일치하는 경우 true를 반환한다. (밑의 로직 이용)
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
3) 핸들러 어댑터 실행 - 핸들러 어댑터를 실행한다(handle) / 프론트 컨트롤러에서 .handle 메서드 호출함
@Override // V3
public ModelView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws ServletException, IOException {
// Object로 받는 이유는 유연성을 위함
ControllerV3 controller = (ControllerV3) handler;
// Casting 해도 괜찮음, frontController에서 supports로 검증함.
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv; // 반환 타입을 맞춰서 반환해줘야 함.
// 근데 V4는 논리뷰 이름만 반환해서 거기서는 로직이 달라짐.
}
@Override // V4
public ModelView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelView modelView = new ModelView(viewName);
modelView.setModel(model);
// model은 어차피 넘어가면 컨트롤러에서 모델에 필요한 데이터 담는다.
return modelView;
}
4) 핸들러 실행 : 핸들러 어댑터가 실제 핸들러(컨트롤러)를 실행한다.
위 handle 코드를 보면 Object로 넘어온 handler 객체를 ControllerV3로 type-casting 한 이후 .process() 메서드를 통해 컨트롤러 로직을 실행하는 것을 볼 수 있다. 이 process()를 실행하면 V3 패키지에서 이전에 구성해둔 MemberxxxControllerV3() 컨트롤러가 호출된다.
이때 V3라면 ModelView를 V4라면 논리 뷰 이름만을 반환한다.
여기서 핸들러별로 handle 메서드에서 process에 넘기는 인자와 처리 방식에 차이가 있다
일관되게 V3, V4를 취사선택하여 사용할 수 있도록 하는 로직이 여기에 있다.
5) 결국에는 두 어댑터 모두 ModelAndView를 반환한다(우리가 구현한 것에서는 ModelView, 스프링에서는 ModelAndView이다)
6) viewResolver 호출 - 넘어온 논리뷰 이름을 통해 물리이름으로 바꾸고 렌더링을 담당할 뷰 객체를 호출함
JSP의 경우 - InternalResourceViewResolver가 자동 등록되고 사용된다 (뷰 타입에 따라 달라짐)
타임리프는 자바 코드 내에서 렌더링 진행되어 반환됨
7) View 반환
-> JSP의 경우 InternalResourceView(JstlView)를 반환하는데, 내부에 forward 로직이 있음.
8) 뷰를 통해서 .render 호출
스프링 MVC의 큰 장점은 DispatcherServlet (= 프론트 컨트롤러) 코드의 변경 없이
원하는 기능을 변경하거나 확장할 수 있다는 점이다. 지금까지 설명한 대부분을 확장 가능하게 인터페이스로 제공한다.
이 인터페이스들만 구현해서 DispatcherServlet에 등록하면 사용자 개인만의 컨트롤러를 만들 수도 있다.
주요 인터페이스 목록
핸들러 매핑: org.springframework.web.servlet.HandlerMapping
핸들러 어댑터: org.springframework.web.servlet.HandlerAdapter
뷰 리졸버: org.springframework.web.servlet.ViewResolver
뷰: org.springframework.web.servlet.View
'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 1편' 카테고리의 다른 글
[스프링 웹 MVC 1편] 18. 스프링 MVC - 시작 (0) | 2023.05.30 |
---|---|
[스프링 웹 MVC 1편] 17. 스프링 MVC - 핸들러 매핑과 어댑터 / 뷰 리졸버 (0) | 2023.05.29 |
[스프링 웹 MVC 1편] 15. MVC 프레임워크 제작해보기 - v5 (0) | 2023.05.23 |
[스프링 웹 MVC 1편] 14. MVC 프레임워크 제작해보기 - v4 (0) | 2023.05.23 |
[스프링 웹 MVC 1편] 13. MVC 프레임워크 제작해보기 - v3 (0) | 2023.05.22 |