목차
1. 기존 프로젝트의 구조
2. 스프링 MVC 구조
3. DispatcherServlet
1. 기존 프로젝트 구조
여태껏 만든 프로젝트의 구조를 한번 정리해보자.
( 프로젝트는 https://github.com/yeon-06/inflearnSpring/tree/master/mvc1 참조)
- 핸들러 조회: Handler 매핑을 통해 요청 url에 매핑된 핸들러(컨트롤러) 조회
- 핸들러 어댑터 조회: 핸들러를 처리할 수 있는 핸들러 어댑터 조회
- 핸들러 어댑터 실행: handler(handler)
- 핸들러 실행: 핸들러 어댑터가 실제 핸들러 실행
- ModelView 반환: 핸들러 어댑터가 핸들러의 반환 정보를 ModelView 형태로 변환해서 반환
- viewResolver 호출
- MyView 반환: viewResolver가 view의 논리 이름 -> 물리 이름으로 변환 후 렌더링 역할을 하는 MyView 객체 반환
- MyView 렌더링: render(model)
코드와 함께 자세히 보기 ▼
1. 핸들러 조회
(예시 데이터: /front-controller/v5/v4/members/save )
@WebServlet(name = "frontControllerServletV5", urlPatterns="/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet { ... }
- urlPatterns으로 /front-controller/v5/* 요청들은 FrontControllerServletV5로 들어오게 됨
2. 핸들러를 처리할 수 있는 핸들러 어댑터 조회
@Override
protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
Object handler = getHandler(req);
if(handler == null) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND); //404
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(req, res, handler);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), req, res);
}
private Object getHandler(HttpServletRequest req) {
String reqURI = req.getRequestURI();
return handlerMappingMap.get(reqURI);
}
- FrontControllerServletV5의 service에서 getHandler(req) 실행
- handlerMappingMap에 있는 {"/front-controller/v5/v4/members/save", new MemberSaveControllerV4()} 데이터를 통해 MemberSaveControllerV4 가져옴
3. handler(handler)
@Override
public ModelView handle(HttpServletRequest req, HttpServletResponse res, Object handler) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(req);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
- 2에서 찾은 핸들러를 ControllerV4HandlerAdapter의 handler() 함수에 파라미터로 넣고 실행
- ControllerV4 controller에는 MemberSaveControllerV4가 들어가게 됨
4. 핸들러 호출
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
model.put("member",member);
return "save-result";
}
- 3의 ControllerV4 controller 이용
- controller에 저장된 MemberListControllerV4의 process 함수 실행
5. ModelView 반환
@Override
public ModelView handle(HttpServletRequest req, HttpServletResponse res, Object handler) throws ServletException, IOException {
...
String viewName = controller.process(paramMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
- ControllerV4HandlerAdapter에서 3의 결과를 통해 viewName을 받아옴 -> "save-result"
- 이를 이용해 ModelView 생성하고 반환
6. viewResolver 호출
@Override
protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
...
ModelView mv = adapter.handle(req, res, handler);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), req, res);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
- FrontControllerServletV5에 ModelView가 저장됨
- ModelView에서 "save-result"(viewName)을 가져오고 이를 이용해 viewResolver를 호출
7. MyView 반환
- viewResolver에서 경로를 새로 정의해 MyView를 반환 -> viewName이 "/WEB-INF/view/save-result.jsp"인 객체
8. render(model) 호출
public void render(Map<String, Object> model, HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
modelToRequestAttribute(model, req);
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, res);
}
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest req) {
model.forEach((key,value)-> req.setAttribute(key, value));
}
- MyView의 render() 실행
- model의 key, value를 HttpServletRequest의 key, value에 저장
- RequestDispatcher 객체 생성 시 이동할 페이지 지정 -> "/WEB-INF/view/save-result.jsp"
- forward()를 통해 req, res를 넘겨주며 페이지 이동
2. 스프링 MVC 구조
- 핸들러 조회: Handler 매핑을 통해 요청 url에 매핑된 핸들러(컨트롤러) 조회
- 핸들러 어댑터 조회: 핸들러를 처리할 수 있는 핸들러 어댑터 조회
- 핸들러 어댑터 실행
- 핸들러 실행: 핸들러 어댑터가 실제 핸들러 실행
- ModelAndView 반환: 핸들러 어댑터가 핸들러의 반환 정보를 ModelAndView 형태로 변환해서 반환
- viewResolver 호출: jsp의 경우 InternalResourceviewResolver가 자동 등록 및 사용됨
- View 반환: viewResolver가 view의 논리 이름 -> 물리 이름으로 변환 후 렌더링 역할을 하는 View 객체 반환
- View 렌더링: View를 이용해 렌더링
거의 동일하다!!!
약간의 용어들만이 바뀌었을 뿐이다
직접 만든 MVC | 스프링 MVC | |
FrontController | ☞ | DispatcherServlet |
handlerMappingMap | ☞ | HandlerMapping |
MyHandlerAdapter | ☞ | HandlerAdapter |
ModelVeiw | ☞ | ModelAndView |
viewResolver | ☞ | ViewResolver |
MyView | ☞ | View |
이제 Front Controller인 DispatcherServlet에 대해 자세히 살펴보자
DispatcherServlet
- 요청을 처리하고 적절한 응답을 렌더링하기 위해 Special Bean
- DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet 상속받고 있음
- Spring boot에서는 DispatcherServlet을 자동으로 urlPattern="/"으로 등록
urlPattern="/"으로 등록해도 문제되지 않는 이유 ▼
urlPattern을 전체로 등록하면 이런 의문이 생길수도 있다.
어떤 경로로 접근해도 DispatcherServlet으로 들어가는건 아닐까?
하지만 경로에도 우선 순위가 존재하기 때문에 괜찮다.
더 자세한 경로일수록 우선 순위가 더 높다.
A 서블릿을 urlPattern="/hello/*"
B 서블릿을 urlPattern="/hello/test/*"라고 등록했다고 가정하자.
만약 "/hello/test/test2" 경로로 접근한다면 B 서블릿의 경로가 더 구체적이니 B 서블릿으로 접근하게 된다.
요청의 흐름
1. 서블릿 호출 시 HttpServlet이 제공하는 service() 호출
2. FrameworkServlet에서 오버라이드한 service() 호출
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
doDispatch(request, response);
...
}
3. 2가 실행되며 DispatcherServlet.doDispatch() 호출
(전체 코드가 아닌 주요 코드들만 뽑아낸 것)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) 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);
}
본 게시글은 김영한 님의 '스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 강의를 구매 후 정리하기 위한 포스팅입니다.
내용을 임의로 추가, 수정, 삭제한 부분이 많으며 정확한 이해를 위해서 강의를 구매하시는 것을 추천 드립니다.
https://docs.spring.io/spring-framework/docs/5.1.9.RELEASE/spring-framework-reference/web.html#mvc
'Develop > Spring+JPA' 카테고리의 다른 글
[스프링 MVC] 뷰 리졸버 (0) | 2021.05.26 |
---|---|
[스프링 MVC] 핸들러 매핑, 핸들러 어댑터 (0) | 2021.05.24 |
[Spring] Path with "WEB-INF" or "META-INF" 에러 (2) | 2021.05.21 |
[Spring] Initializr로 스프링 부트 기반 프로젝트 생성 (0) | 2021.03.10 |
[Spring] @JsonProperty (2) | 2021.03.08 |