04. MVC 프레임워크 만들기
FrontController 패턴 특징
프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
입구를 하나로!
공통 처리 가능
프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨
이번에는 프론트 컨트롤러를 v1~v5까지 만들며 단계적으로 발전하는 형태를 보자.
V1
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
각 버전마다 인터페이스를 만들 것이며, 이번에는 서블릿의 형태를 띄는 인터페이스를 만들었다.
각 컨트롤러는 이 인터페이스를 구현하면 된다.
MemberFormControllerV1, MemberListControllerV1, FrontControllerServletV1은 기존 서블릿에 ControllerV1을 implements하도록 하고 로직은 같다.
FrontControllerServletV1
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1(){
controllerMap.put("/front-controller/v1/members/new-form", new
MemberFormControllerV1());
controllerMap.put("/front-controller/v1/members/save", new
MemberSaveController());
controllerMap.put("/front-controller/v1/members", new
MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service" );
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
urlPatterns = "/front-controller/v1/*" 을 하
/front-controller/v1 를 포함한 하위 모든 요청은 이 서블릿에서 받아들인다.
service()
먼저 requestURI 를 조회해서 실제 호출할 컨트롤러를 controllerMap 에서 찾는다. 만약 없다면 404(SC_NOT_FOUND) 상태 코드를 반환한다. 컨트롤러를 찾고 controller.process(request, response); 을 호출해서 해당 컨트롤러를 실행한다.
v2 - view의 분리
V2에서는 Myview객체를 추가한다.
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));
}
}
이러한 View객체가 추가됨으로써 dispatcher.forward() 를 직접 생성해서 호출하지 않아도 된다. 단순히 MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환하면 된다.
다음 코드를 살펴보자.
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");
}
}
모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있었는데, MyView를 통해 중복이 사라짐을 확인할 수 있다.
이와같이 MemberSaveControllerV2와 MemberListControllerV2 또한 MyView로 인한 중복이 사라졌고 나머지는 V1과 동일하다.
V3 - Model 추가
컨트롤러 입장에서 HttpServletRequest, HttpServletResponse이 필요가 없다.
request 객체를 Model로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 된다.(Map을 사용하여 요청 파라미터 정보를 넘기도록 하자)
ModelView
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
public String getViewName() {
return viewName;
}
public void setViewName(String viewName) {
this.viewName = viewName;
}
public Map<String, Object> getModel() {
return model;
}
public void setModel(Map<String, Object> model) {
this.model = model;
}
}
뷰의 이름과 뷰를 렌더링할 때 필요한 model 객체를 가지고 있다.
model은 단순히 map으로 되어 있으므로 컨트롤러에서 뷰에 필요한 데이터를 key, value로 넣어주면 된다.
이렇게 되면 이제 컨트롤러는 서블릿을 사용하지 않는다.
ControllerV3
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
이제 컨트롤러는 ModelView에서 뷰 이름과 전달할 데이터 정보를 포함하는 객체를 반환해주면 된다.
MemberFormControllerV3
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
MemberSaveControllerV3
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 = new Member(username, age);
memberRepository.save(member);
ModelView mv = new ModelView("save-result");
mv.getModel().put("member", member);
return mv;
}
}
MemberListControllerV3
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;
}
}
paramMap.get("username"); 파라미터 정보는 map에 담겨있다. map에서 필요한 요청 파라미터를 조회하면 된다. mv.getModel().put("member", member); 모델은 단순한 map이므로 모델에 뷰에서 필요한 member 객체를 담고 반환한다
FrontControllerServletV3
@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 {
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);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
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;
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
뷰 리졸버
MyView view = viewResolver(viewName)
컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다. 그리고 실제 물리 경로가 있는 MyView 객체를 반환한다.
view.render(mv.getModel(), request, response)
뷰 객체를 통해서 HTML 화면을 렌더링 한다. 뷰 객체의 render() 는 모델 정보도 함께 받는다
v4
기본적인 구조는 V3와 같지만 대신, 컨트롤러가 ModelView 를 반환하지 않고, ViewName 만 반환한다.
ControllerV4
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
ModelView가 인터페이스에 없는 모습이다.
MemberFormControllerV4
public class MemberFormControllerV4 implements ControllerV4 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object>
model) {
return "new-form";
}
}
정말 단순하게 new-form 이라는 뷰의 논리 이름만 반환하면 된다.
이전 MemberSaveControllerV4와 MemberListControllerV4에서는 직접 Model을 생성했어야 하지만 이제는 생성할 필요 없이 파라미터로 바로 전달된다.
V3에서 생성 코드를 지우고 model.put("member", member); 로 바꿔주면 된다.
FrontControllerServletV4는 V3와 거의 동일하다.
V5 - 유연한 컨트롤러
여러 개발자들이 다른 버전으로 개발을 원한다면?
어댑터 패턴을 사용하자
어댑터 패턴 지금까지 우리가 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다. ControllerV3 , ControllerV4 는 완전히 다른 인터페이스이 따라서 호환이 불가능하다.
어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경하자.
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws ServletException, IOException;
}
boolean supports(Object handler) : 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
: 어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환,
이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다.
ControllerV3HandlerAdapter
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse
response, Object handler) {
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
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;
}
}
handler를 컨트롤러 V3로 변환한 다음에 V3 형식에 맞도록 호출한다.
ControllerV3는 ModelView를 반환하므로 그대로 ModelView를 반환하면 된다.
FrontControllerServletV5
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new
MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new
MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new
MemberListControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new
MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new
MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new
MemberListControllerV3());
//V4 추가
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new
MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new
MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new
MemberListControllerV4());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse
response)
throws ServletException, IOException {
Object handler = getHandler(request);
if (handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
MyView view = viewResolver(mv.getViewName());
view.render(mv.getModel(), request, response);
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다.handler=" + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
private final Map<String, object> handlerMappingMap = new HashMap<>();
매핑 정보의 값이 ControllerV3 , ControllerV4 같은 인터페이스에서 아무 값이나 받을 수 있는 Object 로 변경되었다
핸들러를 매핑하고 핸들러를 처리할 수 있는 어댑터를 조회하는 코드가 추가된 것을 확인할 수 있다.
어댑터 호출 : ModelView mv = adapter.handle(request, response, handler);