WEB/Spring MVC 1

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(MVC 프레임워크 만들기)

Tony Lim 2021. 3. 29. 09:55

FrontController

프론트 컨트롤러의 도입함으로써 하나의 입구를 만들어 공통된 기능을 용이하게 처리할 수 있다.
프론트 컨트롤러를 제외한 나머지는 서블릿을 사용하지 않아도 된다.

스프링 웹 MVC의 DispatcherServlet이 FrontController 패턴으로 구현되어 있음.

 

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {

    private Map<String, ControllerV1> controllerMap = new HashMap<>();

    public FrontControllerServletV1() {
        controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
        controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
        controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServletV1.service");

        String requestURI = request.getRequestURI();

        ControllerV1 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        controller.process(request, response);
    }
}

V1 으로써 하나의 Controller 인터페이스를 통해서 각자 (멤버 저장, 멤버 리스트 등등) 필요한 컨트롤러를 구현한다.

후에 FrontController (유일한 servlet) 에서 매핑정보를 조회( HashMap 으로 <Url, 해당되는 객체> ) 하여 공통적으로 구현된 메서드를 호출 해준다.


 

View의 분리

모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있었다. String viewPath, foward 등등.

@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 {

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

Controller 단에서 MyView("/WEB-INF/views/new-form.jsp") 처럼 객채를 생성해서 return 해준다.

MyView 단에서 render 메소드 안에서 viewPath  == 위의주소 를 받아오고 그곳으로 forward를 호출해줌.
중복되는 코드를 지울 수 있게 되었다.

 

Model 추가 

 

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {

    private Map<String, ControllerV3> controllerMap = new HashMap<>();

    public FrontControllerServletV3() {
        controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
        controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
        controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        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 MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

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

서블릿 종속성 제거

요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 지금 구조에서는 컨트롤러가 서블릿 기술이 필요가 없다.
따라서 request 객체를 별도의 Model 객체를 만들어서 반환하면 된다. 지금은 ModelView로 논리이름 + 매핑된정보Model을 객체로 만들어 DTO 개념으로 전달한다.

뷰이름 중복제거

컨트롤러는 뷰의 논리 이름("new-form" 만) 을 반환하고 실제 물리위치의(WEB-INF~등등) 이름은 프론트 컨트롤러에서 처리하도록 단순화하자. 논리이름을 받은후에 viewResolver를 통해 물리뷰 경로를 배출한다. 

forward 전에 controller와 viewresolver를 통해 받은 model 을 request.setAttribute()를 통해 저장한다. 후에 view 와 함꼐 JSP로 포워드 해서 JSP를 렌더링 해준다.

 

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

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

기본적인 구조는 V3 과 같다. 대신에 컨트롤러가 ModelView를 반환하지않고 ViewName만 반환한다.

모델을 각 controller에서 만드는것이 아니라 FrontController에서 <String, Object> model을 만들어서 넘겨주면 각각의 Controller에서 저 model 에 put을 이용하여 요구되어진 정보들을 넣어준다.

render(model,request,response) 를 해주면 메소드 내부에서 forward 해준다.

 

유연한 컨트롤러 (V1~V5 등등 다양한 컨트롤러를 쓰고싶음)

현재 Controller V1,~V5 는 서로 완전히 다른 인터페이스이다. 호환이 불가능하다.

 

 

@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();
        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());
    }

    @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);

        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) {
        //MemberFormControllerV4
        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");
    }
}

 

어댑터 패턴 = 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경함

핸들러 어댑터 = 여기서 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러를 호출 할 수 있다.

핸들러 = 컨트롤러의 이름의 더 넓은 범위의 이름. 

requestUri를 보고 handler(Controller) 를 찾아옴 -> 이 handler를 handle할수있는 어댑터 조회 -> 어댑터가 실제 handler 호출함

 

SpringMVC도 핸들러 어댑터들을 통해서 @이 붙은 많은 Controller 다 쓸 수 있다. 전체적인 틀을 건드리지 않고

MyHandlerAdapter 라는 interface 는 supports(Handler) == 이 핸들러어뎁터가 해당 핸들러를 처리가능한건지 여부  , handle(request,response , Handler)  위의 support 를 통해 한번 거르기 때문에 해당 핸들러를 캐스팅해서 실제로 사용한다.           

request url 을 통해서 Handler를 받아오면 이게 어떤 HandlerAdapter를 통해서 구동이 가능한지를 HandlerAdapter 목록에서 찾아본다. 

그 HandlerAdapter를 통해서 받은 Handler를 구동시킨다. 

 

기능즉 새로운 V5 ,V6 가 계속 생겨도 메인로직(FrontController) 을 거의 건드리지 않고 Adapter들만 만들어서 넣어주기만 하면 잘 동작하는것을 확인할 수 있다.