MVC Framework 만들기

유병익·2022년 10월 17일
0
post-thumbnail

0. Front Controller Pattern 소개


0.1 Front Controller 도입 이전


0.2 Front Controller 도입 이후


0.3 Front Controller Pattern 특징


  • Front Controller Servlet 하나로 클라이언트의 요청을 받음
    • 입구를 하나로 통일!!
  • Front Controller가 요청에 맞는 Controller를 찾아서 호출
  • 공통 처리 가능
  • Front Controller를 제외한 나머지 컨트롤러는 Servlet을 사용하지 않아도 됨

📌 Spring Web MVC & FrontController

  • Spring Web MVC의 핵심도 바로 Front Controller
  • Spring Web MVC의 DispatcherServletFront Controller패턴으로 구현

이어질 순서에서 Front Controller를 단계적으로 도입해보며 Front Controller를 이해하자!!

1. Front Controller 도입 - V1


  • 목표
    • 기존 코드 최대한 유지 + Front Controller 도입

1.1 V1 구조


1.2 V1 구현


1.2.1 ControllerV1


package hello.servlet.web.frontcontroller.v1;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface ControllerV1 {
 void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
  • Servlet과 비슷한 Controller Interface 도입
  • 각 Controller들은 해당 Interface를 구현
  • Front Controller는 위 인터페이스를 호출
    • 구현과 관계 없이 일관된 로직

📌 내부 로직은 기존 Servlet과 거의 같다.

1.2.2 회원 등록 - MemberFormControllerV1


  • Code
    package hello.servlet.web.frontcontroller.v1.controller;
    import hello.servlet.web.frontcontroller.v1.ControllerV1;
    import javax.servlet.RequestDispatcher;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    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);
    		 }
    }

1.2.3 회원 저장 - MemberSaveControllerV1


  • Code
    package hello.servlet.web.frontcontroller.v1.controller;
    import hello.servlet.domain.member.Member;
    import hello.servlet.domain.member.MemberRepository;
    import hello.servlet.web.frontcontroller.v1.ControllerV1;
    import javax.servlet.RequestDispatcher;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    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);
    		 }
    }

1.2.4 회원 목록 - MemberListControllerV1


  • Code
    package hello.servlet.web.frontcontroller.v1.controller;
    import hello.servlet.domain.member.Member;
    import hello.servlet.domain.member.MemberRepository;
    import hello.servlet.web.frontcontroller.v1.ControllerV1;
    import javax.servlet.RequestDispatcher;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.List;
    
    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);
    		 }
    }

1.2.5 FrontControllerV1 - FrontControllerServletV1

  • Code
    package hello.servlet.web.frontcontroller.v1;import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
    
    import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
    import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    @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);
    		 }
    }

1.3 FrontController 분석


  • urlPatterns = "/front-controller/v1/" : /front-controller/v1/
    • /front-controller/v1를 포함한 하위 모든 요청은 이 Servlet에서 받아들임
    • ex) /front-controller/v1 , /front-controller/v1/a , /front-controller/v1/a/b
  • controllerMap
    • key : 매핑 URL
    • value : 호출될 Controller
  • service()
    1. requestURI 조회
    2. 실제 호출할 Controller를 controllerMap 에서 찾는다.
      • 만약 없다면 404(SC_NOT_FOUND) 상태 코드 반환
    3. Controller를 찾고 controller.process(request, response);호출
    4. 해당 컨트롤러를 실행

2. View 분리 - V2


  • 목표

    • Controller에서 View로 이동하는 부분의 중복 제거

2.1 V2 구조


2.2 V2 구현


2.2.1 MyView


  • Code
    package hello.servlet.web.frontcontroller;
    import javax.servlet.RequestDispatcher;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import java.io.IOException;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);
    		 }
    }

2.2.2 ControllerV2


  • Code
    package hello.servlet.web.frontcontroller.v2;
    import hello.servlet.web.frontcontroller.MyView;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    public interface ControllerV2 {
    
     MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
    }

