FrontController

YH·2023년 4월 19일
0

✅ FrontController 패턴

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출해준다.
  • 공통 처리 기능으로 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
  • 스프링 웹 MVC의 핵심도 FrontController로, DispatcherServlet이 FrontController 패턴으로 구현되어 있음

✅ 프론트 컨트롤러 도입 - V1

@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);
    }
}
  • urlPatterns = "/front-controller/v1/*" : /front-controller/v1를 포함한 하위 모든 요청은 이 서블릿에서 받아들인다.
  • ex) /front-controller/v1 , /front-controller/v1/a , /front-controller/v1/a/b

✅ View 분리 - v2

  • 아래 뷰로 이동하는 코드가 모든 컨트롤러에서 중복으로 사용되므로 별도 뷰를 처리하는 객체로 분리한다.
    String viewPath = "/WEB-INF/views/new-form.jsp";
    RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
    dispatcher.forward(request, response);
//View 호출 용 공통 객체
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);
    }
}
//FrontControllerV2
@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 myView = controller.process(request, response);
        myView.render(request, response);
    }
}
  • 프론트 컨트롤러 적용으로 뷰 호출 용 공통 객체를 만들어 일관되게 처리할 수 있다.
  • 각 컨트롤러에서는 뷰 객체를 생성해서 반환만 하면 된다.

✅ Model 추가 - v3

  • 컨트롤러 입장에서는 HttpServletRequest, HttpServletResponse가 꼭 필요하지 않다.
  • request 객체를 Model로 사용하지 않고, 별도의 Model 객체를 만들어서 반환한다.
  • 컨트롤러가 서블릿을 사용하지 않도록 구현한다.
  • 이렇게하면 구현 코드도 단순해지고 테스트 코드 작성도 용이해진다.

@Getter
@Setter
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();

    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}
  • ModelView 에서는 뷰의 이름뷰를 렌더링할 때 필요한 model 객체를 가지고 있다.
//회원 저장 컨트롤러 V3
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;
    }
}
//FrontControllerV3
@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 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");
    }
}
//수정된 MyView
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);
    }

    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));
    }
}
  • createParamMap() : HttpServletRequest에서 파라미터 정보를 꺼내서 Map으로 변환한다. 그리고 해당 Map(paramMap)을 컨트롤러에 전달하면서 호출한다.
  • MyView view = viewResolver(viewName) - 뷰 리졸버 : 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다. 그리고 실제 물리 경로가 있는 MyView 객체를 반환한다.
    • 논리 뷰 이름 : members
    • 물리 뷰 경로 : /WEB-INF/views/members.jsp
  • view.render(mv.getModel(), request, response)
    • 뷰 객체를 통해서 HTML 화면을 렌더링 한다.
    • 뷰 객체의 render()는 모델 정보도 함께 받는다.
    • JSP는 request.getAttribute() 로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내서 request.setAttribute() 로 담아둔다. - modelToRequestAttribute()

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

  • V3 버전에서 항상 ModelView 객체를 생성하고 반환해야 하는 번거로운 부분을 좀 더 단순하고 편리하게 변경한다.

//회원 저장 컨트롤러 V4
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";
    }
}
//FrontControllerV4
@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");
    }
}
  • V4는 V3 버전과 거의 동일하다.
  • Map<String, Object> model = new HashMap<>(); 코드가 추가되었는데, 모델 객체를 프론트 컨트롤에서 생성해서 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다.
  • 컨트롤러가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해서 실제 물리 뷰를 찾을 수 있다.
    String viewName = controller.process(paramMap, model);
     MyView view = viewResolver(viewName);

✅ 유연한 컨트롤러1 - V5

  • 개발자에 따라서 V3 또는 V4를 사용하고 싶을 때 어떻게 해야할까? -> 어댑터 패턴을 사용하여 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경한다.

  • 핸들러 어댑터 : 중간에 어댑터 역할을 하며, 다양한 종류의 컨트롤러를 호출할 수 있다.
  • 핸들러 : 컨트롤러를 더 넓은 범위인 핸들러로 변경했다. 어댑터가 추가 되었으므로 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.
public interface MyHandlerAdapter {
    boolean supports(Object handler);

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws ServletException, IOException;
}
  • boolean supports(Object handler)
    • handler는 컨트롤러를 지칭한다.
    • 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메소드이다.
  • odelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
    • 어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환한다.
    • 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야한다.
    • 이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다.
/**
 * V3 버전을 지원하는 어댑터
 */
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {

	//ControllerV3을 처리할 수 있는 어댑터인지 판별
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
    
    	//핸들러를 V3 버전으로 변환하여 V3 형식에 맞게 호출한다.
        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;
    }
}
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/frontcontroller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

	//매핑 정보 값을 아무 값이나 받을 수 있도록 Object로 변경
    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());
    }

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

    @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)
        
        //실제 어댑터 호출, 어댑터는 handler(컨트롤러)를 호출하고 그 결과를 어댑터에 맞추어 반환한다.
        ModelView mv = adapter.handle(request, response, handler);
        
        MyView view = viewResolver(mv.getViewName());
        view.render(mv.getModel(), request, response);
    }
    
    /**
    핸들러 매핑 정보인 handlerMappingMap에서 URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환
    */
    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }
    
    /**
    handler를 처리할 수 있는 어댑터를 adapter.supports(handler) 를 통해서 찾는다.
    */
    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        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");
    }
}
  • 컨트롤러 -> 핸들러 : 이전에는 컨트롤러를 직접 매핑해서 사용했다. 하지만 이제는 어댑터를 사용하기 때문에 컨트롤러 뿐만 아니라 어댑터가 지원하기만 하면, 어떤 것이든 URL에 매핑해서 사용할 수 있다.

✅ 유연한 컨트롤러2 - V5

  • FrontControllerServletV5ControllerV4 기능도 추가한다.
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {

    //handler가 ControllerV4 인지 판별
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4);
    }

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

        //handler를 ControllerV4로 캐스팅
        ControllerV4 controller = (ControllerV4) handler;

        //paramMap, model을 만들어서 해당 컨트롤러로 호출
        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();

        //호출뒤 viewName을 반환 받음
        String viewName = controller.process(paramMap, model);

		/**
        어댑터가 호출하는 Controller4는 뷰 이름을 반환한다. 
        그러나 어댑터는 뷰의 이름이 아니라 ModelView를 만들어 반환해야 한다. 
        어댑터는 ModelView를 만들어 형식을 맞추어 반환한다.
        */
        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;
    }
}

✅ 정리

  • v1: 프론트 컨트롤러를 도입
    • 기존 구조를 최대한 유지하면서 프론트 컨트롤러를 도입
  • v2: View 분류
    • 단순 반복 되는 뷰 로직 분리
  • v3: Model 추가
    • 서블릿 종속성 제거
    • 뷰 이름 중복 제거
  • v4: 단순하고 실용적인 컨트롤러
    • v3와 거의 비슷
    • 구현 입장에서 ModelView를 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공
  • v5: 유연한 컨트롤러
    • 어댑터 도입
    • 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계

💡 지금까지 작성했던 코드들이 스프링 MVC 프레임워크의 핵심 코드와 구조의 축약 버전이다.


참고 Reference

  • 인프런 강의
profile
하루하루 꾸준히 포기하지 말고

0개의 댓글