티스토리 뷰

프론트 컨트롤러 패턴 개요

 앞전에 우리는 MVC 패턴을 Servlet과 JSP를 이용하여 만들면서 몇가지 아쉬운점이 있다는 것을 발견했다.

 어떤 동작을 하는 컨트롤러를 생성할 때마다

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

와 같이 반복된 구문을 사용했고, 이러한 부분들이 많아질 수록 컨트롤러에서 공통으로 처리해야할 부분들이 늘어나 차후 유지보수단계에서 불편하게 될 것임을 예상했다.

 따라서 도입이 된 것이 프론트 컨트롤러이다.

 


 이전에는 클라이언트가 제각각 자기가 호출하고자하는 Controller를 직접 호출을 했었다. 따라서 공통된 부분이 있건 없건간에 항상 그 컨트롤러로 직통해서 연결하였고 공통된 부분을 통합하여 관리하는 것이 프론트 컨트롤러의 도입 배경이 되었다.


 마치 수문장처럼 입구에서 하나의 프론트 컨트롤러 서블릿이 요청받고 요청받은 내용을 가공하여 해당되는 각 컨트롤러에 넘겨주는 방식이다.

 이렇게 된다면 컨트롤러마다 servlet을 생성해서 여러 개의 servlet을 만들 필요없이 하나의 servlet만 만들면 되어서 나머지 컨트롤러는 굳이 servlet을 사용하지 않아도된다.

 

프론트 컨트롤러 도입 - v1

 단계적으로 MVC 패턴을 개선해보도록 한다. 먼저 프론트 컨트롤러를 도입한다.


먼저 FrontController를 Mapping 정보 분할로써 구현을 해보도록 한다.