2.2.3 회원 등록 - MemberFormControllerV2


  • Code
    package hello.servlet.web.frontcontroller.v2.controller;
    import hello.servlet.web.frontcontroller.MyView;
    import hello.servlet.web.frontcontroller.v2.ControllerV2;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    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");
    		 }
    }

2.2.4 회원 저장 - MemberSaveControllerV2


  • Code
    package hello.servlet.web.frontcontroller.v2.controller;
    import hello.servlet.domain.member.Member;
    import hello.servlet.domain.member.MemberRepository;
    import hello.servlet.web.frontcontroller.MyView;import hello.servlet.web.frontcontroller.v2.ControllerV2;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    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");
    		 }
    }

2.2.5 회원 목록 - MemberListControllerV2


  • Code
    package hello.servlet.web.frontcontroller.v2.controller;
    import hello.servlet.domain.member.Member;
    import hello.servlet.domain.member.MemberRepository;
    import hello.servlet.web.frontcontroller.MyView;
    import hello.servlet.web.frontcontroller.v2.ControllerV2;
    import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.List;
    
    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");
    		 }
    }

2.2.6 FrontControllerV2 - FrontControllerServletV2


  • Code
    package hello.servlet.web.frontcontroller.v2;
    import hello.servlet.web.frontcontroller.MyView;
    import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
    import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
    import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    
    import java.util.Map;@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);
    		 }
    }

2.3 V2 분석


  • ControllerView (MyView) return
  • 이제 각 Controllerdispatcher.forward()를 직접 생성 및 호출하지 않아도 된다.
    public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    		 RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
    		 dispatcher.forward(request, response);
    }
    • MyView 객체를 생성하고 거기에 이름만 넣고 반환하면 된다.

    • MyView 객체의 render()를 호출하는 부분을 일관되게 처리할 수 있다.

  • Controller 호출 결과인MyView viewview.render() 호출 → forward 로직 수행 → JSP 실행

📌 ControllerV1의 구현 클래스와 ControllerV2의 구현 클래스를 비교해보면, dispatcher.forward() 부분의 중복이 확실하게 제거된 것을 확인 가능!!

3. Model 추가 - V3


  • 목표
    • Servlet 종속성 제거
      • 구현하는 ControllerServlet을 사용하지 않도록 변경
      • request 객체가 아닌 별도의 Model을 직접 만들자
        • 요청 파라미터 정보 → Java의 ****Map으로 대신 전달
        • View 논리 이름 까지 전달
    • View 이름 중복 제거
      • Controller → View의 논리 이름 반환
      • 실제 물리 위치의 이름 → Front Controller에서 처리

3.1 V3 구조


3.2 V3 구현


3.2.1 ModelView


  • Code
    package hello.servlet.web.frontcontroller;
    import java.util.HashMap;
    import java.util.Map;
    
    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;
    		 }
    		}

3.2.2 ControllerV3


  • Code
    package hello.servlet.web.frontcontroller.v3;
    import hello.servlet.web.frontcontroller.ModelView;
    import java.util.Map;
    
    public interface ControllerV3 {
    		 ModelView process(Map<String, String> paramMap);
    }

3.2.3 회원 등록 - MemberFormControllerV3


  • Code
    package hello.servlet.web.frontcontroller.v3.controller;
    import hello.servlet.web.frontcontroller.ModelView;
    import hello.servlet.web.frontcontroller.v3.ControllerV3;
    import java.util.Map;
    
    public class MemberFormControllerV3 implements ControllerV3 {
    
    		 @Override
    		 public ModelView process(Map<String, String> paramMap) {
    				 return new ModelView("new-form");
    		 }
    }

