Servlet의 MVC Framework 만들기

최준호·2021년 7월 6일
0

Spring

목록 보기
14/47

지금까지 공부한 MVC

spring하면 mvc 패턴의 framework로 잘 알고 있다. 하지만 이번에는 spring은 사용하지 않고 지금까지 공부한 servlet을 활용하여 mvc 패턴을 구현해보려고 한다.

위 그림과 같이 기존의 MVC 패턴으로 구현한 Servlet은 client의 하나의 url 호출이 있을때 하나의 매칭되는 하나의 servlet에서 controller와 같이 정보를 넘겨주는 방식이였다. 하지만 작동하는 servlet을 url마다 생성해야하고 하는 동작은 url의 값을 읽어와 view와 연결시켜주는 코드는 사용자가 요청한 서비스 로직을 제외하면 계속해서 반복되어지는 코드가 발생했다. 그래서 이 상황을 더 개발자스럽게 해결하기 위해 FrontController 패턴이 등장했다.

FrontController 패턴 V1

  • 프론트 컨트롤러 서블릿 하나로 client의 모든 요청을 받음
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 공통 처리가 가능해짐
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러에서는 서블릿을 사용하지 않아도 됨

위 그림처럼 하나의 FrontController를 생성하여 모든 client의 요청을 하나의 서블릿에서 받아 각각 매칭되는 controller로 넘겨주는 역할을 하는 FrontController를 생성하였다. 이제 이 방식을 코드로 직접 작성해보자.

  1. Controller interface 생성

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

    controller 인터페이스를 생성하여 각각의 controller들은 로직의 일관성을 부여할 수 있다. 모든 controller는 ControllerV1을 상송박아 process에서 로직을 처리하는 것이다.

  1. 회원 등록 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);
            System.out.println("member = " + member);
            memberRepository.save(member);
            //Model에 데이터를 보관한다.
            request.setAttribute("member", member);
            String viewPath = "/WEB-INF/views/save-result.jsp";
            RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
            dispatcher.forward(request, response);
        }
    }

    로직은 기존의 servlet에서 처리하던 로직과 거의 동일하다. 그럼에도 우리가 이렇게 분리하는 이유는 하나의 servlet을 통해 각각의 controller들을 분리하기 위해서이다.

  2. FrontController 생성

    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 = "/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");
    
            //도메인 이후의 url 값을 가져옴
            String requestURI = request.getRequestURI();
    
            ControllerV1 controller = controllerMap.get(requestURI);
            
            //페이지에 대한 정보가 없을 경우 404 상태값 리턴
            if(controller == null){
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                return ;
            }
    
            controller.process(request, response);
        }
    }

    위 예제 코드는 한개이지만 이렇게 생성자에 여러 url들과 각각의 매칭 controller 객체들을 생성하여 map에 담아둔다. 그리고 servlet의 service에서는 client에서 요청한 url의 정보를 확인하여 매칭되는 controller의 정보를 가져와 해당 controller의 process를 실행시키는 과정이다.

  3. V1 모델

    V1 모델의 client 요청 과정을 살펴보면

    1. client의 HTTP 요청
    2. FrontController (=servlet)에서 url의 정보에서 controller의 정보를 가져옴
    3. Frontcontroller에서 url정보를 이용하여 controller의 process 실행
    4. 각각의 controller들의 process는 client가 요청한 정보의 결과값과 이동할 페이지의 값을 가지고 이동시켜줌

FrontController 패턴 V2

V2가 나온 이유는 모든 controller에서 view로 이동하는 부분이 계속해서 반복되는 코드가 되어버렸다.

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

다음 코드를 공통으로 처리하고 싶어진 것이다.

