3️⃣ V3
🤔💭 가독성이 좀 나아진 것 같아. 그런데 viewPath 의 “/WEB-INF/views” 경로는 항상 겹치는데 이걸 공통 로직으로 만들 수 없을까? 그리고 request 대신 다른 Model 을 사용하면 서블릿에 대한 종속성을 없앨 수 있을 것 같은데…
출처: 김영한의 스프링MVC (인프런)
💡 이번에는 각 컨트롤러에서 Model 과 viewPath 를 둘 다 반환하게 할 것이다. 그리고 viewPath 에서 중복되는 부분을 최소화하기 위해 uri 의 논리적 부분(각 uri 를 구분하기 위한 최소의 데이터)만 넘기게 할 것이다 !
⚡️ 3-1. ModelView 와 MyView
Model 과 View 역할을 수행할 클래스를 만들어준다.
Model 역할을 수행할 새로운 HashMap 객체가 생겼으니 V2에서 사용했던 MyView 클래스에 render 함수를 오버로딩 (매개변수에 Map 객체를 추가함) 하여 새로 V3용 render 함수를 만들어준다.
public class ModelView {
//uri 의 논리적 부분을 viewName 으로 저장한다.
private String viewName;
//Model 로 활용하기 위한 HashMap 객체를 만들어준다.
private Map<String, Object> model = new HashMap<>();
//생성자
public ModelView(String viewName) {
this.viewName = viewName;
}
//getter setter
public String getViewName() {
return viewName;
}
public void setViewName(String viewName) {
this.viewName = viewName;
}
public Map<String, Object> getModel() {
return model;
}
public void setModel(Map<String, Object> model) {
this.model = model;
}
}
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
//jsp는 getAttribute로 값을 가져오기 때문에 request 객체로 값을 세팅해줘야한다.
modelToRequestAttribute(model, request);
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
private static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
}
⚡️ 3-2. ControllerV3
- 모델로 request 객체를 사용할 수 없게 되어서 HashMap 으로 paramMap 객체를 생성해서 각 컨트롤러에서 생성되는 파라미터값을 받아오게 만들었다.
public interface ControllerV3 {
//그간 Model 용도로 썼던 request 객체를 대신하기 위해 paramMap 객체를 만들었다.
//process 메서드를 실행하면 ModelView 객체를 반환하도록 짜여짐.
ModelView process(Map<String, String> paramMap);
}
⚡️ 3-3. FrontControllerServletV3
uri 와 컨트롤러를 매핑하는 기능을 제공하는 것에는 변함이 없으며, 각 컨트롤러에서 paramMap, viewPath 정보를 담은 ModelView 객체를 반환하면 render 함수를 실행시켜서 브라우저로 전송해준다.
또한, 논리적 데이터만 담긴 viewPath 앞 뒤로 경로 및 확장자명을 달아주는 viewResolver 메소드를 추가로 달았다. 이렇게 하면 경로의 수정이 생겨도 프론트 컨트롤러의 해당 메서드 값만 변경해주면 되어 유지보수의 편의성이 높아진다.
@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 controller = controllerMap.get(requestURI);
if(controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
//paramMap 을 넘겨줘야 함
//request getparameterNames 로 username, age 를 가져옴.
//iterator로 돌리면서 key에 name, value에 파라미터값을 paramMap에 넣어줌.
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName();//여기에는 논리 이름만 들어있다 (new-form)
//그래서 앞에 경로를 추가해준다. 컴퓨터가 찾아갈 수 있도록 !
MyView view = viewResolver(viewName);
//이번에는 request 객체로 파라미터값을 넘겨줄 수 없어서 Model 객체를 넘겨준다.
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;
}
}
⚡️ 3-4. 구현체 컨트롤러들…
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
public class MemberSaveControllerV3 implements ControllerV3 {
private 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;
}
}
public class MemberListControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
//findAll() 메서드는 Member 객체를 반환하도록 코드를 짰다. (멤버객체의 필드값은 id, username, age 가 있음)
List<Member> members = memberRepository.findAll();
//modelView 객체 mv 를 생성 (생성자로 viewName 필드값에 members 가 들어간다. 이게 jsp경로가 된다.)
ModelView mv = new ModelView("members");
//mv.getModel() : modelView 객체의 필드인 model (Map<String, Object>) 을 받아온다.
//Map 형태의 자료구조이므로 put 메서드로 key값 : members, value값 : members 라는 이름의 List 타입 데이터를 넣는다.
mv.getModel().put("members", members);
//이렇게 되면 mv객체에는 모든 Member 객체가 들어가게 된다. 이것을 리턴한다.
return mv;
}
}
🤔💭 구현체 컨트롤러들의 코드가 단순해져서 좋긴 한데, 항상 ModelView 객체를 생성해야 한다는 점이 조금 불편하네..?