ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SpringMVC (4) - MVC 프레임워크 만들기 [중요]
    Web/Spring 2023. 10. 2. 14:51

     

    1. 프론트 컨트롤러 패턴 소개

     

    출처: 스프링MVC(인프런) - 김영한

     

    - FrontController 패턴의 특징

    > 하나의 서블릿으로 클라이언트 요청 받기

    > 요청에 맞는 컨트롤러 찾아서 호출 (pojo)

    > 입구를 하나로 만드는 것이 핵심 (공통처리,pojo를 활용한 확장성 < 서블릿 의존성 제거)

    > 스프링 MVC의 핵심은 FrontController이다.

     

    2.  프론트 컨트롤러 V1

     

    v1의 구조 

     

    출처: 스프링MVC(인프런) - 김영한

    - 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의 구조 

    출처: 스프링MVC(인프런) - 김영한

    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에 전달 

     

    출처: 스프링MVC(인프런) - 김영한

    > 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만 반환하도록 함 

     

    출처: 스프링MVC(인프런) - 김영한

    - 컨트롤러 인터페이스 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

     

    스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

    웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 원

    www.inflearn.com

     

Designed by Tistory.