그럼 V2 패턴으로 다시 리팩토링해보자!

  1. MyView Class 생성

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

    MyView라는 class를 공통으로 사용하기위해 먼저 생성해둔다. MyView에서는 client로 전달될 view의 자원의 주소값과 dispatcher의 forward를 실행시켜주는 render()라는 메서드를 생성해둔다.

  2. 회원 등록 Controller 생성

    import hello.servlet.domain.member.Member;
    import hello.servlet.domain.member.MemberRepository;
    import hello.servlet.web.frontcontroller.MyView;
    import hello.servlet.web.frontcontroller.v1.ControllerV1;
    import hello.servlet.web.frontcontroller.v2.ControllerV2;
    
    import javax.servlet.RequestDispatcher;
    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);
            System.out.println("member = " + member);
            memberRepository.save(member);
            //Model에 데이터를 보관한다.
            request.setAttribute("member", member);
    
            return new MyView("/WEB-INF/views/save-result.jsp");
        }
    }

    그럼 기존의 v1과 차이점이 보일것이다.

    v1

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

    v2

    return new MyView("/WEB-INF/views/save-result.jsp");

    기존의 v1에서 다음과 같이 긴 코드를 MyView란 객체 생성을 통해 간결하게 변경된 부분을 확인할 수 있다.

  3. FrontController 생성

    import hello.servlet.web.frontcontroller.MyView;
    import hello.servlet.web.frontcontroller.v1.ControllerV1;
    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 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 = "/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 {
    
            //도메인 이후의 url 값을 가져옴
            String requestURI = request.getRequestURI();
    
            ControllerV2 controller = controllerMap.get(requestURI);
            
            //페이지에 대한 정보가 없을 경우 404 상태값 리턴
            if(controller == null){
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                return ;
            }
    
            MyView view = controller.process(request, response);
            view.render(request, response);
        }
    }

    FrontController의 경우도 코드가 변경되었는데

    MyView view = controller.process(request, response);
    view.render(request, response);

    마지막에 해당 코드가 변경되어졌다. 그 이유는 기존 process에서 처리되던 화면 렌더링 과정이 controller의 process에서 MyView객체를 반환받아서 해당 객체를 통해 render 메서드를 실행시켜야하기 때문이다. 이 방법을 통해 우리는 process에서 계속 반복해서 화면 렌더링 과정을 코드로 적어줬어야했는데 render라는 메서드 하나를 호출하는 것으로 줄여버릴 수 있게되었다.

  4. V2 모델

    V2 모델의 client 요청 과정을 살펴보자

    1. client의 HTTP 요청
    2. FrontController (=servlet)에서 url의 정보에서 controller의 정보를 가져옴
    3. Frontcontroller에서 url정보를 이용하여 controller의 process 실행
    4. process 메서드에서 MyView를 반환
    5. MyView의 render 메서드를 통해 view로 전달하는 과정을 메서드화

    v1과 v2의 차이점으로는 view로 전달하는 과정을 메서드화 시킴으로 인해 반복해서 구현해야했던 자원의 주소값과 해당 주소의 dispatcher.forward() 코드를 render()로 간편하게 구현할 수 있도록 변경되었다.

FrontController 패턴 V3

Servlet에서 모든 로직을 처리하던 과거 버전에서 이제는 servlet의 로직을 controller와 분리하여 서비스 로직은 controller의 process를 통해서 처리하게 되었다. 그런데 이렇게 변경이 되어지니 controller에서 굳이 HttpServletReqeust와 HttpServletResponse의 정보를 계속 가지고 있어야하는 것일까? 굳이 그럴 필요가 없다. 그래서 Servlet과 Controller를 완전히 분리하고자 V3패턴이 등장했다.