3.2.4 회원 저장 - MemberSaveControllerV3


  • Code
    package hello.servlet.web.frontcontroller.v3.controller;
    import hello.servlet.domain.member.Member;
    import hello.servlet.domain.member.MemberRepository;
    import hello.servlet.web.frontcontroller.ModelView;
    import hello.servlet.web.frontcontroller.v3.ControllerV3;
    import java.util.Map;
    
    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.2.5 회원 목록 - MemberListControllerV3


  • Code
    package hello.servlet.web.frontcontroller.v3.controller;
    import hello.servlet.domain.member.Member;
    import hello.servlet.domain.member.MemberRepository;
    import hello.servlet.web.frontcontroller.ModelView;
    import hello.servlet.web.frontcontroller.v3.ControllerV3;
    import java.util.List;
    import java.util.Map;
    
    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;
    		 }
    }

3.2.6 FrontControllerV3 - FrontControllerServletV3


  • Code
    package hello.servlet.web.frontcontroller.v3;
    import hello.servlet.web.frontcontroller.ModelView;
    import hello.servlet.web.frontcontroller.MyView;
    import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
    import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
    import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    @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");
    		 }
    }

3.3 V3 분석


3.3.1 ModelView


  • View의 논리 이름과 View를 Rendering할 때 필요한 model을 갖고 있음
    • model은 Java의 Map 자료구조 → Controller에서 View에 필요한 데이터를 key, value로 삽입

3.3.2 Controller


  • V3 Controller는 Servlet 기술을 전혀 사용하지 않는다.

    📌 따라서, 구현이 매우 단순해지고, 테스트 코드로 테스트 하기 쉽다.
  • 기존의 HttpServletRequest가 제공하는 Parameter들은 어떻게??

    • Front Controller가 paramMap에 담아서 Controller 구현 클래스호출
  • 반환 값으로 View의논리 이름과 View에 전달할Model 데이터를 포함하는 ModelView객체 반환

3.3.3 View Resolver


MyView view = viewResolver(viewName);
private MyView viewResolver(String viewName) {
		 return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
  • Controller가 반환한 View의 논리 이름을 View의실제 물리 경로로 변경
  • 실제 물리 경로가 있는 MyView 객체 반환
    • 논리 이름 : members
    • 물리 경로 : /WEB-INF/views/members.jsp

3.3.4 MyView


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);
}
  • View 객체를 통해서 HTML 화면을 렌더링

  • View의 render()는 model도 함께 받는다.

    📌 Why?
    JSPrequest.getAttribute()로 데이터를 조회한다.

    ```java
    private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
    		 model.forEach((key, value) -> request.setAttribute(key, value));
    }
    ```
    
    - 따라서 model에 담겨있는 데이터`(모든 Key, Value 값)`를 request객체에 옮기는 과정 필요!!

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


📌 좋은 Framework

  • Architecture를 잘 설계해야 한다.
  • 실제 개발자가 단순하고 편리하게 사용할 수 있어야 한다.
  • 목표
    • 실용성
    • 편리함
      • 개발자 입장에서, 항상 ModelView 객체를 생성 및 반환해야 하는 부분이 번거롭다.
      • 개발자들이 매우 편리하게 개발할 수 있도록 개선하자!!

4.1 V4 구조


  • 기본적인 구조는 V3와 같지만, Controller가 ModelView가 아닌 ViewName만 반환한다.

4.2 V4 구현


4.2.1 ControllerV4


  • Code
    package hello.servlet.web.frontcontroller.v4;import java.util.Map;
    public interface ControllerV4 {
    		 /**
    		 * @param paramMap
    		 * @param model
    		 * @return viewName
    		 */
    		 String process(Map<String, String> paramMap, Map<String, Object> model);
    }

4.2.2 회원 등록 - MemberFormControllerV4


  • Code
    package hello.servlet.web.frontcontroller.v4.controller;
    import hello.servlet.web.frontcontroller.v4.ControllerV4;
    import java.util.Map;
    public class MemberFormControllerV4 implements ControllerV4 {
    
    		 @Override
    		 public String process(Map<String, String> paramMap, Map<String, Object> model) {
    				 return "new-form";
    		 }
    }

