빙응의 공부 블로그

[Spring]스프링 MVC 1편 - 프론트 컨트롤러 본문

Spring/인프런_개념

[Spring]스프링 MVC 1편 - 프론트 컨트롤러

빙응이 2024. 1. 9. 17:25

📝프론트 컨트롤러 패턴 

  • MVC 패턴의 중복 코딩이나 공통 작업 문제를 해결하기 위한 프론트 컨트롤러 패턴이다. 
프론트 컨트롤러 도입 전

  • 도입 전 MVC 패턴은 위 사진처럼 각 컨트롤러에 공통 로직 + 개인 로직을 가지고 있었다. 
  • 그렇기에 중복 코드가 있고 공통 작업에 비효율적이였다.
프론트 컨트롤러 도입 후

  • 프론트 컨트롤러 패턴 도입 후
    • 프론트 컨트롤러 서블릿 하나로 클라이언트 요청을 받아 요청에 맞는 컨트롤러를 호출해 준다.
    • 공통 처리는 당연히 가능하며 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용 안해도 된다.
    • 스프링 웹 MVC의 핵심도 FrontController이다. 

 

📝프론트 컨트롤러 도입 - V1

V1의 목표는 여러 컨트롤러를 하나의 프론트 컨트롤러가 효율적으로 관리하고 중앙 집중적인 처리를 통해 코드의 일관성을 높이는 것이다.

V1 구조

1. 서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입
public interface ControllerV1 {
  void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

 

2. 회원 등록, 저장, 리스트 컨트롤러 생성 
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);
  }
}

public class MemberSaveControllerV1 implements ControllerV1 {
  private MemberRepository memberRepository = MemberRepository.getInstance();
  @Override
  public void process(HttpServletRequest request, HttpServletResponse
      response) throws ServletException, IOException {
    String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age"));
    Member member = new Member(username, age);
    memberRepository.save(member);
    request.setAttribute("member", member);
    String viewPath = "/WEB-INF/views/save-result.jsp";
    RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
    dispatcher.forward(request, response);
  }
}

public class MemberListControllerV1 implements ControllerV1 {
  private MemberRepository memberRepository = MemberRepository.getInstance();
  @Override
  public void process(HttpServletRequest request, HttpServletResponse
      response) throws ServletException, IOException {
    List<Member> members = memberRepository.findAll();
    request.setAttribute("members", members);
    String viewPath = "/WEB-INF/views/members.jsp";
    RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
    dispatcher.forward(request, response);
  }
}
  • 내부 로직은 기존 서블릿과 같다.
