들어가며
- 저번 포스트에서는 View를 컨트롤러에서 분리하는 작업을 수행하였다.
- 이번에는 서블릿에 종속적인 코드들을 걷어내고 그 이후에 새로운 Model이라는 컴포넌트를 도입하여 어떻게 변화되는지를 알아본다.
- 모델을 추가한 전체적인 코드는 여기에서 확인할 수 있다.
개선 방향
서블릿 종속성 제거
- 다음의 코드를 보자
public class MemberFormControllerV2 implements ControllerV2 {
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
return new MyView("/WEB-INF/views/new-form.jsp");
}
}
public class MemberSaveControllerV2 implements ControllerV2 {
private static final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String userName = request.getParameter("username");
int userAge = Integer.parseInt(request.getParameter("age"));
Member member = new Member(userName, userAge);
memberRepository.save(member);
request.setAttribute("member", member);
return new MyView("/WEB-INF/views/save-result.jsp");
}
}
- 이 코드들은 저번 포스트에서 구현했던 버전 2 컨트롤러이다.
- 그런데 잘 보면 내부의 어디에도 request, response를 찾아볼 수 없다. 저장에도 request를 뷰에 전달하기 위한 모델의 역할로 사용하고 있다.
- 이 말은 적절한 Model만 구현하여 파라미터로 넣어준다면 서블릿 종속성을 제거할 수 있다는 것이다,
View 이름 중복 제거
- 모든 컨트롤러의 MyView 생성을 보면 "/WEB-INF/views"와 ".jsp"가 중복되어 나타난다.
- 중복되어 나타나는 것은 하나로 뽑아내어 제거하는것이 효율이 좋기 때문에 중복된 것들을 지우고, 컨트롤러는 오직 중간에 들어가는 뷰의 논리적 이름만 반환하도록 한다.
- 해당 방식을 적용하면 중복되는 스트링이 제거되어 불필요한 타이핑을 줄일 수 있다는 것 이외에도, 뷰의 전체적인 경로가 재설정되어도 해당 부분만 다시 적어두면 된다는 것이다. 즉 변경하는 곳들이 적어진다.
개선된 구조
- 이전과는 조금 달라진 점들이 보인다.
- 바로 ModelView와 ViewResolver의 존재이다.
- ModelView는 말 그대로 Model과 View를 가지고 있는 객체이다. 이때 View는 컨트롤러에서 반환한 뷰의 논리적 이름으로 String 타입이 될 것이다.
- ViewResolver는 위에서 설명한 중복된 경로 스트링과 ModelView에서 넘어오는 View의 논리적 이름을 조합하여 새로운 MyView 객체를 반환하는 책임을 가진다.
- 프론트 컨트롤러는 View Resolver에서 반환받은 MyView를 랜더링하여 JSP를 랜더링하고 이를 Http 응답으로 반환하게 된다.
구현
ModelView 구현
- 먼저 위 구조의 핵심적인 역할을 맡을 ModelView 객체부터 구현하도록 한다.
@Getter
@Setter
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
- 위에서 말한 것 처럼 컨트롤러에서 넘어오는 뷰의 논리적 이름을 viewName으로 가지고, 뷰에 넘겨줄 데이터들을 가지고 있는 Model을 가진다.
- 모델은 key, value로 이루어지도록 Map을 사용하여 구현하였고 value에는 여러가지 값이 들어갈 수 있기 때문에 Object로 구현하였다.
- 이제 각 컨트롤러들은 이 ModelView를 반환하도록 하면 된다.
Controller 구현
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
public class MemberListControllerV3 implements ControllerV3 {
private static final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
List<Member> members = memberRepository.findAll();
ModelView modelView = new ModelView("members");
modelView.getModel().put("members", members);
return modelView;
}
}
public class MemberSaveControllerV3 implements ControllerV3 {
private static final 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 = memberRepository.save(new Member(userName, age));
ModelView modelView = new ModelView("save-result");
modelView.getModel().put("member", member);
return modelView;
}
}
- paramMap은 요청에서 넘어오는 데이터들로, 프론트 컨트롤러에서 한번 가공되어 넘어온다. 각 컨트롤러는 이 paramMap에서 필요한 데이터를 조회하여 가져다 사용하기만 하면 된다.
- Model은 modelView에 있는 모델을 사용한다.
- 수정된 코드가 매우 깔끔하다고 생각할 수 있다. 이유는 사용하지 않는 서블릿 관련 코드들을 들어내면서 함수 파라미터 부분이 깔끔하게 되었기 때문이다.
FrontController 구현
@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
private final 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 modelView = controllerV3.process(paramMap);
MyView view = viewResolver(modelView.getViewName());
view.render(modelView.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;
}
}
- 프론트 컨트롤러에 새로 구현된 것들이 많은데 하나씩 살펴보도록 하자
- 먼저 paramMap은 위에서 설명한 것 처럼 요청에서 넘어오는 데이터들을 모두 가지고 있는 일종의 모델이다.
- 이 paramMap을 초기화 시키는 행동 자체가 꽤 큰 행동이기 때문에 이를 별개의 메서드로 빼내어 인식하기 쉽도록 하였다.
- 그리고 새로 보이는게 viewResolver가 있는데 위의 구조 단에서 설명했던 그 ViewResolver이다. 보면 컨트롤러를 호출하고 얻은 ModelView에서 View의 논리적 이름을 가져와 MyView로 만들어 반환하는 것을 볼 수 있다.
- view의 render가 이전과는 조금 다른 render() 메서드가 있는데 코드를 보면서 확인해보자.
View 변경점
public class MyView {
...
public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
modelToRequestAttribute(model, request);
request.getRequestDispatcher(viewPath).forward(request, response);
}
private static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach(request::setAttribute);
}
}
- 보면 request에 setAttribute를 하는 동작을 먼저 수행한다. 이는 우리가 ModelView에서 받은 Model을 그대로 뷰에 넘겨줄 수 없기 때문이다.
- 따라서 request에 Model의 데이터들을 하나씩 매핑해주고 foward를 통해 응답을 보내준다.
결과
- 개발자는 이제 각 컨트롤러를 구현할 때 마다 서블릿 관련 코드들을 삽입해야할 필요성이 사라졌다.
- 이제 컨트롤러를 개발하는 개발자는 ModelView를 만들어 그 내부에 자신이 반환해야 할 View의 논리적 이름과 View에 넘겨줄 모델만 넣어주고 반환하면 된다. 기존에 request에 하나씩 데이터를 넣는 작업이나, 뷰의 중복된 이름을 적는 불편함이 사라진 것이다.
- 다음 포스트에서는 약간의 접근 방식을 바꾸어 더 편리하게 컨트롤러를 만들 수 있도록 해볼 것이다.
'Spring & JPA > SpringMVC' 카테고리의 다른 글
Spring MVC - MVC 구조 - 7. 확장성 적용(1) (0) | 2023.06.02 |
---|---|
Spring MVC - MVC 구조 - 6. 개선된 MVC 구조 (0) | 2023.06.01 |
Spring MVC - MVC 구조 - 4. View의 분리 (1) | 2023.05.31 |
Spring MVC - MVC 구조 - 3. 프론트 컨트롤러 구현(2) (0) | 2023.05.30 |
Spring MVC - MVC 구조 - 2. 프론트 컨트롤러 구현(1) (0) | 2023.05.30 |