4.2.3 회원 저장 - MemberSaveControllerV4


  • Code
    package hello.servlet.web.frontcontroller.v4.controller;
    import hello.servlet.domain.member.Member;
    import hello.servlet.domain.member.MemberRepository;
    import hello.servlet.web.frontcontroller.v4.ControllerV4;
    import java.util.Map;
    
    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";
    		 }
    }

4.2.4 회원 목록 - MemberListControllerV4


  • Code
    package hello.servlet.web.frontcontroller.v4.controller;import hello.servlet.domain.member.Member;
    import hello.servlet.domain.member.MemberRepository;
    import hello.servlet.web.frontcontroller.v4.ControllerV4;
    import java.util.List;
    import java.util.Map;
    
    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";
    		 }
    }

4.2.5 FrontControllerV4 - FrontControllerServletV4


  • Code
    package hello.servlet.web.frontcontroller.v4;
    import hello.servlet.web.frontcontroller.MyView;
    import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
    import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
    import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    @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");
    		 }
    }

4.3 V4 분석


4.3.1 Controller


  • V4는 Controller Interface에 ModelView가 없음
    • model 객체는 Parameter로 전달 → 전달된 model 객체를 그대로 사용
    • 반환 값으로 View 이름만 반환

4.3.3 FrontController


  • 모델 객체 전달
    Map<String, Object> model = new HashMap<>();
    • model 객체를 Front Controller에서 생성해서 넘겨준다.
    • Controller에서 model 객체에 값을 담으면 그대로 담겨있게 된다.
  • View의 논리 이름을 직접 반환
    String viewName = controller.process(paramMap, model);
    MyView view = viewResolver(viewName);
    • Controller는 직접 View의 논리 이름을 반환한다.
    • View Resolver와 해당 값을 사용해서 실제 물리 뷰를 찾을 수 있다.

4.3.4 정리


  • Front Controller V4 & Controller V4는 매우 단순하고 실용적 !!
  • ModelParameter로 전달
  • View의 논리 이름 반환

📌 개발자 입장에서 V4를 사용해 효율적이고실용적으로 코드를 작성할 수 있다.

📌 Framework 또는 공통 기능이 수고로워야 사용하는 개발자가 편리해진다.

5. 유연한 컨트롤러 - V5


  • 목표
    • Adapter 패턴 적용

📌 Front Controller가 다양한 방식의 Controller를 처리할 수 있도록 변경하자!!

5.1 V5 구조


5.2 V2 구현


5.2.1 MyHandlerAdapter


  • Code
    package hello.servlet.web.frontcontroller.v5;
    import hello.servlet.web.frontcontroller.ModelView;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    public interface MyHandlerAdapter {
    
    		 boolean supports(Object handler);
    		 ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
    
    }

5.2.2 ControllerV3HandlerAdapter


  • Code
    package hello.servlet.web.frontcontroller.v5.adapter;
    import hello.servlet.web.frontcontroller.ModelView;
    import hello.servlet.web.frontcontroller.v3.ControllerV3;
    import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.HashMap;
    import java.util.Map;
    
    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;
    		 }
    }

5.2.3 ControllerV4HandlerAdapter


  • Code
    package hello.servlet.web.frontcontroller.v5.adapter;
    import hello.servlet.web.frontcontroller.ModelView;import hello.servlet.web.frontcontroller.v4.ControllerV4;
    import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.HashMap;
    import java.util.Map;
    
    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);
    				 
    				 ModelView mv = new ModelView(viewName);
    				 mv.setModel(model);
    				 
    				 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;
    		 }
    }

5.2.4 FrontControllerV5 - FrontControllerServletV5


  • Code
    package hello.servlet.web.frontcontroller.v5;
    import hello.servlet.web.frontcontroller.ModelView;
    import hello.servlet.web.frontcontroller.MyView;
    import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
    import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
    import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
    import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
    import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter;
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    @WebServlet(name = "frontControllerServletV5", urlPatterns = "/frontcontroller/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());
    
    				 //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 추가
    		 }
    
    		 @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);
    		 }
    
    		 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");
    		 }
    }