3. 프론트 컨트롤러 생성
//url에 *은 하위 컨트롤러가 나오기전에 먼저 호출된다.
@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 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 = "/front-controller/v1/*"
    • /front-controller/v1 을 포함한 하위 모든 요청은 이 서블릿에서 받아들인다.
    • 한마디로 하위 요청이 들어올 시 /front-controller/v1/*을 먼저 처리하고 하위 URL을 처리한다. 

📝View 분리 - V2

MVC의 문제였던 View 생성에 중복이 있던 것을 1개로 분리할 것이다. 

해당 과정은 viewPath 기준으로 MyView객체를 생성하여 하나로 통합할 것이다. 

각 컨트롤러에서 하는것은 MyView 객체를 만들어 반환만 해주고 Front Controller는 객체로 jsp를 실행시키면 된다. 

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

 

1. 서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입
public interface ControllerV2 {
  MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
2. 회원 등록, 저장, 리스트 컨트롤러 생성 
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");
  }
}

public class MemberListControllerV2 implements ControllerV2 {
  private MemberRepository memberRepository = MemberRepository.getInstance();
  @Override
  public MyView process(HttpServletRequest request, HttpServletResponse
      response) throws ServletException, IOException {
    List<Member> members = memberRepository.findAll();
    request.setAttribute("members", members);
    return  new MyView( "/WEB-INF/views/members.jsp");

  }
}

public class MemberSaveControllerV2 implements ControllerV2 {
  private MemberRepository memberRepository = MemberRepository.getInstance();
  @Override
  public MyView process(HttpServletRequest request, HttpServletResponse
      response) throws ServletException, IOException {
    String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age"));
    Member member = new Member(username, age);
    memberRepository.save(member);
    request.setAttribute("member", member);

    return new MyView( "/WEB-INF/views/save-result.jsp");
  }
}
3. 프론트 컨트롤러 생성
@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 {
    System.out.println("FrontControllerServletV2.service");
    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);
  }
}

 

 

📝Model(모델) 추가  - V3 

이 과정은 여러가지가 바뀔 것이다.

  • 서블릿 종속성을 제거할 것이다. 
    • 컨트롤러 입장에서 HttpServletRequest, HttpServletResponse는 필요가없다. 
    • 요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 지금 구조에서는 컨트롤러가 서블릿을 몰라도 된다.
  • 뷰 이름 중복 제거
    • 컨트롤레에서 지정하는 뷰 이름에 중복이 있다. 그렇기 때문에 컨트롤러는 뷰의 논리 이름만 반환하게 한다.
    • 또한 뷰의 위치가 바뀌어도 프론트 컨트롤러만 고치면 된다. 
/WEB-INF/views/new-form.jsp -> new-form
/WEB-INF/views/save-result.jsp -> save-result
/WEB-INF/views/members.jsp -> members

ModelView

지금까지 컨트롤러에서 서블릿에 종속적인 것들을 모두 제거하고 Model을 직접 만들며, 추가로 View 이름까지 전달하는 객체를 만들 것이다. 이것이 ModelView이다. 

public class ModelView {

  private String viewName;
  private Map<String, Object> model = new HashMap<>();

  public ModelView(String viewName) {
    this.viewName = viewName;
  }
  public void setViewName(String viewName){
    this.viewName= viewName;
  }
  public String getViewName() {
    return viewName;
  }
  public Map<String, Object> getModel() {
    return model;
  }
  public void setModel(Map<String, Object> model) {
    this.model = model;
  }

}

 

1. 서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입
public interface ControllerV3 {
  ModelView process(Map<String, String> paramMap);
}
2. 회원 등록, 저장, 리스트 컨트롤러 생성 
public class MemberFormControllerV3 implements ControllerV3{

  @Override
  public ModelView process(Map<String, String> paramMap) {
    return new ModelView("new-form");
  }
}

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

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

 

3. 프론트 컨트롤러 생성
@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 {
    System.out.println("FrontControllerServletV3.service");
    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");
  }
}

 

해당 코드는 만약 경로 정보가 바뀌어도 FrontController에 viewResolver(viewName)만 바꿔주면 된다. 

 

📝단순하고 실용적인 컨트롤러 - V4

앞 서 만든 V3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하였다. 그런데 실제 개발자 입장에선, 

항상 ModelView 객체를 생성하고 반환해야 하는 부분이 번거롭다.

좋은 프레임워크는 아키텍처도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다. 

v3와 구조가 같지만 대신해서 컨트롤러가 ModelView를 반환하는 것이 아닌, ViewName만 반환한다.

1. 서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입
public interface ControllerV4 {
  /**
   *
   * @param paramMap
   * @param model
   * @return
   */
  String process(Map<String, String> paramMap, Map<String, Object> model);
}

 

2. 회원 등록, 저장, 리스트 컨트롤러 생성 
public class MemberFormControllerV4 implements ControllerV4 {
  @Override
  public String process(Map<String, String> paramMap, Map<String, Object> model) {
    return "new-form";
  }
}

public class MemberListControllerV4 implements ControllerV4 {
  private MemberRepository memberRepository = MemberRepository.getInstance();
  @Override
  public String process(Map<String, String> paramMap, Map<String, Object> model) {
    List<Member> members = memberRepository.findAll();
    model.put("members", members);

    return  "members";
  }
}

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";
  }
}
3. 프론트 컨트롤러 생성
@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/ 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");
  }
}

해당 과정은 사실상 바뀐 부분이 별로 없다.

  • 모델 객체 전달
    • 각 컨트롤러에 모델 객체를 추가로 생성해서 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다.
  • 뷰의 논리 이름을 직접 반환
    • 컨트롤러는 ModelView 객체 대신에 이름만 반환하게 하였다. 이 값을 이용해 실제 물리 뷰를 찾을 수 있다.
  • 정리
    • 기본 구조에서 모델을 파라미터로 넘기고, 뷰의 논리 이름을 반환한다는 작은 아이디어에서 시작되었다.
    • 전보다 개발자 입장에서 구현 및 이해가 쉬워졌다.