서블릿의 종속성을 제거함으로 인해 구현 코드도 매우 단순해지고 테스트 코드 작성이 매우 쉬워지게 된다.

  1. ModelView 생성

    지금까진 model의 정보를 담기 위해 request.setAttribute()를 통해서 정보를 담아 view에 전달했었다. 하지만 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고 View 이름까지 전달하는 객체를 ModelView라는 객체를 통해서 하려고한다.

    import lombok.Getter;
    import lombok.Setter;
    
    import java.util.HashMap;
    import java.util.Map;
    
    @Getter
    @Setter
    public class ModelView {
        private String viewName;
        private Map<String, Object> model = new HashMap<>();
    
        public ModelView(String viewName) {
            this.viewName = viewName;
        }
}
```

view의 자원의 주소를 찾기위한 viewName과 model에 담을 정보를 담기 위한 map을 선언해준다.
  1. ControllerV3 인터페이스 생성

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

    위에서는 ControllerV2는 생성 코드는 보여주지 않았다. 그 이유는 V1과 다르지 않기 때문이였다. 하지만 V3는 V1,V2와는 다르게 process에 paramMap이란 Map을 받아야한다. 이 paramMap은 기존 httpServlet을 받지 않기 위해 map으로 변경된 구조이다.

    v1, v2

    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;

    v3

    ModelView process(Map<String, String> paramMap) throws ServletException, IOException;
  2. 회원 등록 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");
            Integer age = Integer.valueOf(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;
        }
    }

    v2와 차이점부터 확인해보자!

    v2

    request.setAttribute("member", member);
    return new MyView("/WEB-INF/views/save-result.jsp");

    v3

    ModelView mv = new ModelView("save-result");
    mv.getModel().put("member", member);
    return mv;

    v2와 v3를 비교하면 v3가 더 길어진거 같다. 하지만 기존의 request에 model의 정보들을 담는 방식에서 ModelView객체를 통해 model이라는 map을 불러오고 해당 map에 우리가 view로 전달하려는 정보를 담아준다. 이것의 차이점은 이제 controller에서는 HttpServlet의 기능을 사용하지 않아도 된다는 것이고 그 점으로 인해 HttpServlet의 종속적이지 않게 되었다.
    v2와 v3를 비교하면 v3가 더 길어진거 같다. 하지만 기존의 request에 model의 정보들을 담는 방식에서 ModelView객체를 통해 model이라는 map을 불러오고 해당 map에 우리가 view로 전달하려는 정보를 담아준다. 이것의 차이점은 이제 controller에서는 HttpServlet의 기능을 사용하지 않아도 된다는 것이고 그 점으로 인해 HttpServlet의 종속적이지 않게 되었다.

  3. FrontController(=Servlet) 생성

    import hello.servlet.web.frontcontroller.ModelView;
    import hello.servlet.web.frontcontroller.MyView;
    import hello.servlet.web.frontcontroller.v2.ControllerV2;
    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 hello.servlet.web.frontcontroller.v3.controller.MemberFromControllerV3;
    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 = "/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 MemberFromControllerV3());
            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 {
    
            //도메인 이후의 url 값을 가져옴
            String requestURI = request.getRequestURI();
    
            ControllerV3 controller = controllerMap.get(requestURI);
            
            //페이지에 대한 정보가 없을 경우 404 상태값 리턴
            if(controller == null){
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                return ;
            }
    
            //paramMap
            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 MyView viewResolver(String viewName) {
            MyView view = new MyView("/WEB-INF/views/" + viewName + ".jsp");
            return view;
        }
    
        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;
        }
    }

    기존의 프론트 컨트롤러들에 비해 코드가 많이 복잡해진 기분이다... 하지만 어쩔 수 없다. 우리가 고생할 수록 사용자들이 편해진다고 한다. 우리가 고생해야한다. 우선 기존의 방식과 같이 url을 통해 controller들을 구별해내고 controller의 process를 실행시킬건데 우리는 더이상 process에 servlet을 매개변수로 사용하지 않고 우리가 만든 paramMap을 사용할것이다.

    • createParamMap 메서드

      request의 정보를 통해 paramMap에 request에 담긴 정보들을 그대로 가져와 다시 담고 반환해주는 메서드이다.

    createParamMap을 통해 우리가 사용할 map을 만들고 해당 controller의 process를 통해 ModelView 객체를 반환 받고 ModelView에서 viewName 정보를 가져와 viewResolver 메서드를 실행시킨다.

    • viewResolver 메서드

      기존의 중복으로 계속 적어줘야했던 "/WEB-INF/views/"와 ".jsp" 주소값을 중간의 중복되지 않던 viewName만 받아와서 MyView를 반환시켜주는 메서드이다.

    viewResolver를 통해 반환받은 MyView의 render를 통해 view로 정보를 전달해주는 과정이다.

  4. V3 모델

    V3 모델의 요청과정은

    1. client의 HTTP 요청
    2. FrontController (=servlet)에서 url의 정보에서 controller의 정보를 가져옴
    3. Servlet request에 담겨있는 정보들을 createParamMap 메서드를 통해 map 형태로 변환
    4. controller의 process를 servlet이 아닌 map을 매개변수로 사용하여 실행
    5. process 메서드에서 ModelView를 반환
    6. ModelView에서 MyView에 사용될 viewName의 정보를 가져옴
    7. viewResolver 메서드를 통해 공통으로 처리되는 부분을 처리한 후 MyView를 반환
    8. 반환 받은 MyView의 render 메서드를 통해 view로 전달

    V3 모델은 지금까지 사용되던 Servlet을 controller와 완전히 분리하기 위한 작업들과 viewResolver 메서드를 통해 중복으로 코딩해야했던 자원의 주소값을 간단하게 입력할 수 있도록 변경하였다.

FrontController 패턴 V4

앞서 만든 V3 컨트롤러는 서블릿의 종속성을 제고하고 뷰 경로의 중복을 제거하는 등 잘 설계된 컨트롤러이다. 하지만 항상 ModelView 객체를 생성하고 반환해야하는 부분이 조금 번거롭다. 좋은 아키텍쳐도 중요하지만 개발자가 쉽게 사용하는 실용성도 중요하기 때문에 이번 V4 패턴은 V3를 좀더 실용적으로 리팩토링하기 위한 버전이다.

  1. ControllerV4 생성

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

    V4는 ControllerV3와는 다르게 ModelView를 직접 반환하지 않고 String만 반환하게 설계한다. 또한 ModelView의 model을 직접 호출하여 정보를 담는 방식이 아니라 model 자체를 함께 받는 식으로 변경하였다. 반환하는 정보는 뷰의 이름으로 반환하려고 한다.

  2. 회원 등록 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");
            Integer age = Integer.valueOf(paramMap.get("age"));
    
            Member member = new Member(username, age);
            memberRepository.save(member);
    
            model.put("member", member);
            return "save-result";
        }
    }

    이제 컨트롤러에서는 전달 받은 model객체에 우리가 view로 전달하고자 하는 정보를 담고 view의 이름만 반환해주면 된다. 개발자 입장에서는 엄청 간편해졌다.

  3. V4 모델

    V4 모델의 요청과정은
    1. client의 HTTP 요청
    2. FrontController (=servlet)에서 url의 정보에서 controller의 정보를 가져옴
    3. Servlet request에 담겨있는 정보들을 createParamMap 메서드를 통해 map 형태로 변환
    4. controller의 process를 servlet이 아닌 map을 매개변수로 사용하여 실행
    5. process에는 paramMap 뿐 아니라 model Map과 함께 전달
    6. process 메서드에서 view에 전달하고자 하는 데이터는 model 객체에 담고 전달되는 페이지의 이름만 반환
    7. process에서 반환 받은 페이지의 이름을 viewResolver 메서드를 통해 공통으로 처리되는 부분을 처리한 후 MyView를 반환
    8. 반환 받은 MyView의 render 메서드를 통해 view로 전달

    V4 모델은 V3를 조금 리팩토링한 모델이므로 많은 수정은 없었다. 거의 동일한 동작을 수행하지만 개발자 입장에서는 엄청 간편하게 작동할 수 있도록 코드가 리팩토링되었다.

FrontController 패턴 V5

V4 패턴까지의 프론트 컨트롤러는 한가지의 컨트롤러 인터페이스만 사용할 수 있다. V3와 V4는 완전히 다른 방식으로 구현되어 있는 인터페이스라 호환이 불가능하다. 그래서 나온 방식이 어댑터 패턴이다. 어댑터 패턴을 사용하여 프론트 컨트롤러가 다양한 컨트롤러를 처리할 수 있도록 변경해보자.

  1. MyHandlerAdapter 인터페이스 생성

    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 support(Object handler);
    
        ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
    }
    

    MyHandlerAdapter에서는

    • support 메서드

      • handler는 support에 매개변수로 들어올 controller를 말한다.
      • 어댑터가 해당 컨트롤러를 처리할 수 있는 판단하기 위한 메서드이다.
    • handle 메서드

      • 어댑터는 실제 컨트롤러를 호출하고, 결과로 ModelView를 반환
      • 컨트롤러가 ModelView를 반환하지 못하면 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다.
      • 이전에는 프론트 컨트롤러가 직접 컨트롤러를 호출했지만 이젠 이 어댑터를 통해 컨트롤러를 호출한다.
  2. adapter 구현

    import hello.servlet.web.frontcontroller.ModelView;
    import hello.servlet.web.frontcontroller.v3.ControllerV3;
    import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
    
        @Override
        public boolean support(Object handler) {
            return (handler instanceof ControllerV3);
        }
    
        @Override
        public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
            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;
        }
    }

    V3를 사용하기 위한 adapter를 생성했다. 인터페이스에서 구현해놓은 메서드들을 실제로 구현해봤는데

    • support는 ControllerV3가 맞는지 확인하여 boolean으로 반환한다.
    • handle은 매개변수로 받은 handler 객체를 Controller로 변환해주고 createParameterMap을 생성해준 뒤 controller의 process에 주입해주고 반환 받은 ModelView를 반환시킨다. 여기서 V4라면 model 객체를 생성하여 process에 model을 함께 넣어주면 된다.
  3. FrontController 생성

    import hello.servlet.web.frontcontroller.ModelView;
    import hello.servlet.web.frontcontroller.MyView;
    import hello.servlet.web.frontcontroller.v3.controller.MemberFromControllerV3;
    import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
    import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
    import hello.servlet.web.frontcontroller.v4.controller.MemberFromControllerV4;
    import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
    import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;
    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 = "/front-controller/v5/*")
    public class FrontControllerServletV5 extends HttpServlet {
    
        private final Map<String, Object>  handlerMappingMap = new HashMap<>();
        private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
    
        public FrontControllerServletV5() {
            initHandlerMappingMap();
            intiHandlerAdapters();
        }
    
        private void initHandlerMappingMap() {
            handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFromControllerV3());
            handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
            handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
        }
    
        private void intiHandlerAdapters() {
            handlerAdapters.add(new ControllerV3HandlerAdapter());
        }
    
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            //도메인 이후의 url 값을 가져옴
            Object handler = getHandler(request);
    
            //페이지에 대한 정보가 없을 경우 404 상태값 리턴
            if(handler == null){
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                return ;
            }
            // adapter 가져오기
            MyHandlerAdapter adapter = getHandlerAdapter(handler);
            ModelView mv = adapter.handle(request, response, handler);
    
            String viewName = mv.getViewName();
            MyView view = viewResolver(viewName);
    
            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.support(handler)){
                    return adapter;
                }
            }
            throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
        }
    
        private MyView viewResolver(String viewName) {
            MyView view = new MyView("/WEB-INF/views/" + viewName + ".jsp");
            return view;
        }
    }
    • initHandlerMappingMap()

      서블릿에서 매핑되는 url의 정보와 url과 연결되는 컨트롤러의 정보를 담아준다.

    • intiHandlerAdapters()

      서블릿에서 사용할 어댑터의 정보들을 담아준다.

    위의 메서드들을 통해 각 컨트롤러와 어댑터의 정보를 담아준다. 그리고 service에서는

    1. getHandler : url을 통해 controller의 정보를 가져오는 메서드
    2. getHandlerAdapter : 가져온 controller가 adapter에서 지원하는지 확인 후 지원한다면 해당 어댑터를 반환
    3. 어댑터의 handle 메서드를 통해 실제 컨트롤러를 호출하고 그 결과를 어댑터에서 가공한 정보로 맞추어 반환한다.
    4. ModelView로 반환 받은 정보를 사용하여 viewName으로 viewResolver를 통해 공통 처리를 진행 후 MyView 반환
    5. MyView의 render를 통해 view로 전달

    이렇게 처리되는 과정을 다른 Controller를 추가하기 위해서 우리는 이제 dapter와 서블릿에 해당 adapter에 대한 정보를 추가해주는 것으로 다른 컨트롤러들을 모두 사용할 수 있게 되었다.

  4. V5 모델

    V5 모델의 요청과정은

    1. client의 HTTP 요청
    2. FrontController (=servlet)에서 url의 정보에서 controller의 정보를 가져옴
    3. 가져온 controller가 사용할 수 있는 adapter인지 조회
    4. 사용 가능한 adapter라면 핸들러 어댑터를 사용하여 controller를 호출
    5. 호출한 controller에서 반환 받은 정보를 adapter에서 재가공하여 ModelView로 반환
    6. 반환 받은 ModelView에서 ViewName을 viewResolver로 공통 처리 후 MyView 반환
    7. 반환 받은 MyView의 render를 통해 view로 정보 전달

    이렇게 MVC 패턴의 프레임워크를 직접 만들고 리팩토링을 통해 더 나은 방향으로 개선해봤다. 이 동작 구조는 다음에 공부할 Spring MVC 패턴과 같으므로 꼭 기억하고 다음 공부에서는 Spring은 어떻게 이걸 우리가 쉽게 사용할 수 있도록 해주었는지 확인해보자!

출처 https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글