이전 과정에서는 RequestDispatcher와 viewName 그리고 forward 메서드를 MyView 클래스와 render 메서드를 통해 리팩터링함으로써 중복을 제거하였다.
하지만 여전히 코드를 보면 물리 저장 Path의 중복이 반복되고 있고
컨트롤러 입장에서는 사실 HttpServletRequest, HttpServletResponse가 굳이? 필요하지 않을 수도 있다
(서블릿에 종속적이지 않게 설계, 즉 서블릿 기술을 몰라도 동작할 수 있다)
-> 이렇게 하면 테스트도 간편해지고 간결해지는 효과를 낼 수 있다
request 객체를 Model로 사용하는 대신 별도의 Model 객체를 만들어서 반환하는 방법이 해결책이 된다!
그리고 컨트롤러가 뷰의 논리 이름을 반환하고 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화하면
나중에 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다는 장점이 있다
-> 이런 설계가 좋은 설계인것이 변경 포인트를 하나로 만들면 수정이 쉬워진다!
V3 설계도는 다음과 같다 V2와 비교해보자
차이가 보이는가? 크게 보면 viewResolver가 추가되었고 컨트롤러는 ModelView를 반환한다
컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용한것과 request.setAttribute를 이용해 Model을 사용한 V2버전과는 차이가 있다.
ModelView 클래스를 다음과 같이 구현한다.
package hello.servlet.web.frontcontroller;
// 스프링에는 ModelAndView가 있다!
import java.util.HashMap;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
뷰의 이름과 뷰를 렌더링할때 필요한 Model 객체를 갖고 있다.
Model은 단순히 Map으로 되어 있기 때문에 컨트롤러에서 뷰에 필요한 데이터를 key, value 형태로 넣어주면 된다.
이전과 같게 ControllerV3 컨트롤러 인터페이스를 구현하겠다
package hello.servlet.web.frontcontroller.v3;
import hello.servlet.web.frontcontroller.ModelView;
import java.util.Map;
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
/*
MyView process(
HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
V2와 비교했을 때 서블릿 기술에 종속적인것과 다르게 프레임워크에만 종속적임
*/
}
현재 구현을 보면 알겠지만 process는 서블릿 기술을 사용하지 않고 있다.
그 대신 paramMap을 통해 프론트 컨트롤러가 파라미터를 담아서 호출해줘야한다.
응답 결과로 뷰 이름과 뷰에 전달할 모델 데이터를 포함한 ModelView를 반환값으로 설정했다.
(1) MemberFormControllerV3 구현
package hello.servlet.web.frontcontroller.v3.controller;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import java.util.Map;
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
지금은 단순히 ModelView에 new-form 논리 이름만을 반환하고 있다.
실제 물리적인 이름은 프론트 컨트롤러에서 처리하게 된다.
(2) MemberSaveControllerV3 구현
package hello.servlet.web.frontcontroller.v3.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import java.util.Map;
public class MemberSaveControllerV3 implements ControllerV3 {
MemberRepository memberRepository =MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelView mv = new ModelView("save-result");
mv.getModel().put("member", member);
return mv;
}
}
1) paramMap에서 username을 꺼냄
2) paramMap에서 age를 꺼내서 parseInt를 통해 int형 데이터로 변환함
이 paramMap의 경우 FrontControllerServletV3에서 주어진다.
mv.getModel().put("member", member) -> model이 단순한 Map이기 때문에 뷰에서 필요한 member 객체를 담는다.
중간에 갑자기 의문이 든 내용이 있어서 첨부한다
request.setAttribute 관련하여 질문 드립니다. - 인프런 | 질문 & 답변
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.1. 강의 내용과 관련된 질문을 남겨주세요.2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼
www.inflearn.com
Response에 .setAttribute가 있어야 할 것 같은데 왜 Request에 있는지에 대한 답변이 들어있다.
(3) MemberListControllerV3 구현
package hello.servlet.web.frontcontroller.v3.controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import java.util.List;
import java.util.Map;
public class MemberListControllerV3 implements ControllerV3 {
MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
List<Member> members = memberRepository.findAll();
ModelView mv = new ModelView("members");
mv.getModel().put("members", members);
return mv;
}
}
마지막으로 대망의 프론트 컨트롤러를 보자
이전보다 코드가 꽤 늘었는데, 당황하지 않고 하나씩 살펴보았다.
package hello.servlet.web.frontcontroller.v3;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
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 java.io.IOException;
import java.util.HashMap;
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 = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
private Map<String, ControllerV3> controllerMap = new HashMap<>();
public FrontControllerServletV3() {
controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
controllerMap.put("/front-controller/v3/members/", new MemberListControllerV3());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV3 controllerV3 = controllerMap.get(requestURI);
if (controllerV3 == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controllerV3.process(paramMap);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
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;
}
}
먼저 프론트 컨트롤러로 요청이 들어오기 때문에 @WebServlet과 HttpServlet을 상속한 FrontControllerServletV3 객체를 생성
이전과 동일하게 /v3/~로 들어오는 모든 경우의 수(경로들을)들을 생성자를 통해 controllerMap에 등록해주었다.
어떤 것을 할 것인가? 를 service에 명시했다
1) V1과 V2에서 했던것처럼 URI를 받아와서 controllerMap에 그에 대응하는 객체 인스턴스(MemberFormControllerV3, ...중 하나)
를 생성했다
2) controllerMap에 대응하는 생성할 객체가 없는 경우 response 객체의 setStatus를 404로 설정하는 조건문을 작성했다
3) 조건문 충족이 안되는 경우 컨트롤러 객체가 생성되었음을 의미하는데
V2에서 컨트롤러에 서블릿 기술을 의존하게 만든것과 달리, 이제 프론트 컨트롤러에서 컨트롤러에 필요한 정보를 담아 전달하기 때문에
paramMap이라는 Map 객체를 생성하고 request로부터 parameter들을 파싱한다.
그에 관한 작업들을 Method Extraction한 결과가 createParamMap 메소드에 해당한다.
getParameterNames를 asIterator, forEachRemaining을 통해 paramMap에 넣는 과정에 해당한다.
4) createParam으로부터 반환된 paramMap을 인수로 controllerV3(controllerMap에서 호출된 컨트롤러)의 process를 호출한다
-> 이 process를 통해 호출된 컨트롤러에서 작업이 일어나고 ModelView 타입 객체 인스턴스를 반환한다
ModelView가 내포하는 내용 -> (렌더링에 필요한 정보와, 논리 뷰 이름)
5) 4)에서 받은 정보가 mv 객체 변수에 들어있다.
이 객체 변수에 들어있는 렌더링에 필요한 모델정보와 논리뷰 이름중 논리 뷰 이름을 꺼내서 (mv.getViewName) view 변수를 만든다.
그리고 view의 render 메서드를 통해 모델과 request, response 객체를 전달함으로써 뷰 rendering을 시작하는데
이때 view를 생성하는 과정에서 viewResolver는 논리뷰 이름에 따른 절대경로를 생성하는 역할을 맡기 때문에
결론적으로 컨트롤러에서 모델뷰 반환 -> 모델뷰에서 논리 뷰 네임만 String으로 꺼냄 -> 그걸 viewResolver에 인수로 주면 절대 경로를 포함한 MyView 객체를 뱉어냄 -> MyView 객체의 render에 모델과 HttpServletRequest, Response 타입 객체 request, response를 전달함 -> requestDispatcher -> forward를 통해 jsp로 이동.
이런 긴 과정을 통해 뷰가 렌더링 된다
여기서 이전 V2 과정에서 설명 생략했던 부분에 대해 이야기한다.
package hello.servlet.web.frontcontroller;
import java.io.IOException;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
// v2 이전에 설명했다
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
requestDispatcher.forward(request, response); // 이제 MyView가 forward 처리 (JSP 처리)
}
// V3, 인수에 모델이 추가되었다
public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
modelToRequestAttribute(model, request);
RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
requestDispatcher.forward(request, response);
}
private static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
}
Map으로 전달된 model의 각 요소들을 request.setAttribute에 인수로 주어 모든 요소를 setAttribute 적용한다.
-> Model 정보를 View에 전달하기 위한 과정이 여기서 일어나는 것
(뷰를 만들어 내기 위해 필요한 정보와, RequestDispatcher를 통한 forwarding이 여기서 일어남)
'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 1편' 카테고리의 다른 글
[스프링 웹 MVC 1편] 15. MVC 프레임워크 제작해보기 - v5 (0) | 2023.05.23 |
---|---|
[스프링 웹 MVC 1편] 14. MVC 프레임워크 제작해보기 - v4 (0) | 2023.05.23 |
[스프링 웹 MVC 1편] 12. MVC 프레임워크 제작해보기 - v2 (0) | 2023.05.22 |
[스프링 웹 MVC 1편] 11. MVC 프레임워크 제작해보기 - v1 (0) | 2023.05.22 |
[스프링 웹 MVC 1편] 10. MVC 패턴 - 한계 (0) | 2023.05.22 |