MVC1 4th Step

최보현·2022년 7월 27일
0

MVC

목록 보기
4/18
post-thumbnail

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - sec04
출처 : 스프링 MVC 1편

MVC 프레임워크 만들기

프론트 컨트롤러 패턴

특징

  1. 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  2. 프런트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  3. 공통 처리 가능!
  4. 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨!

스프링 웹 MVC의 핵심이 바로 FrontController! -> DispatcherServlet이 이 패턴으로 구현되어 있음

프론트 컨트롤러 도입 - v1

ControllerV1

public interface ControllerV1 {
  void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

서블릿과 유사한 모양의 컨트롤러 인터페이스를 도입!
-> 각 컨트롤러들이 이 인터페이스를 구현해야 함 ex)

public class MemberFormControllerV1 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);
  }
}

-> 프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져감

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/frontcontroller/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 MemberSaveControllerV1());
    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 : urlPatterns = "/front-controller/v1/*" v1을 포함한 모든 하위 요청을 이 서블릿에서 받아들임
controllerMap : key - 매핑 URL , value - 호출될 컨트롤러
service() : 먼저 requestURI 를 조회해서 실제 호출할 컨트롤러를 controllerMap 에서 찾음 if 없다면 404(SC_NOT_FOUND) 상태 코드를 반환 -> 컨트롤러를 찾고 controller.process(request, response); 을 호출해서 해당 컨트롤러를 실행
JSP : 이전과 동일

View 분리 - v2

모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고 깔끔하지 않음
하단의 코드를 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 만듬

String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

뷰 객체

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

ControllerV2 - 컨트롤러가 뷰를 반환함

public interface ControllerV2 {
  MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

각 Controller의 변화

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

dispatcher.forward()를 직접 생성해서 호출하지 않고 MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환함

FrontControllerV2

@WebServlet(name = "frontControllerServletV2", urlPatterns = "/frontcontroller/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);
  }
}

ControllerV2의 반환 타입이 MyView이므로 프론트 컨트롤러는 컨트롤러의 호출 결과를 MyView를 반환 받음 => view.render()를 호출함으로써 forward로직을 수행해서 JSP 실행

//Myview.render()
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
  dispatcher.forward(request, response);
}

프론트 컨트롤러의 도입으로 MyView 객체의 render() 를 호출하는 부분을 모두 일관되게 처리할 수 있음 => 각각의 컨트롤러는 MyView 객체를 생성만 해서 반환하면 끝!

Model 추가 - v3

서블릿 종속성 제거

컨트롤러 입장에서 요청 파라미터 정보를 자바의 Map으로 대신 넘기도록 하면 지금 구조에서는 컨트롤러가 서블릿 기술을 몰라도 동작 가능! & request 객체를 Model로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 됨!
즉, 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경

뷰 이름 중복 제거

컨트롤러는 뷰의 논리 이름을 반환하고, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화 => 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 됨!
ex) /WEB-INF/views/new-form.jsp => new-form

ModelView

지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했음 & Model도 request.setAttribute() 를 통해 데이터를 저장하고 뷰에 전달했음
=> 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View 이름까지 전달하는 객체를 생성

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

이 컨트롤러는 서블릿 기술을 전혀 사용❌ -> 구현이 매우 단순해지고, 테스트 코드 작성시 테스트 용이

  • HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아서 호출해줌
  • 응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체를 반환

각각의 Controller

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;
  }
}
  • ModelView 를 생성할 때 "save-result" 이라는 view의 논리적인 이름을 지정 -> 실제 물리적인 이름은 프론트 컨트롤러에서 처리
  • paramMap.get("username"); -> 파라미터 정보는 map에 담겨있고 map에서 필요한 요청 파라미터를 조회
  • mv.getModel().put("member", member); -> 모델은 단순한 map이므로 모델에 뷰에서 필요한 member 객체를 담고 반환

FrontControllerServletV3

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/frontcontroller/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");
  }
}

createParamMap() -> HttpServletRequest에서 파라미터 정보를 꺼내서 Map으로 변환 & 해당 Map( paramMap )을 컨트롤러에 전달하면서 호출

View 객체

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

뷰 리졸버

  • MyView view = viewResolver(viewName) -> 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경 & 실제 물리 경로가 있는 MyView 객체 반환
  • view.render(mv.getModel(), request, response) -> 뷰 객체를 통해서 HTML 화면을 렌더링 & 뷰 객체의 render() 는 모델 정보도 함께 받음
  • JSP는 request.getAttribute() 로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내서 request.setAttribute() 로 담아둠
  • JSP로 포워드 해서 JSP를 렌더링

단순하고 실용적인 Controller - v4

ControllerV4

public interface ControllerV4 {
  /**
  * @param paramMap
  * @param model
  * @return viewName
  */
  String process(Map<String, String> paramMap, Map<String, Object> model);
}

인터페이스에 ModelView가 없음, model 객체는 파라미터로 전달되기 때문에 그냥
사용하면 되고, 결과로 뷰의 이름만 반환