5.3 V5 분석


5.3.1 MyHandlerAdapter


  • boolean supports(Object handler)
    • handler는 Controller
    • Adapter가 해당 Controller를 처리할 수 있는지 판단
  • ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
    • Adapter는 실제 Controller를 호출 → 결과로 ModelView를 반환
    • 이전에는 Front Controller가 실제 Controller를 호출
    • 이제는 Adapter를 통해서 실제 Controller가 호출

5.3.2 ControllerV3HandlerAdapter


ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
  • handler를 ControllerV3로 변환 → V3 형식으로 호출
  • supports() 를 통해 ControllerV3 지원 여부 확인 → 타입 변환은 걱정하지 않아도 됨
  • ControllerV3는 ModelView를 반환하므로 그대로 ModelView를 반환

5.3.3 ControllerV4HandlerAdapter


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을 만들어서 해당 Handler(Controller)를 호출
  • viewName을 반환 받는다.
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
  • ControllerV4 는 뷰의 이름을 반환한다.
  • Adapter는 View의 이름이 아니라 ModelView를 반환해야 한다.

📌 ControllerV4는 View의 이름을 반환했지만, Adapter는 이것을 ModelView로 만들어서 반환

5.3.4 FrontControllerV5 - FrontControllerServletV5


  • 생성자

    public FrontControllerServletV5() {
    		 initHandlerMappingMap(); //핸들러 매핑 초기화
    		 initHandlerAdapters(); //어댑터 초기화
    }
    • 생성자는 핸들러 매핑과 어댑터를 초기화(등록)한다.
  • Handler Mapping

    private Object getHandler(HttpServletRequest request) {
    		 String requestURI = request.getRequestURI();
    		 return handlerMappingMap.get(requestURI);
    }
    • handlerMappingMap에서 URL에 매핑된 Handler(Controller)객체를 찾아서 반환
  • Handler(Controller)를 처리할 수 있는 Adapter조회

    for (MyHandlerAdapter adapter : handlerAdapters) {
    		 if (adapter.supports(handler)) {
    				 return adapter;
    		 }
    }
    • Handler(Controller)를 처리할 수 있는 Adapter를 찾는다.

5.3.5 전체 흐름


  1. HTTP 요청
  2. Front Controller는 HTTP 요청을 처리할 수 있는 Handler(Controller)를 찾는다.
  3. 이후, Front Controller는 해당 Handler(Controller)를 처리할 수 있는 Adapter를 찾는다.
  4. Front Controller는 해당 Adapter를 호출한다.
  5. Adapter는 Handler(Controller)를 호출한다.
  6. Adapter는 Handler(Controller) 실행 결과를 ModelView 객체로 만들어 반환
  7. ModelView 객체의 ViewName(논리 이름)을 viewResolver를 통해 물리 주소로 변환
  8. View의 render() 를 호출할 때, ModelView의 model을 Parameter로 전달

6. 정리


6.1 Review


6.1.1 V1 - Front Controller 도입

  • Front Controller를 도입

6.1.2 V2 - View 로직 분리

  • 단순 반복 되는 View 로직 분리

6.1.3 V3 - Model 추가

  • Servlet 종속성 제거
  • View 이름 중복 제거

6.1.4 V4 - 실용적인 Controller

  • ModelView를 직접 생성 및 반환하지 않도록 편리한 Interface 제공

6.1.5 V5 - 유연한 Controller

  • Adapter도입
  • 유연하고 확장성 있는 Framework 설계

📌 다형성과 Adapter덕분에 기존 구조를 유지하면서, Framework의 기능을 확장할 수 있다.

profile
Backend 개발자가 되고 싶은

0개의 댓글