들어가며
- 저번 포스트에서는 프론트 컨트롤러를 도입하여 구현한 서블릿 구조를 보았다.
- 이번 포스트에서는 각 컨트롤러에 묶여있는 뷰들을 어떻게 효과적으로 빼낼지에 대해 알아본다.
- 모든 코드는 깃허브 링크에 올려저 있다.
View의 분리
- 지금까지 구현한 구조는 컨트롤러가 JSP로 포워딩하기 위해 JSP파일의 물리적 경로를 가지고 있었어야 했다.
- 단적으로 다음의 코드를 보자.
public class MemberFormControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
request.getRequestDispatcher(viewPath).forward(request, response);
}
}
- 보면 viewPath라는 변수가 우리가 표현할 View의 물리적 경로를 명시해두고 있고, 이를 이용하여 foward를 수행하고 있다.
- 해당 부분이 모든 컨트롤러에 분산되어 있다. 이렇게 분산되어 있는 코드를 하나의 View 객체를 통하여 관리하도록 해보자. 이를 위해서는 새로운 View 클래스가 필요하다.
public class MyView{
String viewPath;
MyView(String viewPath){
this.viewPath = viewPath;
}
public void render(
HttpServletRequest request, HttpServletResponse response
) throws ServletException, IOException{
request.getRequestDispatcher(viewPath).foward(request, response)
}
}
- 코드를 보면 위의 코드를 클래스로 구현한 것에 불과하다는 것을 알 것이다.
- viewPath를 통해 객체를 생성하여 해당 객체가 뷰를 렌더링하여 반환하도록 하였다. 즉 이 MyView 객체는 jsp 뷰를 랜더링하여 반환하는 책임을 가진다.
- 이제 이 MyView를 이용하여 컨트롤러들을 구현해보자.
public interface ControllerV2{
void process(
HttpServletRequest request,
HttpServletResponse response
) throws ServletException, IOException;
}
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 MemberListControllerV2 implements ControllerV2 {
private static final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
return new MyView("/WEB-INF/views/members.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");
}
}
- request객체를 활용하여 뷰로 포워딩하는 부분을 삭제하고 대신 MyView 객체를 반환하도록 하였다.
- 각 객체들은 자신들이 랜더링해야할 뷰의 물리적 경로를 가지고 있다. 이를 랜더링하는 책임은 바로 자신들을 호출하는 프론트 컨트롤러에서 맡아야 한다.
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV1 extends HttpServlet {
private final Map<String, ControllerV2> controllerMap = new HashMap<>();
public FrontControllerServletV1(){
controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerV1.service");
String requestURI = request.getRequestURI();
System.out.println(requestURI);
ControllerV2 controllerV2 = controllerMap.get(requestURI);
if (controllerV2 == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyView myView = controllerV2.process(request, response);
myView.render(request, response);
}
}
- 프론트 컨트롤러에서 이제 view를 랜더링한다. 이제 각 컨트롤러들이 뷰를 랜더링 할 필요가 없다. 그져 뷰 객체에 랜더링해야할 jsp의 물리적 경로만 넣어 반환하면 된다.
MyView로 변경된 점
- 원래 뷰로 포워딩 하는 것은 각 컨트롤러들이 책임지던 것이었다. 그로 인해 각 컨트롤러마다 자신이 포워딩해야할 뷰의 물리적 경로를 가지고 있어야 했고 포워딩하는 함수도 호출하는 중복이 발생했다.
- 이번 과정을 통해서 MyView라는 객체에 포워딩하는 책임을 넘기고, 컨트롤러는 MyView 객체만을 생성하여 반환하였다.
- 이렇게 반환받은 MyView 객체는 프론트 컨트롤러가 해당 객체의 render() 메서드를 통해 보여줄 뷰로 포워딩하도록 요청하고, 각 MyView 객체들이 해당 요청을 받고 포워딩하는 책임(기능)을 수행한다.
개선해야 할 점
- 각 뷰로 포워딩 하는 기능들을 제거하고 MyView에서 처리하도록 책임을 옮겼지만 아직 부족하다. 위의 코드를 보면 뷰의 위치는 항상 "/WEB-INF/views/something.jsp"로 앞의 "/WEB-INF/views"와 .jsp는 항상 동일하단 것을 알 수 있다.
- 그렇다면 굳이 매번 번거롭게 타이핑할 필요 없이 해당 스트링을 미리 만들어 두고 컨트롤러는 뷰의 논리적 이름만 반환하도록 하면 조금 더 편리해질 것 같다.
public class MemberListControllerV2 implements ControllerV2 {
private static final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
return new MyView("/WEB-INF/views/members.jsp");
}
}
- 그리고 이 코드를 보면 컨트롤러는 request, response를 받고 있는 모습인데 막상 해당 스펙들을 활용하는 코드는 단 한줄에 불과하다.
- 심지어 다른 페이지로 넘어가는 컨트롤러의 경우 MyView로 인하여 request.getRequestDispatcher()가 호출되지도 않기 때문에 해당 스펙이 아예 사용되지도 않는다.
- 위의 코드도 뷰에 전달할 모델(Model)의 역할로 request를 사용하고 있기 때문에 사용한다. 따라서 적절한 모델을 제공하도록 한다면 request, response가 아예 필요하지 않게 된다.
- 다음 포스트에서 해당 개선 사항들을 고쳐보도록 한다.
'Spring & JPA > SpringMVC' 카테고리의 다른 글
Spring MVC - MVC 구조 - 6. 개선된 MVC 구조 (0) | 2023.06.01 |
---|---|
Spring MVC - MVC 구조 - 5. Model의 추가 (0) | 2023.06.01 |
Spring MVC - MVC 구조 - 3. 프론트 컨트롤러 구현(2) (0) | 2023.05.30 |
Spring MVC - MVC 구조 - 2. 프론트 컨트롤러 구현(1) (0) | 2023.05.30 |
Spring MVC - MVC 구조 - 1. 프론트 컨트롤러 (0) | 2023.05.30 |