본문 바로가기
Java

[Java] 뷰(View)를 포함한 모델 데이터에 대한 추상화를 담당하는 ModelAndView 구현하기

by bkuk 2023. 3. 29.

Controller의 반환값은 String이다

public class HomeController implements Controller {
    @Override
    public String execute(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        QnaDao qnaDao = new QnaDao();
        req.setAttribute("questions", qnaDao.selectAll());
        return "home.jsp";
    }
}

 

위 Controller의 excute() 메서드 반환 값 String을 받아 서블릿에서 JSP로 이동한다.

 

만약 JQuery의 ajax() 함수를 활용해 서버에 요청을 보낸다면? 응답 데이터 타입은 JSON이다. 그렇다면 반환되는 값은 null로 지정을 해주면 된다. 이때, DispatcherServlet이 null 처리를 하지 않고 있기 때문에 뷰 이름(전달받은 문자열)이 null 인 경우 페이지 이동을 하지 않도록 null 처리를 해준다.

@WebServlet(name = "dispatcher", urlPatterns = "/", loadOnStartup = 1)
public class DispatcherServlet extends HttpServlet {
    @Override
    public void init() throws ServletException {
	[..생략..]
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Controller controller = rm.findController(requestUri);
        try {
            String viewName = controller.execute(req, resp);
            if( viewName != null ) {
            	move(viewName, req, resp);
	[생략..]
    }

 

 

AJAX 기능 지원에 따른 두가지의 문제점

public class addAnswerController implements Controller {
    @Override
    public String execute(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        if (!UserSessionUtils.isLogined(req.getSession())) {
            return "redirect:/users/loginForm";
        }
    	HttpSession session = req.getSession();
    	User user = (User) session.getAttribute("user");
        Ans answer = new Ans(user.getUserId(), req.getParameter("contents"),
                Long.parseLong(req.getParameter("questionId")));
        
        logger.debug("answer : {}", answer);
        AnsDao answerDao = new AnsDao();
        Ans savedAnswer = answerDao.insert(answer);
        
        // JSON 변환 시작
        ObjectMapper mapper = new ObjectMapper();
        resp.setContentType("application/json;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.print(mapper.writeValueAsString(savedAnswer));
        return null;
    }

}

 

위 코드를 보고, 문제점을 두가지 찾아보자.

  1. 응답으로 보내는 경우 이동할 JSP 페이지가 없다보니 불필요하게 null을 반환해야 한다.
  2. 자바 객체를 JSON으로 변환하고 응답하는 부분에서 중복이 발생한다.

 

문제를 해결하기 위해, 계획부터 세워보자.

  1. View 라는 인터페이스를 추가해보자.
  2. 인터페이스 View 를 구현하는 JspView와 JSonView를 생성해 각 View 맞도록 구현한다.

 

 

View 라는 인터페이스를 추가

public interface View {
	void render(HttpServletRequest req, HttpServletResponse resp) throws Exception;
}

 

 

View 라는 인터페이스를 구현하는 두개의 클래스

public class JspView implements View {
	private static final String DEFAULT_REDIRECT_PREFIX = "redirect:";
	private String viewName;
	
	public JspView( String viewName ) {
		
		if( viewName.equals("") || viewName == null ) {
			throw new NullPointerException("이동할 URL을 추가해 주세요.");
		}
		
		this.viewName = viewName;
	}
	@Override
	public void render(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        if (viewName.startsWith(DEFAULT_REDIRECT_PREFIX)) {
            resp.sendRedirect(viewName.substring(DEFAULT_REDIRECT_PREFIX.length()));
            return;
        }

        RequestDispatcher rd = req.getRequestDispatcher(viewName);
        rd.forward(req, resp);
	}
}
public class JsonView implements View {
	@Override
	public void render(HttpServletRequest req, HttpServletResponse resp) throws Exception {
		
        ObjectMapper mapper = new ObjectMapper();
        resp.setContentType("application/json;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.print(mapper.writeValueAsString(createModel(req)));
	}
	
	private Map<String, Object> createModel(HttpServletRequest req) {
		Enumeration<String> names = req.getAttributeNames();
		Map<String, Object> model = new HashMap<>();
		while ( names.hasMoreElements()) {
			String name = names.nextElement();
			model.put(name, req.getAttribute(name));
		}
		return model;
	}
}

 

 

현재까지의 클래스 사이의 관계

 

View 를 인터페이스로 분리한 것만으로도 충분히 유연한 MVC 프레임워크가 될 수 있다. 추가적으로 리팩토링할 부분을 고민해보자. 

HttpServletRequest를 사용하면서 발생하는 하나의 이슈는 JsonView는 HttpServletRequest에 추가되어 있는 모든 데이터를 JSON으로 변경한다.

이 경우, 개발자가 의도하지 않은 데이터가 불필요하게 JSON으로 변경되어 클라이언트 응답으로 보내줄 수도 있다. HttpServletRequest를 통해 데이터를 전달하지 않고, 개발자가 원하는 데이터만 뷰에 전달할 수  있도록 모델 데이터에 대한 추상화 작업을 진행해 본다.

 

 

모델 데이터에 대한 추상화 작업

 

모델 데이터를 View와 같이 전달해야 하기 때문에 ModelAndView와 같은 이름의 클래스를 추가하고, View와 모델 데이터를 Map<String, Object> 형태로 관리하자.

public class ModelAndView {
	private View view;
	private Map<String, Object> model = new HashMap<>();
	
	public ModelAndView(View view) {
		this.view = view;
	}
	
	public ModelAndView addModel(String key, Object value ) {
		model.put(key, value);
		return this;
	}
	
	public Map<String,Object> getModel() {
		return Collections.unmodifiableMap(model);
	}
	
	public View getView() {
		return view;
	}
}

 

 

View 인터페이스의 render 메서드에 모델 데이터를 인자로 추가하고, JspView와 JsonView를 수정한다.

public class JspView implements View {
	[..생략]
	@Override
	public void render(Map<String, ?> model, HttpServletRequest req, HttpServletResponse resp) throws Exception {
        if (viewName.startsWith(DEFAULT_REDIRECT_PREFIX)) {
            resp.sendRedirect(viewName.substring(DEFAULT_REDIRECT_PREFIX.length()));
            return;
        }
        Set<String> keys = model.keySet();
        for( String key : keys ) {
        	req.setAttribute(key, model.get(key));
        }
        RequestDispatcher rd = req.getRequestDispatcher(viewName);
        rd.forward(req, resp);
	}
}
public class JsonView implements View {
	@Override
	public void render(Map<String, ?> model, HttpServletRequest req, HttpServletResponse resp) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        resp.setContentType("application/json;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.print(mapper.writeValueAsString(model));
	}
}

댓글