FrontControllerServletV1
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 = "/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);
    }
}

 차후에 사용하기 위해서 Map에 사용할 Controller 생성자와 url명을 저장해둔다.

 

 가장 먼저 눈에 띄는 것은 urlPatterns다. /front-controller/v1/*로 되어있는데 이는 URL가 /front-controller/v1/ 하위 경로에 대한 모든 URL 요청을 이 controller에서 받겠다는 의미이다.

 

 다음으로 요청된 URI를 가져온다. URI란 URL이 가르키는 위치로 기존의 urlPatterns에 해당한다.

 예를들어 http://localhost:8080/front-controller/v1/members/new-form이라는 URL이 있다면 가르키는 위치는 /front-controller/v1/members/new-form이 되겠다.

 이후 Map에 맞는 Controller 객체를 생성한 후 null이라면 404에러를, 아니라면 request, response를 해당 controller process 메소드를 진행한다.

 

 이제 controller 하나하나를 쪼갤 시간이다. 인터페이트 컨트롤러를 도입해서 이 인터페이스로부터 구현을 강제화시켜서 로직의 일관성을 가져온다.

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

 

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

 기존의 MemberFormController와 큰 차이점은 없다. request, response를 받아서 자신이 원하는 viewPath를 생성해서 forward한다.

 나머지 Controller도 다 같은 방식이므로 생략한다.

 

 

 결국 중요한점은 FrontControllerServlet을 통해서 앞단에 도입부가 생겼고, 3개의 Controller가 각각의 Servlet을 새로 생성하는 것이 아닌 도입부의 Servlet만으로 동작하게 되었다는 것이다.

 

  1. FrontController에서 URL매핑 정보를 받는다.
  2. FrontController가 매핑 정보에 맞는 Controller들을 호출한다.
  3. 해당 Controller가 결과를 Forward한다.

 

View 분리 - v2

 이제 분할할 것은 view를 불러오는 영역이다. 지금까지 controller를 쓰면서 반복되었던 다음 구문이 있다.

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

 이걸 분리하기 위해서 이제 View 객체를 만들어보도록 한다.

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;
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 클래스에 통합되게 되었고 일일히 코드를 칠 필요없이 원하는 viewPath를 가지고 MyView 객체를 생성하고, render() 메소드를 통해서 forward하면 된다.

 실제코드에서는 그럼 어떻게 변화되었을까?

MemberFormControllerV2
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");
    }
}
MemberSaveControllerV2
package hello.servlet.web.frontcontroller.v2.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepsoitory;
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 MemberRepsoitory memberRepsoitory = MemberRepsoitory.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);
        memberRepsoitory.save(member);

        request.setAttribute("member", member);

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

 각각의 Controller들이 직접 Forward했던 이전과는 반대로 본인들이 희망하는 Path를 가지고 MyView 객체를 만들어서 return 하고 있다.

 즉 Forward 한다는 공통의 작업을 FrontControllerService에게 넘겼다는 것이다. 그럼 FrontControllerService를 보자.

FrontControllerServletV2
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 = "/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 {
        System.out.println("FrontControllerServletV2.service");

        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.process()를 사용하여 컨트롤러들이 view를 반환하도록 시켰다면 방금전에 말한대로 controller로부터 view "경로"에 대한 정보만 받는다.

 그리고 이를 보내는 Forward 즉, rendering작업은 받은 view를 통해서 FrontController가 직접 진행하게 되는 것이다.

 


 훨씬 깔끔해졌다. 각각의 컨트롤러가 제각각 Forword하는 것이아니라 본인이 원하는 Path주소만 보내고, FrontController는 주소 객체를 받아서 자기가 한번에 rendering 한다.

  1. FrontController에서 URL매핑 정보를 받는다.
  2. FrontController가 매핑 정보에 맞는 Controller들을 호출한다.
  3. Controller는 요청된 model과 Path를 담아서 FrontController로 전달한다.
  4. FrontController가 받은 view에 전송할 model을 랜더링한다.

 

Model 추가 - v3

 FrontController도 도입해서 입구를 만들었고, view를 분리해서 중복된 viewPath에 대한 내용도 일관되게 정리했다. 이제 보이는 것은 HttpServletRequestHttpServletResponse이다.

 이 둘이 꼭 필요할까?라는 의문이 생긴다. 지금까지 만들면서 Request는 setAttribute()해서 Model을 담는데에만 사용했고 Response는 쓰지도 않았다.

 

 이 둘을 사용하지 않고 자바의 Map으로만 파라메터 정보를 넘김으로써 서블릿을 몰라도 동작할 수 있도록 만들어보도록 한다. 한마디로 Model을 직접 만들어서 전송함으로써 Request, Response를 쓰지 않는 것이다. 따라서 지금부터 Model 객체를 만들기로 한다.

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;
    }
}
  • viewName : viewPath에서 view의 논리적인 이름을 지정한다.
  • model : 파라메터들을 포함해서 view로 보낼 Model들을 담는다.

 

 그럼 결국 Controller를 구현하기 위한 인터페이스 또한 return 값을 ModelView를 리턴해주어야한다.

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

 V2까지는 HttpServletRequsetHttpServletResponse를 입력으로 받아서 request에 viewPath를 담아서 전달했었다. 따라서 View객체에는 Path만 담고, 파라메터를 requeset에 담아놓기만하면 이를 FrontController에서 처리하는 방식을 사용했다.

 하지만 지금은 그 둘의 의존성을 없애기 위해서 변경하는 단계이기 때문에 사용할 파라메터를 Map에 받아서 입력받고, Model 객체를 따로 만들어서 리턴할 파라메터를 여기 담아서 return 하기로 한다.

 

 일단은 그럼 controller들을 먼저 보자.

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

 Form을 전달하는 MemberFormControllerV3은 viewPath만 전송하고 전송할 파라메터는 없던 컨트롤러이다. 따라서 ModelView에는 view값만 생성한 객체를 전달한다.

 

MemberSaveControllerV3
package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepsoitory;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.Map;

public class MemberSaveControllerV3 implements ControllerV3 {

    private MemberRepsoitory memberRepsoitory =  MemberRepsoitory.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);
        memberRepsoitory.save(member);

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

 member에 저장할 파라메터를 paramMap을 통해 받아서 memberRepository에 저장한다. 여기까지는 동작구조가 같지만 다음부터 달라진다.

 뿌려줄 viewPath를 가지고 Model 객체를 만든 다음 이 Model에 View에게 전달할 member의 파라메터 정보를 넣는다. 따라서 Key가 파라메터이름이 될것이고 value가 그 값이 될것이다.

 

MemberListControllerV3
package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepsoitory;
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 MemberRepsoitory memberRepsoitory = MemberRepsoitory.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {

        List<Member> members = memberRepsoitory.findAll();
        ModelView mv = new ModelView("members");
        mv.getModel().put("members", members);

        return mv;
    }
}

MemberSaveControllerV3과 결국 동일하다. return하는 값도 동일하며 결국 다른 것은 Model에 담는 것이 List객체라는 것이다.

 

FrontControllerServletV3
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 = "/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 {
        System.out.println("FrontControllerServletV3.service");

        String requestURI = request.getRequestURI();

        ControllerV3 controller = controllerMap.get(requestURI);
        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) {
        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;
    }
}

 이제부터 조금 복잡하지만 클라이언트로부터 받은 파라메터를 HttpServlestRequest를 사용하지 않고 controller로 넘기는것부터 차근차근 시작한다.

 

 HttpSevletRequest를 사용하지않을 것이기 때문에 controller들이 파라메터에 대한 정보가 필요해도 받을 곳이 없다. 따라서 Map<String, String> paramMap = createParamMap(request);를 이용해서 request에 있는 파라메터값들을 paramMap객체에 담는다.

 이걸 담는 메소드가 createParamMap()메소드의 내용이다. paramMap객체를 생성하고, request에 담긴 파라메터들을 Iterator를 통해서 다 담고 return해준다.

 

 controller들에게 전송해줄 파라메터를 만들었으니 controller에게 파라메터를 전송하고, controller들이 처리하고 나온 결과물인 ModelView를 받는다. 당연히 ModelView에는 ViewPath정보와, 만약 return할 파라메터값이 있다면 파라메터들이 Map에 담겨 전송된다.

 

 Model에 담긴 View는 전체 경로를 나타내는 변수가 아니라 중복된 경로를 제거한 논리적인 이름이 담긴 변수이다. 따라서 viewResolver를 통해서 실제 View 경로를 지정해준다.

 

 이제 모든 작업이 다끝났다. 결과물로 우리가 얻은 것은 View경로와 파라메터(request에 담기지않은 별개의)이다. 이를 이제 랜더링해주면 되는데, 원래 어떻게 보냈는지 잠깐 생각해보자.

public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
    dispatcher.forward(request, response);
}

 forward메소드는 request와 response를 받아서 전송한다. 따라서 일단 우리가 Model을 만들긴했지만 JSP에서는 전송할 때는 request에 담아서 전송을 할 수 밖에 없다.

 

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

private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
    model.forEach((key, value) -> request.setAttribute(key, value));
}

 따라서 render()메소드를 커스텀하는 작업이 필요하다. Model을 받아서 modelToRequestAttribute()메소드로 전달하여 request에 setAttribute로 파라메터들을 다 담고, 지금까지와 동일하게 reqeust와 response를 forward에 담아 전송한다.

 


일련의 과정들이 복잡했지만 정리하면 다음과 같다.

  1. FrontController에서 URL매핑 정보를 받는다.
  2. FrontController는 Controller가 사용할 paramMap을 request에서 뽑아서 객체를 만든다.
  3. FrontController는 paramMap 객체를 Controller에 전달한다.
  4. Controller는 paramMap에서 파라메터를 받아 요청을 수행한다.
  5. Controller는 요청된 model과 Path를 request가 아닌 ModelView객체에 담아서 FrontController로 전달한다.
  6. FrontController는 받은 ModelView에서 ViewPath를 만들고 model을 request에 담아 랜더링한다.

 

 

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

 v3까지 진행하면서 Servlet에 대한 종속성을 제거하고, 중복된 코드를 제거하는 등 잘 설계된 컨트롤러를 만들었다. 근데 사실 개발자가 쓰기 편하다 까지는 아직 무리가 있다. ModelView를 컨트롤러 생성마다 만들어야하는데 번거롭다.

 따라서 실제 구현하는 개발자들이 편하게 사용할 수 있게 v4로 업그레이드하고자 한다.

 

ControllerV4
package hello.servlet.web.frontcontroller.v4;

import java.util.Map;

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

 리턴값이 ModelView가 아닌 String이다. Model을 매개변수를 통해서 전달해주기 때문에, 매개변수에 파라미터를 넣어서 전달해주면 되고 필요한 뷰 네임을 String으로 리턴해주도록 한다.

 

MemberFormControllerV4
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";
    }
}
MemberListControllerV4
package hello.servlet.web.frontcontroller.v4.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepsoitory;
import hello.servlet.web.frontcontroller.v4.ControllerV4;

import java.util.List;
import java.util.Map;

public class MemberListControllerV4 implements ControllerV4 {

    private MemberRepsoitory memberRepsoitory = MemberRepsoitory.getInstance();

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {

        List<Member> members = memberRepsoitory.findAll();

        model.put("members", members);
        return "members";
    }
}

 너무 간단해졌다. v3에서는 ModelView 객체를 만들어서 내부에 View이름넣고, 파라미터 넣고 복잡한 과정이었지만 지금은 굳이 ModelView 객체를 생성하지 않고 받은 model Map에 파라미터를 넣고 리턴으로 View 이름을 보낸다.

 

FrontControllerServletV4
package hello.servlet.web.frontcontroller.v4;

import hello.servlet.web.frontcontroller.ModelView;
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 = "/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 {
        System.out.println("FrontControllerServletV4.service");

        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<>();    //model 객체를 추가해줌

        String viewName = controller.process(paramMap, model);

        MyView view = viewResolver(viewName);
        view.render(model, 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;
    }
}

 사실 Front Controller도 변경된건 거의 없다.

 지금 과정에서 변화한건 ModelView를 주고받는 것이 아닌 파라미터로 Model을 담을 공간을 주고, String으로 View 이름을 받는 것이기 때문에 model Map을 생성해서 컨트롤러에 전달하고, String으로 View 이름을 받아 model과 함께 이를 랜더링한다.

 

 사실 변경된건 크게없지만 모델을 직접 매개변수로 넘기고 View 이름만 String으로 바꾼다는 변화하나때문에 Controller 짜기가 훨씬 수월해졌다.

 

유연한 컨트롤러 - v5

 우리는 Controller v3도 개발했고 v4도 개발했다. 근데 Controller V3와 V4는 동시에 사용하지 못한다. 이 둘은 전혀 다른 인터페이스로 호환이 불가능하기 때문이다.

 이때 필요한 것이 어댑터이다.


 굉장히 복잡한 과정인거 같지만 내용자체는 간단하다. 일단 컨트롤러에서 핸들러로 개념을 확장하여 사용할 것이다. 왜냐하면 어댑터로 컨트롤러 뿐만아니라 어떠한 형태이건 간에 처리할 수 있어졌기 때문에 핸들러라 표현하는 것이다.

  1. request에서 주어진 매핑정보를 가지고 그에 맞는 핸들러(controller)를 가져온다.
  2. Front Controller가 핸들러를 처리할만한 어댑터를 들고온다.
  3. Front Controller가 핸들러 어댑터에게 핸들러를 전달하고 핸들러 어댑터가 핸들러를 실행한다.

 

 즉, "핸들러 어댑터"라는 중간과정이 생김으로써 어떤 핸들러가 나오건 핸들러 어댑터를 변경하면 처리할 수 있게 되는 것이다.

 

 그럼 과정에 따라서 Front Controller를 보자

FrontControllerV5
package hello.servlet.web.frontcontroller.v5;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
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.v4.controller.MemberFormControllerV4;
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 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 = "frontControllerV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerV5 extends HttpServlet {

    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
    private Map<String, String> paramMap;

    public FrontControllerV5() {
        initHandlerMappingMap();
        initHandlerAdapters();
    }

    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
    }

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

    @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 MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. hander = " + handler);
    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}

 

Object handler = getHandler(request);
if(handler == null) {
  response.setStatus(HttpServletResponse.SC_NOT_FOUND);
  return;
}

 FrontContoller가 실행되면 request를 가지고 handler를 받아야 한다.

private Object getHandler(HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    return handlerMappingMap.get(requestURI);
}
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());
}

 request에서 URI를 꺼내서 이제 내가 사용할 핸들러(Controller)를 꺼낸다. 이때 사용할 Controller들은 Map에 저장해둔 값을 꺼내는 것이다.

 그리고 만약 handler가 null이라면 handlerMap에서 해당 핸들러가 존재하지 않는 다는 뜻이기 때문에 404리턴해준다.

 

 

 이제 이 핸들러를 사용할 수 있는 어댑터를 찾을 시간이다.

MyHandlerAdapter adapter = getHandlerAdapter(handler);

private MyHandlerAdapter getHandlerAdapter(Object handler) {
    for (MyHandlerAdapter adapter : handlerAdapters) {
        if (adapter.supports(handler)) {
            return adapter;
        }
    }
    throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. hander = " + handler);
}

 

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.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 supports(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;
    }
}

 각각의 HandlerAdapter들은 MyHandlerAdapter 인터페이스를 상속받아 구현되었다.

 이때 supports에서 해당 핸들러가 이 어댑터를 instance of 한다면 true를 리턴한다. 지금 이 어댑터는 v3버전들의 process를 사요하기 때문에 ControllerV3 인터페이스로 검사한 모습니다.

 

 이제 사용할 어댑터까지 찾았다. 그럼 어댑터를 가지고 Model을 찾을 시간이다.

ModelView mv = adapter.handle(request, response, handler);

 handle 메소드는 request, response, handler를 받아서 동작한다. 사실 이후 동작 구조는 결국 FrontControllerV3에서 했던 내용의 반복이다.

 request로부터 받은 파라메터를 받고 이를 controller로 전달한다. controller는 받아서 Model과 View를 담은 ModelView 객체를 전달하고 이를 리턴한다.

 

 그럼 이제 FrontController도 할 일은 똑같다.

String viewName = mv.getViewName();
MyView view = viewResolver(viewName);

view.render(mv.getModel(), request, response);

 View이름 받아서 Myview객체를 만들고 model과 request, response를 가지고 랜더링한다.

 

 뭔가 조금 복잡한 구조가 된거 같지만 사실 V4를 포함한다면 좀 더 눈에 띄는 결과를 볼 수 있다. V5로 올린 이유가 결국 V3, V4 컨트롤러를 모두 통용하는 FrontController를 만들기 위함이기 때문이다.

FrontControllerV5
private void initHandlerAdapters() {
    handlerAdapters.add(new ControllerV3HandlerAdapter());
    handlerAdapters.add(new ControllerV4HandlerAdapter());
}

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

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

 새로 추가될 V4의 handlerAdapter와 Controller들을 포함해준다. 이후과정은 똑같고 이제 V4의 핸들러들을 찾아서 사용해 줄 Adapter 클래스를 만든다.

 

ControllerV4HandlerAdapter
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.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 ControllerV4HandlerAdapter implements MyHandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {

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

 handler를 받아서 ControllerV4의 모양에 맞게 castirng하고 나머지 내용은 FrontControllerServletV4에서 했던 내용과 같긴한데 마지막이 조금 다르다.

 이전에는 ViewName받아서 Myview만들고 model을 바로 랜더링했다.

 근데 지금 FrontControllerServletV5에서는 ModelView로 공통으로 받는다. 따라서 viewName으로 ModelView 객체를 만들고 model을 객체에 담아서 리턴한다.

 

 

 다양한 Controller의 형태를 받아들이기 위해서 개발했던 V5는 계속해서 새로운 형태의 controller가 들어왔을 때 이를 손쉽게 모듈화한다는 것을 알 수 있었다.

  1. FrontController에서 URL매핑 정보를 받는다.
  2. request에서 주어진 매핑정보를 가지고 그에 맞는 핸들러(controller)를 가져온다.
  3. FrontController가 핸들러를 처리할만한 어댑터를 찾아 온다.
  4. FrontController가 핸들러 어댑터에게 핸들러를 전달하고 핸들러를 실행한다.
  5. 이후 내용은 V3, V4에서 설명한 각각의 FrontController가 어댑터의 역할을 수행한다.

* 김영한님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술을 듣고 기록한 학습 자료입니다.

댓글
최근에 올라온 글
Total
Today
Yesterday