인프런 김영한 님의 스프링 강의이며, 섹션 4 - MVC 프레임워크 만들기를 정리한다.
이전 시간의 프론트 컨트롤러를 개선하면서 진행한다.
자세한 설명이 궁금하면 수강을 권장한다.
View 분리 - v2
프론트 컨트롤러로 여러 컨트롤러를 제어하지만 뷰로 이동되는 코드가 중복이 된다. 이를 개선해보자.
View를 사용하는 ControllerV2 만들어 보자.
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
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);
}
}
이전에는 ControllerV1의 process는 반환하는 게 없었는데 컨트롤러마다 뷰를 독립하기 위해 변경되었다.
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
private Map<String, ControllerV2> controllerMap = new HashMap<>();
public FrontControllerServletV2() {
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("FrontControllerServletV2.service");
String requestURI = request.getRequestURI();
ControllerV2 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyView view = controller.process(request, response);
view.render(request, response);
}
}
프론트컨트롤러V2도 로직 마지막 부분에 MyView로 forward를 실행하게 처리한다.
public class MemberListControllerV2 implements ControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("members", memberRepository.findAll());
return new MyView("/WEB-INF/views/members.jsp");
}
}
public class MemberSaveControllerV2 implements ControllerV2 {
...
}
public class MemberFormControllerV2 implements ControllerV2 {
...
}
MyView를 반환하게 되어 중복 코드 코드들이 많이 사라졌다.
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
// 아래의 코드가 반환되는 MyView로 대체되었다.
// String viewPath = "/WEB-INF/views/members.jsp";
// RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
// dispatcher.forward(request, response);
return new MyView("/WEB-INF/views/members.jsp");
}
이어서 모델도 도입해보자.
Model 추가 - v3
서블릿 종속성 제거
프론트 컨트롤러가 생기면서 기존의 컨트롤러의 서블릿 의존이 없어도 될 정도가 되었다.
이제 컨트롤러의 서블릿 종속성을 제거해보자.
뷰 이름 중복 제거
컨트롤러에서 지정되는 뷰 이름이 매번 물리적인 경로를 다 넣다 보니 /WEB-INF/views/... 중복이 되고 있다.
프론트 컨트롤러에서 중복 경로를 처리하고, 컨트롤러는 뷰의 논리 이름을 반환하게 단순화를 진행해보자.
기존 컨트롤러의 서블릿을 대체하기 위한 ModelView를 만들어보자 (스프링의 ModelAndView를 떠올리면 된다.)
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
public Map<String, Object> getModel() {
return model;
}
public void setModel(Map<String, Object> model) {
this.model = model;
}
public String getViewName() {
return viewName;
}
}
이 ModelView는 컨트롤러의 서블릿 객체를 대체하는 역할을 한다.
ModelView를 반환하는 ControllerV3 인터페이스를 생성하고 컨트롤러가 상속받게 구현한다
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 MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
ModelView mv = new ModelView("members");
mv.getModel().put("members", memberRepository.findAll());
return mv;
}
}
public class MemberSaveControllerV3 implements ControllerV3 {
...
}
이전과 다르게 컨트롤러에 서블릿 객체가 없어지면서 파라미터로 Map 형식으로 받게 되었다.
현재 구조에 맞게 호출하는 프론트컨트롤러V3를 만들자
@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 {
System.out.println("FrontControllerServletV3.service");
String requestURI = request.getRequestURI();
ControllerV3 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// Map에 담아 컨트롤러에 보내준다
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName(); // 논리이름 new-form, members 등 가져오기
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;
}
}
request의 모든 정보를 map 에 담는 로직이 추가되었고 이를 컨트롤러에 보내게 되었다.
기존의 MyView는 컨트롤러에서 반환하지 않고, 프론트컨트롤러에서 viewResolver로 반환하게 변경했다.
프론트 컨트롤러가 좀 더 복잡해졌지만, 기존의 컨트롤러는 불필요한 중복 코드가 없어져서 많이 깔끔해졌다.
마지막으로 MyView.rander()를 오버 로딩한다.
public class MyView {
...
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));
}
}
컨트롤러가 서블릿에서 벗어나 순수 자바 코드로만 구성되었다.
하지만 더 나아가, 여기서도 불편함이 존재한다. 컨트롤러가 매번 ModelView를 생성해서 반환해야 한다.
다음 시간에는 이를 개선한 v4를 알아보자.
'교육 및 인강 > 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 카테고리의 다른 글
스프링 MVC 이해 - 전체 구조, 핸들러 매핑과 핸들러 어댑터, 뷰 리졸버 (0) | 2021.07.13 |
---|---|
MVC 프레임워크 만들기 - 실용적인 컨트롤러 (v4), 유연한 컨트롤러 (v5) (0) | 2021.07.13 |
MVC 프레임워크 만들기 - 프론트 컨트롤러 패턴 소개, 도입 (0) | 2021.07.05 |
MVC 패턴 - 개요, 적용, 한계 (0) | 2021.06.29 |
서블릿, JSP - 회원 관리 요구사항, 서블릿으로 만들기, JSP로 만들기 (0) | 2021.06.29 |