각각 Controller

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-result";
  }
}
  • 단순하게 뷰의 논리 이름 "save-result"만 반환하면 됨!
  • model.put("member", member) -> 모델이 파라미터로 전달되기 때문에, 모델을 직접 생성하지 않아도 됨

FrontControllerServletV4

@WebServlet(name = "frontControllerServletV4", urlPatterns = "/frontcontroller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
  private Map<String, ControllerV4> controllerMap = new HashMap<>();
  public FrontControllerServletV4() {
    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 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");
  }
}
  • 모델 객체 전달 : Map<String, Object> model = new HashMap<>(); //추가 -> 모델 객체를 프론트 컨트롤러에서 생성해서 넘겨줌 & 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 됨
  • 뷰의 논리 이름을 직접 반환 : 컨트롤로가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해서 실제 물리 뷰를 찾을 수 있음
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);

유연한 컨트롤러1 - v5

어댑터 패턴

지금까지 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있음 하지만, 각자 개발자들이 서로 원하는 방식이 다를 경우, 사용하는 것이 바로 어댑터 => 어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리

핸들러 어댑터

중간 어댑터 역할, 다양한 종류의 컨트롤러를 호출할 수 있음

핸들러

더 넓은 범위인 핸들러, 어댑터가 있기 때문에 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리 가능

MyHandlerAdapter -> 어댑터용 인터페이스

public interface MyHandlerAdapter {
  boolean supports(Object handler);
  ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
  • boolean supports(Object handler) : handler -> 컨트롤러 + 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드
  • ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) : 어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환해야 함, 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환!
  • 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출됨

ControllerV3HandlerAdapter - ControllerV3를 지원하는 어댑터

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
  @Override
  public boolean supports(Object handler) {
    return (handler instanceof ControllerV3);
    //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;
    //handler를 컨트롤러 V3로 변환한 다음에 V3 형식에 맞도록 호출
	//supports() 를 통해 ControllerV3 만 지원하기 때문에 타입 변환은 걱정없이 실행
	//ControllerV3는 ModelView를 반환하므로 그대로 ModelView를 반환하면 됨
  }
  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;
  }
}

FrontControllerServletV5

@WebServlet(name = "frontControllerServletV5", urlPatterns = "/frontcontroller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
  private final Map<String, Object> handlerMappingMap = new HashMap<>(); //매핑 정보의 값이 아무 값이나 받을 수 있는 Object 로 변경됨
  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());
  }
  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);
  //어댑터의 handle(request, response, handler) 메서드를 통해 실제 어댑터가 호출됨
  //어댑터는 handler(컨트롤러)를 호출하고 그 결과를 어댑터에 맞추어 반환
  //ControllerV3HandlerAdapter의 경우 어댑터의 모양과 컨트롤러의 모양이 유사해서 변환 로직이 단순
  MyView view = viewResolver(mv.getViewName());
  view.render(mv.getModel(), request, response);
  }
  private Object getHandler(HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    return handlerMappingMap.get(requestURI);
    //핸들러 매핑 정보인 handlerMappingMap 에서 URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환
  }
  private MyHandlerAdapter getHandlerAdapter(Object handler) {
    for (MyHandlerAdapter adapter : handlerAdapters) {
    if (adapter.supports(handler)) {
    	return adapter;
    //handler 를 처리할 수 있는 어댑터를 adapter.supports(handler) 를 통해서 찾음
    //handler가 ControllerV3 인터페이스를 구현했다면, ControllerV3HandlerAdapter 객체가 반환됨
  	}
  }
  throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
  }
  private MyView viewResolver(String viewName) {
  return new MyView("/WEB-INF/views/" + viewName + ".jsp");
  }
}

컨트롤러(Controller) => 핸들러(Handler)

이전에는 컨트롤러를 직접 매핑해서 사용했지만 이제는 어댑터를 사용하기 때문에, 컨트롤러 뿐만 아니라 어댑터가 지원하기만 하면, 어떤 것이라도 URL에 매핑해서 사용할 수 있음 -> 이름을 컨트롤러에서 더 넒은 범위의 핸들러로 변경한 이유

유연한 컨트롤러2 - v5

ControllerV4HandlerAdapter

public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
  @Override
  public boolean supports(Object handler) {
  	return (handler instanceof ControllerV4);
  }
  @Override
  public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    ControllerV4 controller = (ControllerV4) handler;
    Map<String, String> paramMap = createParamMap(request);
    Map<String, Object> model = new HashMap<>();
    String viewName = controller.process(paramMap, model);
    //handler를 ControllerV4로 케스팅 하고, paramMap, model을 만들어서 해당 컨트롤러를 호출 & viewName을 반환 받음
    ModelView mv = new ModelView(viewName);
    mv.setModel(model);
    return mv;
    //어댑터가 호출하는 ControllerV4 는 뷰의 이름을 반환
    //어댑터는 뷰의 이름이 아니라 ModelView 를 만들어서 반환해야 함
    //ControllerV4 는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식을 맞추어 반환
  }
  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;
  }
}

어댑터와 ControllerV4

public interface ControllerV4 {
  String process(Map<String, String> paramMap, Map<String, Object> model);
}
public interface MyHandlerAdapter {
  ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
profile
Novice Developer's Blog

0개의 댓글