스프링 MVC 밑바닥부터 만들어보기5 - 서블릿 종속성 제거, Model분리
이번에는 서블릿 종속성을 제거해보자. 지금은 컨트롤러들이 모두 request와 response를 강제로 받아서 사용하고있다. 꼭 사용하지 않는 컨트롤러도 있고 심지어 response는 거의 사용하지 않는데도 불구하고 서블릿에 종속되있어서 어쩔 수 없이 파라미터를 넘기고 받아서 처리하고있다. 이 부분을 수정해서 request, response가 아니라 http요청데이터에서 넘어온 파라미터를 담은 map을 컨트롤러가 받아와서 처리하도록 변경할 것이다.
그리고 view의 호출방식도 변경할 것이다. 각 컨트롤러에서 jsp의 주소를 모두 넘겨주고 있는데 이렇게 됐을 때 만약 jsp의 패키지 주소가 변경되었을 경우 모든 컨트롤러를 수정해야하는 일이 발생한다. 그렇기 때문에 컨트롤러에서는 논리 뷰 이름만 넘겨주고(jsp파일의 아름) ModelView클래스를 만들어서 여기서 물리경로를 넘겨주도록 수정하도록 하겠다. ModelView클래스에 Model도 함께 담을 것이다.
전체적인 요청의 흐름을 살펴보자면
앞에서만든 방식에서 조금씩 변경되었다.
- 컨트롤러가 반환하는 타입이 변경됨
- viewResolver의 역할을 하는 ModelView클래스가 추가됨
- 사진에서는 보이지 않지만 MyView로 전달하는 파라미터가 추가됨
ControllerV3
그럼 먼저 ModelView클래스를 만들어주자.
public class ModelView {
private String ViewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewPath) {
ViewName = viewPath;
}
public String getViewName() {
return ViewName;
}
public void setViewName(String viewName) {
ViewName = viewName;
}
public Map<String, Object> getModel() {
return model;
}
public void setModel(Map<String, Object> model) {
this.model = model;
}
}
ModelView에서는 논리 뷰 이름과 컨트롤러에서 view로 넘길 model을 받아서 저장한다.
이제 컨트롤러 인터페이스를 작성하자.
public interface ControllerV3 {
public ModelView process(Map<String, String> paramMap);
}
기존에 서블릿에 종속되었던것과는 달리 파라미터를 paramMap하나만 받고있다. 프론트 컨트롤러가 만들어서 보낼 파라미터의 값들을 map으로 받아서 처리할 에정이다. 반환타입은 ModelView이다.
이제 컨트롤러 3개를 만들자
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 = memberRepository.save(new Member(username, age));
ModelView mv = new ModelView("save");
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) {
List<Member> members = memberRepository.findAll();
ModelView mv = new ModelView("members");
mv.getModel().put("members", members);
return mv;
}
}
더이상 request와 response는 받을 필요가 없어졌다. 프론트 컨트롤러에서 가공해서 보내주는 파라미터를 받아서 사용할 수 있다.
컨트롤러 내부에서 ModelView를 생성해서 논리 뷰 이름과 비즈니스 로직을 처리하고 넘겨줄 데이터를 담아서 반환한다.
다음은 프론트 컨트롤러이다.
@WebServlet(name = "frontControllerServiceV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServiceV3 extends HttpServlet {
private Map<String, ControllerV3> controllerMap = new HashMap<>();
public FrontControllerServiceV3() {
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;
}
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
MyView view = getViewName(mv.getViewName());
view.render(mv.getModel(), request, response);
}
private MyView getViewName(String viewPath) {
MyView view = new MyView("/WEB-INF/views/" + viewPath + ".jsp");
return view;
}
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;
}
}
이전과 다르게 코드의 양이 많아진 것을 알 수 있다. 컨트롤러가 간소회되면서 프론트 컨트롤러가 하는 역할이 조금씩 많아지기 시작했다.
- createParamMap : 컨트롤러에 넘겨줄 데이터를 저장한다. request에 들어온 모든 파라미터들을 map에 담아준다.
- getViewName : 컨트롤러에서 받아온 논리 뷰 이름과 실제 파일이 있는 물리 주소를 합쳐서 view랜더링을 할 주소를 만들어낸다.
이전과는 다르게 MyView에 랜더링을 위해 파라미터를 보낼 때 paramMap을 함께 보내야한다. 이전에는 request를 model로서 활요했기 때문에 request에 데이터가 들어있었지만 이제는 paramMap으로 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 {
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));
}
}
model이 추가된 render를 하나 더 만들고 model에 담겨서 넘어온 데이터들을 request에 다시 담아서 forward를 해주면 된다.
점점 역할이 하나씩 나눠지면서 더 편리하게 사용하기 시작했다. 여기까지 이해하고 만들었다면 이제 SpringMVC의 구조를 이해했다고해도 과언이 아니다. 하지만 아직 조금 남았다.
지금 사용하는 방식은 결국 컨트롤러가 일일이 ModelView를 만들어서 반환해줘야 한다. 이런 방식도 좋지만 조금 더 단순하고 실용적이게 사용 할 수 있을 것 같다. 아키텍쳐는 잘 짜여진 것도 좋지만 무엇보다 개발자가 사용하기 편리하게 만들어져야 할 것이다. Spring을 생각해보면 굉장히 복잡하고 잘 짜여져있지만 개발자가 사용하기에 너무나도 편리하게 만들어져 있다.
이제 편리하고 실용적이게 한번 더 진화시켜보자.
ControllerV4
V3까지는 컨트롤러에서 ModelView를 만들어서 반환해줘야 했다. 이제는 정말 Spring에서 사용하는 것처럼 model을 가져와서 넣고 반환은 논리 뷰이름을 반환하기만 하면 되는 구조로 변경시켜보자.
요청의 흐름은 V3와 동일하다.
이번에는 컨트롤러와 프론트 컨트롤러만 변경하면 된다. 먼저 컨트롤러의 구조를 바꾸자.
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
model을 추가해서 데이터를 담을 수 있도록하고 반환 타입은 String으로 해서 논리 뷰 이름을 반환할 수 있도록 한다.
public class MemberFormControllerV4 implements ControllerV4 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "new-form";
}
}
public class MemberSaveControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
model.put("member", member);
return "save";
}
}
public class MemberListControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
List<Member> members = memberRepository.findAll();
model.put("members", members);
return "members";
}
}
이제 모든 컨트롤러에서 ModelView를 만들어서 반환하는 것이 아니라 내보낼 데이터는 model에 담았고 랜더링하고자 하는 논리 뷰 이름만 반환하고 있다.
@WebServlet(name = "frontControllerServiceV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServiceV4 extends HttpServlet {
private Map<String, ControllerV4> controllerMap = new HashMap<>();
public FrontControllerServiceV4() {
controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV4 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
// 변경된 부분 //
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
MyView view = getViewName(viewName);
///////////////
view.render(model, request, response);
}
private MyView getViewName(String viewPath) {
MyView view = new MyView("/WEB-INF/views/" + viewPath + ".jsp");
return view;
}
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;
}
}
변경된 부분이 적어서 따로 표기를 했다.
컨트롤러에 넘겨줄 model을 만들고 넘겨준다. 이 model은 당연하게도 빈 값이다.
컨트롤러에게 받은 논리 뷰 주소와 viewResolver를 이용해서 물리 주소를 만들어서 랜더링을 하고있다.
이렇게 함으로서 개발자는 가장 편리한 방법으로 사용할 수 있게 되었다.
컨트롤러에 작성하는 코드는 더이상 군더더기없이 필요한 내용만 작성하고 필요한 것만 내보낼 수 있는 상태가 되었다.
이제 SpringMVC를 이해해가 위한 마지막 순서로 어댑터만 남았다. 다음에는 어댑터를 구현함으로서 마무리를 해보고자 한다.
모든 내용은 인프런의 김영한님의 강의를 참고하여 작성되었습니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의
웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -
www.inflearn.com
사용된 전체 코드는 깃허브에서 보실 수 있습니다.
https://github.com/ssung0810/spring_study/tree/main/spring_mvc_study
GitHub - ssung0810/spring_study
Contribute to ssung0810/spring_study development by creating an account on GitHub.
github.com