MVC 프레임워크 만들기

스프링 MVC 1편 강의 정리

Front Controller Pattern

  • 프론트 컨트롤러 도입 전에는 각 컨트롤러마다 공통 로직을 가지게 됨
  • 프론트 컨트롤러 도입 후에는 프론트 컨트롤러에서 공통 로직을 처리 후, 각 컨트롤러가 처리하게 됨
  • 스프링 웹 MVC의 dispatcher servlet 구현 원리
  • 특징
    • 프론트 컨트롤러 서블릿으로 클라이언트 요청을 받게 됨
    • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
    • 공통 처리 가능
    • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨

프론트 컨트롤러 도입 (v1)

  • 프론트 컨트롤러에서 클라이언트의 요청을 받음
  • 프론트 컨트롤러에서 URL 매핑 정보에서 컨트롤러 조회
  • 해당 컨트롤러 호출
  • 컨트롤러에서 JSP forward 통해 HTML 응답
public interface ControllerV1 {

	void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
  
}
  • 서블릿과 비슷한 형태의 인터페이스 도입
  • 각 컨트롤러는 해당 인터페이스를 구현하고 프론트 컨트롤러는 인터페이스를 호출해서 구현과 상관없이 로직의 일관성을 가져갈 수 있음
// MemberFormController
public class MemberFormController implements ControllerV1 {

	@Override
	public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String viewPath = "/WEB-INF/views/new-form.jsp";
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		dispatcher.forward(request, response);
	}
}

// Front controller
@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 MemberFormController());
		controllerMap.put("/front-controller/v1/members/save", new MemberSaveController());
		controllerMap.put("/front-controller/v1/members", new MemberListController());
	}

	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String requestURI = request.getRequestURI();
		ControllerV1 controller = controllerMap.get(requestURI);

		if (controller == null) {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		controller.process(request, response);
	}
}
  • 프론트 컨트롤러에서 요청을 받아 controllerMap에서 매핑된 URI에 따라 컨트롤러 호출

View 분리 (v2)

  • 모든 컨트롤러에서 뷰로 이동하는 부분이 중복됨
  • 뷰를 처리하는 객체를 만들어 중복되는 부분을 분리해서 처리
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 interface ControllerV2 {

	MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
  • 컨트롤러의 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 {
		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);
	}
}
  • 프론트 컨트롤러에서 뷰 객체의 render 메서드 호출하여 JSP forward

Model 추가 (v3)

  • servlet 종속성 제거
    • 컨트롤러에서는 HttpServletRequest, HttpServletResponse 가 꼭 필요하지 않음
    • 컨트롤러에서는 서블릿 기술을 몰라도 동작 가능
    • request 객체를 별도의 model 객체로 만들어서 반환
  • view 이름에서 중복 제거
    • 컨트롤러이서 뷰의 논리 이름을 반환하고 실제 물리 위치는 프론트 컨트롤러에서 처리하게 함
    • 뷰의 폴더 위치가 변경되어도 프론트 컨트롤러만 변경하면 됨
public class ModelView {
  private String viewName;
  private Map<String, Object> model = new HashMap<>();

  public ModelView(String viewName) {
    this.viewName = viewName;
  }

  // getter, setter method
}
  • ModelView 객체는 뷰 이름과 모델 맵을 가짐
// interface
public interface ControllerV3 {
  ModelView process(Map<String, String> paramMap);
}

// 구현 컨트롤러
public class MemberListControllerV3 implements ControllerV3 {

  private MemberRepository memberRepository = MemberRepository.getInstance();

  @Override
  public ModelView process(Map<String, String> paramMap) {
    List<Member> members = memberRepository.findAll();
    ModelView modelView = new ModelView("members");
    modelView.getModel().put("members", members);
    return modelView;
  }
}
  • 컨트롤러의 process에서는 ModelView 반환
  • 구현체에서는 ModelView를 생성하고 뷰 이름과 화면에서 보여줄 데이터를 모델에 넣음
@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerV3 extends HttpServlet {

  private Map<String, ControllerV3> controllerMap = new HashMap<>();

  public FrontControllerV3() {
    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 modelView = controller.process(paramMap);

    MyView view = viewResolver(modelView);
    view.render(modelView.getModel(), request, response);
  }

  private static MyView viewResolver(ModelView modelView) {
    return new MyView("/WEB-INF/views/" + modelView.getViewName() + ".jsp");
  }

  private static Map<String, String> createParamMap(HttpServletRequest request) {
    Map<String, String> paramMap = new HashMap<>();
    request.getParameterNames().asIterator()
        .forEachRemaining(name -> paramMap.put(name, request.getParameter(name)));
    return paramMap;
  }
}
  • 프론트 컨트롤러에서는 컨트롤러에 전달할 파라미터 map 생성
  • 컨트롤러에서 ModelView 객체를 받아 뷰의 논리 이름을 물리 경로로 변경
  • 실제 물리 경로를 가지는 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);
		this.render(request, response);
	}

	private static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
		model.forEach(request::setAttribute);
	}
}
  • 기존의 MyView에 모델을 처리하는 render 메서드 생성

단순하고 실용적인 컨트롤러 (v4)

  • 컨트롤러 인터페이스에서 매번 ModelView 객체를 생성, 반환하는 부분 개선
public interface ControllerV4 {

  String process(Map<String, String> paramMap, Map<String, Object> model);

}
  • 인터페이스의 process 메서드는 ModelView가 아닌 뷰 이름만 반환
