-
SpringMVC (4) - MVC 프레임워크 만들기 [중요]Web/Spring 2023. 10. 2. 14:51
1. 프론트 컨트롤러 패턴 소개
- FrontController 패턴의 특징
> 하나의 서블릿으로 클라이언트 요청 받기
> 요청에 맞는 컨트롤러 찾아서 호출 (pojo)
> 입구를 하나로 만드는 것이 핵심 (공통처리,pojo를 활용한 확장성 < 서블릿 의존성 제거)
> 스프링 MVC의 핵심은 FrontController이다.
2. 프론트 컨트롤러 V1
v1의 구조
- V1의 핵심은 서블릿 1개로 만들고 컨트롤러 분리해서 처리하기이다.
- 기존 프론트 컨트롤러에 맵핑과 서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입
-컨트롤러 v1
public interface ControllerV1{ void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; }
- 각 컨트롤러는 위 인터페이스를 구현하여, 로직의 일관성 가져갈 수 있다.
- 프론터 컨트롤러로 들어온 request와 response를 넘겨주면, 서블릿에서 처럼 꺼내쓸 수 있음
> 회원 등록 컨트롤러
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); } }
- 서블릿에서 수행하던 로직을 옮긴 것 뿐 -> 단 수행 객체는 서블릿이 아닌 pojo
> frontContrllerv1
@WebServlet(name="frontcontrollerServletV1", urlPatterns="/front-controller/v1/*") public class FrontControllerServletV1 extends HttpServlet{ private Map<String,ControllerV1> controllerMap = new HashMap<>(); public FrontControllerServletV1(){ controllerMap.put("urlPath",new MemberFormControllerV1()); //url에 따라 ControllerV1들 맵핑 } @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String Uri = request.getRequestURI(); controllerV1 controller = controllerMap.get(Uri); if(controller == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } controller.process(request,response); } }
- 프론트 컨트롤러는 uri를 얻어서 맵핑 정보 중 해당 uri와 맵핑된 컨트롤러 찾아서 request,response 전달만 수행
- /*패턴으로 해당 url의 하위 요청 모두 받게 만듦
* 아직 뷰로 이동하는 부분 forward부분 중복됨 ! 중복 제거해보자
3. 프론트 컨트롤러 V2
- 모든 컨트롤러에서 뷰로 이동하는 부분 중복을 제거해보자
- 별도로 뷰를 처리하는 객체 만들기
- 핵심은 각 컨트롤러의 포워딩 부분을 MyView가 수행하도록하고, 각 컨트롤러는 viewPath를 담은 MyView를 리턴하게 함
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 ds = request.getRequestDispathcer(viewPath); ds.forward(request,response); }
- 컨트롤러 v2
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; }
-v2 회원 등록 컨트롤러
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 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"); } }
- 회원 목록 컨트롤러
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"); } }
- 컨트롤러들이 직접 포워딩하지 않고, 포워딩 역할을 수행할 수 있는 MyView객체에 viewPath등록해서 return
- 프론트 컨트롤러 V2
@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); } }
- 기존 V1은 process만 호출했다면, V2는 process호출해서 MyView객체를 전달받고, 이를 통해 render를 호출하는 형식으로 바뀌었다.
- 이를 통해 각 컨트롤러는 중복되는 포워딩 부분을 Myview객체에게 맡기고, MyView만 반환하는 형식으로 교체됨
- 여기까지는 model사용안하고, request에 직접 저장
4. V3 Model 추가 (컨트롤러 서블릿 종속성 제거 - 매우 중요)
- 기존 컨트롤러는 process부분이 서블릿의 service와 매우 유사하여 서블릿 종속적인 컨트롤러였다.
- 이는 컨트롤러를 테스트하기 어렵게 만들고, request 밑 response를 사용하지 않아도 매개변수로 선언하게 하여 유연성을 낮춘다 이를 개선해보자.
- 또한 prefix와 suffix를 지정하여 view이름 중복도 제거해 보자
- model을 도입 컨트롤러는 request를 통해 파라미터 전달받지 않고, request에 처리결과를 저장하지 않기 때문에, request 필요없음
* 두가지 모델임 파람모델,저장모델
- 핵심은 프론트컨트롤러가 request로 전달받은 파라미터 모두 꺼내서 새로운 Map에 담아 컨트롤러에 전달 ->
컨트롤러는 전달받은 Map을 통해 로직 처리 후 Model객체를 생성하여 결과를 담고 return
-> 프론트 컨트롤러는 return받은 model을 view에 전달
> ModelView : request대신에 로직 처리 결과를 담고, viewPath를 담을 객체이다.
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() {this.viewName = viewName;} public Map<String,Object> getModel() {return model;} public void setModel(Map<String,Object> model) {this.model = model;}
* Map model은 request.setAttribute와 정확하게 같은 동작 수행
- 컨트롤러 V3
public interface ControllerV3 { ModelView process(Map<String, String> paramMap); }
- 서블릿 종속성 제거, 처리 결과 ModelView반환
- 회원 등록 컨트롤러
public class MemberFormControllerV3 implements ControllerV3 { @Override public ModelView process(Map<String, String> paramMap) { return new ModelView("new-form"); } }
- 회원 저장 컨트롤러
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; }
- 모델에 뷰에 필요한 member객체 담아서 mv리턴
-회원 목록 컨트롤러
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; } }
- 프론트 컨트롤러 v3
@WevServlet(name ="frontControllerServletV3",urlPatterns = "/front-fontroller/v3/*") public class FrontControllerServletV3 extends HttpServlet{ private Map<String, ControllerV3> controllerMap = new HashMap<>(); public FrontControllerServletV3(){ //컨트롤러 v3와 url맵핑 } @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ String requestUri = request.getRequestURI(); ControllerV3 controller = controllerMap(requestURI); if(controller == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } Map<String,String> paramMap = new HashMap<>(); getParam(paramMap,request); ModelView mv = controller.process(paramMap); MyView view = viewResolver(mv.getViewName()); view.rander(mv.getModel(),request,response); } private Map<String,String> getParam(Map<String,String> paramMap,HttpServletRequest request) throws ServletException{ request.getParameterNames().asIterator().forEachRemaining(name -> paramMap.put(name,request.getParameter(name)); } private MyView viewResolver(String viewName){ return new MyView("/WEB-INF/views/"+viewName+".jsp"); } }
- paramMap에 request에 담긴 파라미터 전부 넘기기
- 뷰 리졸버를 통해 뷰이름 중복 제거, 이제 컨트롤러는 뷰의 논리 이름만 return하면됨
- view.render를 통해 HTML화면 랜더링함
- MyView에 추가 된 메서드
public void render(Map<String,Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOExcpetion{ model.forEach((key,value) -> request.setAttribute(key,value)); //포워딩하는 것은 동일 }
- jsp는 request에 저장된 값을 꺼내기 때문에 따로 mv에 저장된 값을 다시 request로 옮기는 과정이 필요함
5. 단순하고 실용적인 컨트롤러 v4
- 앞서 만든 v3컨트롤러는 서블릿 종속성을 제거하고, 뷰 경로의 중복을 제거했음
- 한발 더 나아가서 컨트롤러가 ModelView를 리턴하는게 아니라 viewName만 리턴하도록 바꿔보자
- 핵심 로직은 request요청 정보를 담은 paramMap과 처리 결과를 담을 model을 컨트롤러에 매개변수로 주입하고, 컨트롤러는 메서드 실행결과로 viewName만 반환하도록 함
- 컨트롤러 인터페이스 v4
public interface ControllerV4 { /** * @param paramMap * @param model * @return viewName */ String process(Map<String, String> paramMap, Map<String, Object> model); }
- 회원 등록 컨트롤러
public class MemberFormControllerV4 implements ControllerV4 { @Override public String process(Map<String, String> paramMap, Map<String, Object> model) { return "new-form"; } }
- 회원 저장 컨트롤러
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"; } }
- 회원 목록 컨트롤러
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"; } }
- 프론트 컨트롤러 v4
@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를 생성해서 return하는게 아니라,
- 프론트 컨트롤러로부터 주입받은 model에 처리결과를 담고, viewName을 리턴
- viewName과 ViewResolver를 통해 MyView객체를 만들고, model을 넘겨서 처리
참고자료: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1
'Web > Spring' 카테고리의 다른 글
SpringMVC (5) - 스프링 MVC 구조 이해 (0) 2023.10.02 SpringMVC (4.5) - FrontController V5 [어댑터 패턴 - 중요] (0) 2023.10.02 Spring MVC (3) - 서블릿, JSP, MVC 패턴 (0) 2023.10.02 SpringMVC (2) - 서블릿 (0) 2023.10.02 스프링 MVC (1) 웹 애플리케이션 이해 (0) 2023.09.04