이전 포스팅에선 컨트롤러에서 view를 분리하는 과정을 다뤘다.
https://healthdevelop.tistory.com/entry/spring50
MyView 클래스를 별도로 생성하여
컨트롤러에서 프론트 컨트롤러로 view 경로가 담긴 문자열을 MyView에 담아 반환시켰다.
MyView 객체를 받은 프론트 컨트롤러는 해당 컨트롤러의 로직을 수행하고(process() 실행)
해당 객체에 view 경로 필드를 바탕으로 render를 시켜준다(render() 실행)
하지만 아직까지는 깔끔하게 코드가 간결화되지 않고 중복되는 부분이 남아있다.
// 서블릿 종속성 => 모든 컨트롤러 process 메서드 매개변수에 servlet이 포함됨
// ex.
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
// 뷰 이름 중복
/WEB-INF/views/new-form.jsp -> new-form
/WEB-INF/views/save-result.jsp -> save-result
/WEB-INF/views/members.jsp -> members
하나의 controller 인터페이스로부터 구현된 모든 컨트롤러는 매개변수로 servlet을 갖고 있다.
이후 나올 내용에선 servlet에 관련된 처리를 frontcontroller에서만 하게끔 코드를 작성하고자 한다.
또한 컨트롤러에서 반환하는 뷰 이름의 중복되는 부분을 제거하고
논리적인 뷰 이름만 반환하게끔 하고자 한다.
● 구조
이전 포스팅과의 차이점은 컨트롤러에서 반환하는 타입과 바로 viewResolver다.
이전에는 controller에선 Myview를 반환했다.(viewPath와 viewPath 정보로 render()를 실행하는 클래스)
지금 우리가 구현할 것은 ModelView라는 클래스를 생성하고
해당 클래스에 필드로 viewName을 담을 String과 serlvet request에 모든 attribute를 담을 Map을 생성한다.
결과적으로 frontcontroller에서 요청 attribute를 모두 controller에 넘겨주고
controller는 필요한 attribute만 가져와 로직을 수행한 후(ModelView를 생성 포함) 모델에 담고
해당 ModelView에 viewpath를 넣어준 뒤 ModelView를 반환한다.
frontcontroller에선 controller로부터 반환받은 ModelView에서 viewpath를 가져와
viewResolver(String viewName)을 수행하여 MyView를 생성하고
해당 MyView 객체에서 render()를 수행할 때 ModelView에 담긴 Model(Map)도 넘겨준다.
직접 설명보단 구현이 이해가 빠르다.
● ModelView 생성
ModelView.
package hello.servlet.web.frontcontroller;
import java.util.HashMap;
import java.util.Map;
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>(); // request.setAttribute 같은 기능
public ModelView(String viewName) {
this.viewName = viewName;
}
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;
}
}
ModelView 클래스의 인스턴스는
1. String -> viewName을 담을 필드
2. Map<String, Object> -> servletRequest의 모든 attribute를 담을 필드
위와 같이 구성되어있다.
Controller
import hello.servlet.web.frontcontroller.ModelView;
import java.util.Map;
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
이전에 작성한 Controller와는 다르게 ModelView를 반환하면서
매개변수로 servlet을 받지 않아도 된다. (render() 부분은 viewResolver가 수행)
MemberSaveController
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 {
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;
}
}
실제 회원 저장 로직을 수행하는 컨트롤러다.
Controller를 구현하여 process()를 작성한다. frontcontroller로부터 받은 paramMap(attribute의 모음)에서 필요한 attribute를 받아와서 member로 넘겨준 후 save()를 수행한다.
ModelView를 생성하여 viewPath와 model을 담아 반환한다.
MyView
package hello.servlet.web.frontcontroller;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
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 {
modelToRequestAttribute(model, request);
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
// 추가
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
}
기존의 render()를 오버로딩하여 매개변수에 model을 추가해준다.
해당 모델은 컨트롤러에서 반환받은 ModelView의 paramMap 객체이다.
FrontController
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 javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
// v1 하위에 어떤 url이 들어와도 일단 이 서블릿이 호출됨
@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
private Map<String, ControllerV3> controllerV3Map = new HashMap<>();
public FrontControllerServletV3() {
// 매개변수 1 url 요청이 오면 , MemberFormControllerV1 생성
controllerV3Map.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
controllerV3Map.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
controllerV3Map.put("/front-controller/v3/members", new MemberListControllerV3());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();// 들어온 URL을 얻을 수 있다.
ControllerV3 controller = controllerV3Map.get(requestURI); // 요청 온 URI에 의해 Controller 객체가 다르게 호출
if(controller == null){ // 읽어온 URI에 매치되는 Controller 객체가 없다면
response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 404 Not Found
return;
}
// paramMap 넘겨주기 ( request 파라미터 다 읽어와 map에 저장)
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName();// 논리 이름 ex. new-form
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private 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;
}
}
이전에 작성된 코드와 크게 다를 건 없다.
단지 map을 생성하여 controller에서 수행하고 반환받은 mv를 토대로
render()를 수행한다.
어느 정도 각 코드마다 역할 분담이 되었지만 더 간결하게 작성할 수 있다.
다음 포스팅에서는 더욱 간결하게 구조를 짜고 코드를 작성하고자 한다.