public class MemberSaveControllerV4 implements ControllerV4 {

  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-result";
  }
}
  • 인터페이스 구현체에서는 저장, 조회 등의 기존 로직은 동일하고 뷰의 논리 이름만 반환
  • ModelView에 있던 모델은 파라미터로 전달
@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerV4 extends HttpServlet {

  private Map<String, ControllerV4> controllerMap = new HashMap<>();

  public FrontControllerV4() {
    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 = viewResolver(viewName);
    view.render(model, request, response);
  }

  private static Map<String, String> createParamMap(HttpServletRequest request) {
    Map<String, String> paramMap = new HashMap<>();
    request.getParameterNames().asIterator()
        .forEachRemaining(name -> paramMap.put(name, request.getParameter(name)));
    return paramMap;
  }

  private static MyView viewResolver(String viewName) {
    return new MyView("/WEB-INF/views/" + viewName + ".jsp");
  }
}
  • viewResolver와 모델을 파라미터로 넘기는 부분만 변경

유연한 컨트롤러1 (v5)

  • 어댑터 패턴을 사용해서 프론트 컨트롤러에서 다양한 컨트롤러 인터페이스를 사용하도록 변경
  • Handler Adaptor: 다양한 종류의 컨트롤러를 호출하는 어댑터
  • Handler: 컨트롤러의 이름을 더 넓은 범위의 이름으로 변경
public interface MyHandlerAdaptor {
  boolean support(Object handler);

  ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws ServletException, IOException;
}
  • support()
    • handler는 컨트롤러를 뜻하며 support 메서드에서 어댑터가 해당 컨트롤러를 처리 가능 여부 판단
  • handle()
    • 실제 컨트롤러를 호출하고 ModelView 반환
    • 프론트 컨트롤러가 직접 컨트롤러를 호출하던 것을 어댑터가 호출하게 됨
public class ControllerV3HandlerAdaptor implements MyHandlerAdaptor {
  @Override
  public boolean support(Object handler) {
    return handler instanceof ControllerV3;
  }

  @Override
  public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws ServletException, IOException {
    ControllerV3 controller = (ControllerV3) handler;
    Map<String, String> paramMap = createParamMap(request);
    return controller.process(paramMap);
  }

  private static Map<String, String> createParamMap(HttpServletRequest request) {
    Map<String, String> paramMap = new HashMap<>();
    request.getParameterNames().asIterator()
        .forEachRemaining(name -> paramMap.put(name, request.getParameter(name)));
    return paramMap;
  }
}
  • MyHandlerAdapter 구현하는 핸들러 어댑터
  • support()에서 ControllerV3 인스턴스인지 확인
  • handle()에서 파라미터 맵을 생성해서 컨트롤러에 전달
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

  private final Map<String, Object> handlerMappingMap = new HashMap<>();
  private final List<MyHandlerAdaptor> handlerAdaptors = new ArrayList<>();

  public FrontControllerServletV5() {
    initHandlerMappingMap();
    intHandlerAdaptors();
  }

  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());
  }

  private void intHandlerAdaptors() {
    handlerAdaptors.add(new ControllerV3HandlerAdaptor());
  }

  @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;
    }

    MyHandlerAdaptor handlerAdaptor = getHandlerAdaptor(handler);
    ModelView modelView = handlerAdaptor.handle(request, response, handler);

    String viewName = modelView.getViewName();
    MyView view = viewResolver(viewName);
    view.render(modelView.getModel(), request, response);

  }

  private MyHandlerAdaptor getHandlerAdaptor(Object handler) {
    for (MyHandlerAdaptor adapter : handlerAdaptors) {
      if (adapter.support(handler)) {
        return adapter;
      }
    }
    throw new IllegalArgumentException("handler adapter is not found");
  }

  private Object getHandler(HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    return handlerMappingMap.get(requestURI);
  }

  private static MyView viewResolver(String viewName) {
    return new MyView("/WEB-INF/views/" + viewName + ".jsp");
  }
}
  • 기존의 컨트롤러는 모두 핸들러로 매핑
  • 어댑터가 지원하기만 하면 어떤 것이라도 URL에 매핑 가능

유연한 컨트롤러 2 (v5)

// FrontController
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());

  //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()); handlerAdapters.add(new ControllerV4HandlerAdapter()); //V4 추가
}

  • controllerV4도 처리할 수 있도록 프론트 컨트롤러에 controllverV4를 추가
public class ControllerV4HandlerAdaptor implements MyHandlerAdaptor {

  @Override
  public boolean support(Object handler) {
    return handler instanceof ControllerV4;
  }

  @Override
  public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws ServletException, IOException {
    ControllerV4 controller = (ControllerV4) handler;

    Map<String, String> paramMap = createParamMap(request);
    Map<String, Object> model = new HashMap<>();

    String viewName = controller.process(paramMap, model);
    ModelView mv = new ModelView(viewName);
    mv.setModel(model);

    return mv;
  }

  private static Map<String, String> createParamMap(HttpServletRequest request) {
    Map<String, String> paramMap = new HashMap<>();
    request.getParameterNames().aㄴsIterator()
        .forEachRemaining(name -> paramMap.put(name, request.getParameter(name)));
    return paramMap;
  }
}
  • controllerv4를 지원하는 핸들러 어댑터도 MyHandlerAdaptor 구현

  • handle()에서 controllerv3와 다르게 ModelView를 만들어서 메서드 형식에 맞춰 반환