Project Winter
지금까지 구현해오던 MVC 기능 중 Model, View 를 개발과정이다.
간단하게 JSP 를 이용한 view 를 개발했다.
View
public interface View {
void render(Map<String, ?> model, HttpServletRequest req, HttpServletResponse res) throws Exception;
}
생성된 View
에 따라 각각 알맞은 방법으로 렌더링 해주기 위한 메서드를 선언해뒀다.
JspView
public class JspView implements View {
private final String viewName;
public JspView(String viewName) {
this.viewName = viewName;
}
@Override
public void render(Map<String, ?> model, HttpServletRequest req, HttpServletResponse res) throws Exception {
model.forEach(req::setAttribute);
RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewName);
requestDispatcher.forward(req, res);
}
}
Servlet
을 통해 JSP 파일을 렌더링 하는 함수를 정의해줬다.
ViewResolver
public interface ViewResolver {
View resolveView(String viewName);
}
핸들러에서 응답으로 받은 결과를 View
로 변경해 줄 ViewResolver
코드이다.
JspViewResolver
public class JspViewResolver implements ViewResolver {
@Override
public View resolveView(String viewName) {
return new JspView("/" + viewName + ".jsp");
}
}
JspView
로 변환해줄 ViewResolver
만 있으면 되기 때문에 JspViewResolver
만 구현해뒀다.
JspView
의 경로는 절대경로로 foward
해주어야 경로를 올바르게 찾아가기 때문에 절대경로를 파라미터로 넣어주었다.
Model
데이터를 담아두고, View 에 전달해 줄 역할인 Model
코드이다.
public interface Model {
void addAttribute(String attributeName, Object attributeValue);
Map<String, Object> asMap();
}
가장 기본적으로 key-value
형태로 데이터를 담아두는 Model
인터페이스를 정의했다.
ModelMap
public class ModelMap {
private final Map<String, Object> modelMap = new LinkedHashMap<>();
public ModelMap() {
}
public ModelMap(Map<String, Object> model) {
this.modelMap.putAll(model);
}
public ModelMap(String attributeName, Object attributeValue) {
this.addAttribute(attributeName, attributeValue);
}
public void addAttribute(String attributeName, Object attributeValue) {
this.modelMap.put(attributeName, attributeValue);
}
public void addAllAttribute(Map<String, Object> model) {
this.modelMap.putAll(model);
}
public Map<String, Object> getModelMap() {
return this.modelMap;
}
}
Map
형태로 데이터를 담아두는 클래스인 ModelMap
이다.
Spring
에선 LinkedHashMap
을 상속받은 형태로 ModelMap
을 만들었지만, 개인적으로 컬렉션을 상속받아 구현하기 보단 변수로 가지고 있는 형태를 좋아하기 때문에 변수로 Map
을 가지게 구현했다.
ExtendedModelMap
public class ExtendedModelMap extends ModelMap implements Model {
@Override
public void addAttribute(String attributeName, Object attributeValue) {
super.addAttribute(attributeName, attributeValue);
}
@Override
public void addAllAttribute(Map<String, Object> model) {
super.addAllAttribute(model);
}
@Override
public Map<String, Object> asMap() {
return this.getModelMap();
}
}
ModelMap
과 Model
을 상속한 클래스이다.
ModelAndView
public class ModelAndView {
private Object view;
private ModelMap model;
public ModelAndView() {
}
public ModelAndView(String viewName) {
this.view = viewName;
}
public ModelAndView(String viewName, Map<String, Object> model) {
this.view = viewName;
this.getModel().addAllAttribute(model);
}
public Object getView() {
return view;
}
public String getViewName() {
return (this.view instanceof String) ? (String) this.view : null;
}
public ModelMap getModel() {
if (this.model == null) this.model = new ModelMap();
return model;
}
public Map<String, Object> getModelInternal() {
return this.getModel().getModelMap();
}
}
Handler
의 반환값으로 사용할 클래스로 개발했다. 이 클래스로 인해 Handler
는 특정 View
에 의존하지 않아도 되게 했다.
ModelAndViewContainer
public class ModelAndViewContainer {
Object view;
ModelMap model = new ExtendedModelMap();
HttpStatus status;
public void setViewName(String viewName) {
this.view = viewName;
}
public String getViewName() {
return (this.view instanceof String ? (String) this.view : null);
}
public Object getView() {
return this.view;
}
public void setView(Object view) {
this.view = view;
}
public void addAttribute(String attributeName, Object attributeValue) {
this.model.addAttribute(attributeName, attributeValue);
}
public void addAllAttribute(Map<String, Object> model) {
this.model.addAllAttribute(model);
}
public ModelMap getModel() {
return model;
}
public Map<String, Object> getModelMap() {
return model.getModelMap();
}
public HttpStatus getStatus() {
return status;
}
public void setStatus(HttpStatus status) {
this.status = status;
}
}
HandlerMethod
가 호출되기 전 생성하여 Model
과 Handler
응답값의 View
를 담아둘 클래스다.
DispatcherServlet 수정
// doDispatch()
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
String result = ha.handle(req, res, mappedHandler.getHandler());
기존 doDispatch()
함수의 핸들러 함수를 호출하는 로직이다.
이전에는 String
을 반환하였지만,
// doDispatch()
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
ModelAndView mv = ha.handle(req, res, mappedHandler.getHandler());
render(mv, req, res);
이젠 ModelAndView
를 반환하게 하였다.
반환받은 ModelAndView 는
private void render(ModelAndView mv, HttpServletRequest req, HttpServletResponse res) throws Exception {
String viewName = mv.getViewName();
log.debug("render view: {}", viewName);
View view = resolveViewName(viewName, req);
try {
view.render(mv.getModelInternal(), req, res);
} catch (Exception e) {
log.debug("Failed rendering view [{}], {}", view, e);
throw e;
}
}
private View resolveViewName(String viewName, HttpServletRequest req) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveView(viewName);
if (view != null) return view;
}
return null;
}
render()
함수를 통해 View
를 생성, 렌더링까지 하게 된다.
Handler 수정
HandlerMethod
private Object[] initParameters(HttpServletRequest req, HttpServletResponse res, ModelAndViewContainer mavContainer) throws Exception {
List<Object> parameterList = new ArrayList<>();
Arrays.stream(this.method.getParameters()).forEach(parameter -> {
Class<?> parameterType = parameter.getType();
if (ServletRequest.class.isAssignableFrom(parameterType)) parameterList.add(req);
else if (ServletResponse.class.isAssignableFrom(parameterType)) parameterList.add(res);
else if (Model.class.isAssignableFrom(parameterType)) parameterList.add(mavContainer.getModel());
else if (ModelMap.class.isAssignableFrom(parameterType)) parameterList.add(mavContainer.getModel());
else {
// TODO 서블릿 요청/응답을 제외한 나머지 파라미터에 대한 처리 필요
}
return parameterList.toArray();
}
public void handle(HttpServletRequest req, HttpServletResponse res, ModelAndViewContainer mavContainer) throws Exception {
this.parameters = initParameters(req, res, mavContainer);
Object view = this.method.invoke(bean, parameters);
mavContainer.setView(view);
req.getAttributeNames().asIterator().forEachRemaining(name -> {
Object value = req.getAttribute(name);
mavContainer.addAttribute(name, value);
});
}
요청이 들어오면 ModelAndViewContainer
를 만들고, 파라미터에 Model
을 넘겨줄 수 있도록 했다.
응답값으로 String
을 반환해주던 로직을 void
로 변경하고 ModelAndViewContainer
에 View
와 Model
을 담도록 변경했다.
RequestMappingHandlerAdapter
public ModelAndView handle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
((HandlerMethod) handler).handle(req, res, mavContainer);
Object view = mavContainer.getView();
if (!(view instanceof String)) throw new IllegalArgumentException("Unknown return value type: " + view.getClass().getSimpleName());
return getModelAndView(mavContainer);
}
public ModelAndView getModelAndView(ModelAndViewContainer mavContainer) {
return new ModelAndView(mavContainer.getViewName(), mavContainer.getModelMap());
}
HandlerMethod
를 호출하기 전 ModelAndViewContainer
를 생성해 파라미터로 넘겨준다.
Model, View 기능 확인
여기까지 Model
과 View
기능을 구현했다.
정상 작동 확인을 위해 샘플 코드를 만들었다.
Controller
@Controller
public class JspController {
@RequestMapping("/messages")
public String testJsp(Model model) {
model.addAttribute("list", List.of("Hello", "World"));
return "jsp-test";
}
}
Model
을 파라미터로 받는 컨트롤러를 만들고 해당 Model
에 문자열 배열을 받도록 했다.
반환값은 JSP 파일의 이름을 반환하면 된다.
JSP
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="kr">
<head>
<meta charset="UTF-8"/>
<title>list</title>
</head>
<body>
<table>
<thead>
<tr>
<th>#</th>
<th>message</th>
<th></th>
</tr>
</thead>
<tbody>
<c:forEach items="${list}" var="str" varStatus="status">
<tr>
<th scope="row">${status.count}</th>
<th>${str}</th>
<th></th>
</tr>
</c:forEach>
</tbody>
</table>
</body>
</html>
Model
에 담긴 배열을 JSP 문법에 맞게 반복을 돌려 테이블로 보여지게끔 했다.
결과
기대한 결과값과 일치하게 나오는것을 확인했다.