article thumbnail image
Published 2022. 1. 24. 12:06
반응형

프론트 컨트롤러란?


  • 서블릿 하나로 클라이언트의 요청을 다 받아서 공통 처리를 중복없이 하기 위해 사용된다.
  • 이후 나머지 컨트롤러들은 서블릿이 사용하지 않는다.
    프론트 컨트롤러가 요청을 받고 응답하기 때문이다. 프론트 컨트롤러가 받아서 다른 컨트롤러를 사용하기 떄문이다.
  • 스프링도 DispatcherServlet이 FrontController이다.


출처 : 김영한의 MVC1편

  • 위와 같이 프론트 컨트롤러에 모든 요청이 오고 공통 작업을 처리하고 각 컨트롤러를 호출해 주는 것이다.

구조 살펴 보기

구조를 살펴보면 아래와 같다.

  1. 프론트 컨트롤러에 요청이 온다.
    → 프론트 컨트롤러는 HttpServlet을 상속 받는다.
  2. 프론트 컨트롤러는 컨트롤러들의 URL 관련 정보들을 갖고 있는데 여기서 조회를 한다.
    → interface에 컨트롤러들을 Map으로 갖고 있다.
  3. 매핑된 컨트롤러를 호출한다.
  4. 매핑된 컨트롤러는 공통이외의 본인의 역할을 수행한다.

그림을 보면 Controller는 인터페이스일 뿐이며 FrontController는 인터페이스에 의존할 뿐이고,
구현체들이 따로 존재하여 조회를 할 뿐이다. 프론트 컨트롤러를 간단하게 코드로 살표보자.

프론트 컨트롤러

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

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

    public FrontControllerServletV1() {
        controllerV1Map.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
        controllerV1Map.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
        controllerV1Map.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 = controllerV1Map.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        controller.process(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 MemberListControllerV1 implements ControllerV1 {

    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 viewPaht = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPaht);
        dispatcher.forward(request, response);
    }
}

View 역할 분리하기


프론트 컨트롤러 패턴으로 만들긴 했지만 다형성만 적용되었을 뿐 공통되는 코드가 제거 되지는 않았다.
구현체 코드를 보면 View 렌더링을 담당하는 부분이 공통되는 부분인데 이를 개선 시켜보자.

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

초록색 부분이 View역할을 분리한 부분이다.
Controller 인터페이스가 하던 일이 Controller 인터페이스는 MyView라는 뷰 역할을 담당하는
타입으로 반환만 해주었다. 이후 FrontController는 Myview에서 render()를 호출하여 JSP forward를 한다.

코드로 살펴보자.

프론트 컨트롤러

  • MyView view = controller.process(request, response); 이 부분이 변경이 되었다.
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {

    private Map<String, ControllerV2> controllerV1Map = new HashMap<>();

    public FrontControllerServletV2() {
        controllerV1Map.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
        controllerV1Map.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
        controllerV1Map.put("/front-controller/v2/members", new MemberListControllerV2());
    }

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

        MyView view = controller.process(request, response); // 이 부분이 MyView를 리턴하도록 바뀜.
        view.render(request, response);
    }
}

구현체 컨트롤러

  • 중복(공통) 되었던 코드가 사라졌고 MyView에게 Path만 전달하게 된다.
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 {

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

        // Model에 데이터를 보관한다.
        request.setAttribute("member", member);
        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}

MyView

  • 공통되는 부분이었던 forward 부분을 처리하는 render() 메서드를 활용하게 된다.
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);
    }
}

ModelView로 개선하기


현재 불편한 사항이 있다.

  • 컨트롤러가 서블릿에 종속
    • 컨트롤러가 서블릿에 종속되어 있다. 서블릿을 실제 사용하는 것은 프론트 컨트롤러인데
      다른 컨트롤러들도 파라미터로 받고 있기 때문에 서블릿에 종속되어 있다.
  • view의 경로가 중복
    • view 경로의 중복 /WEB-INF/views/이 있다.

이를 ModelView 객체로 개선해 해보자.

  • MyView
    → model이라는 map을 받아 JSP에 렌더링 하기위한 setAttribute model에 담아준다.
    이전에는 이를 컨트롤러에서 하고 있었다. 즉, 공통되는 부분을 MyView가 하도록 분리한 것이다.
public class MyView {

    private final String viewPath;

    public MyView(String viewPath) {
        this.viewPath = viewPath;
    }

    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) {
        for (String key : model.keySet()) {
            request.setAttribute(key, model.get(key));
        }
    }
}
  • Controller
    → 컨트롤러는 서블릿 객체가 필요한 것이 아닌, HTTP의 요청 파라미터만 필요한 것이다.
    그래서 종속성을 제거하기 위해 paramMap이라는 model을 파라미터로 받아
    본인의 로직을 처리하고, ModelView에 렌더링할 논리 화면 명만 넘겨준다.
    → 이제 setAttribute를 하지 않기 때문에 ModelView에서 이를 하도록 가공한 member 객체를 넣고 반환한다.

핵심은 컨트롤러는 이제 본인 로직 이외에는 아무것도 하지 않고, 서블릿에 종속되지 않고, ModelView에게 위임하는 것이다.

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;
    }
}
  • FrontController
    → createParamMap()을 사용하여 Http 요청 파라미터를 컨트롤러에게 넘겨 준다.
    → 컨트롤러가 생성해준 ModelView를 사용하여 viewResolver메서드에서 컨트롤러가 했던 viewPath를 설정한다.
    → 컨트롤러가 생성해준 ModelView를 사용하여 model을 추출하여 view에 렌더링 역할을 하는
    MyView에 model을 넘겨준다.

이 전 보다는 더 복잡해졌다. 하지만 프론트 컨트롤러는 공통되는 기능들을 관리하는 역할을 하기 때문에
다른 컨트롤러들이 편리하게 사용될 수 있다.

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

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

    public FrontControllerServletV3() {
        controllerV1Map.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
        controllerV1Map.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
        controllerV1Map.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 = controllerV1Map.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);
        Map<String, Object> model = mv.getModel();

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

더욱 실용적으로 컨트롤러 개선하기


지금 컨틀로러를 개발자가 사용하려면 불편한 점이 있다.
아래와 같이 객체를 생성하고 모델을 put을 해주어야 한다는 점이다.
이 부분을 프론트 컨트롤러가 해줄 수 있도록 개선 해보자.

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

구현체 컨트롤러

  • 바뀐 점은 model이라는 파라미터가 추가된 것이다.
    그래서 ModelView객체 생성이 필요없고, model에 넣어 주기만 하면 된다.
  • 반환도 화면명인 문자열만 리턴 해주면 된다.
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";
    }
}

프론트 컨트롤러

  • service 메서드만 살펴보면 단지 컨트롤러의 파라미터에 빈 model 객체만 넘겨준 것 뿐이다.
    기존과 다른 점은 mv.getViewName(), mv.getModel()을 없애고, 추가라는 주석 두 줄만 추가했을 뿐이다.
    빈 모델 객체를 넘겨준 것 만으로 컨트롤러가 실용적으로 사용할 수 있도록 개선된 것이다.
@Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServletV4.service");

        String requestURI = request.getRequestURI();

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

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

어탭터 패턴으로 프론트 컨트롤러 개선(여러 컨트롤러 호환하기)


지금은 컨틀러마다 인터페이스가 다르기 때문에 호환이 안된다.
그래서 특정 상황마다 다른 컨트롤러를 사용할 수가 없다. 그래서 중간에서 어댑터처럼
변환하여 주는 역할을 하는 어탭터 패턴으로 리팩토링 할 수 있다. 스프링에도 어댑터 패턴으로
RequestMappingHandlerAdapter같은 클래스들이 있다.

스프링도 아래와 완전히 같은 구조로 되어있다.

먼저 용어 정리를 하자.

  • 컨트롤러 → 핸들러로 칭한다.
    굳이 핸들러라고 칭하는 이유는 컨트롤러에만 국한된 것이 아니고,
    모든 적용할 수 있는 개념이기 때문에 핸들러라고 했다.

이제 알아 보도록 하자.

그림으로 알아 보자.

패턴은 거의 비슷하다. 다만 핸들러 어댑터가 프론트 컨트롤러와 핸들러 사이에 추가되고,
핸들러 어댑터 목록이 생긴것만 다르다. 그림 먼저 살펴 보자.

추가된 부분은 핸들러 어댑터 목록과 핸들러 어댑터이다.

핸들러 어댑터 목록

  • 어댑터들을 가진 List이다. 여기서는 프론트 컨트롤러가 가지고 있다.
    DI를 해도 되지만 연습용으로 쉽게 하였다.
  • 프론트 컨트롤러에서 목록 중 원하는 컨트롤러와 알맞은 어댑터를 조회한다.

핸들러 어댑터

  • 중간에 어댑터 역할을 한다. 여기서 어댑터 역할을 해주기 때문에 다양한 종류의
    컨트롤러를 호출할 수 있다.

핸들러

  • 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 어댑터가 있기 때문에
    꼭 컨트롤러의 개념 뿐 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.

나머지는 동일하다.

코드로 알아 보자.

프론트 컨트롤러

변경된 프론트 컨트롤러를 살펴 보자.

@WebServlet(name = "frontControllerV5", 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());

        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); // 1. 핸들러 매핑 정보 조회 : MemberFormControllerV3
        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 2. 핸들러 어댑터 목록 조회 : ControllerV3handlerAdapter
        MyHandlerAdapter adapter = getHandlerAdapter(handler); // 핸들러 어댑터 목록 조회 : (MemberFormControllerV3)
        // 3. handle 호출 ControllerV3handlerAdapter 에서 호출출
        // 5. ModelView 반환
        ModelView mv = adapter.handle(request, response, handler);

        String viewName = mv.getViewName();
        // 6. viewResolver호출
        // 7. MyView 반환
        MyView view = viewResolver(viewName);
        Map<String, Object> model = mv.getModel();

        // 8. 응답
        view.render(model, 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.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. = " + handler);
    }

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

간략히 코드를 설명 하자면

  1. 핸들러 조회

다른 유형의 컨트롤러를 추가 했다. 실제로 이런 식으로 추가 되는 것이다.
원래는 v3만 있었는데 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 Object getHandler(HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    return handlerMappingMap.get(requestURI);
}
  1. 핸들러를 처리할 수 있는 핸들러 어댑터 조회

v3, v4를 처리하는 핸들러 어댑터 목록이다. 여기서 처리하고 싶은 핸들러의 어댑터를 고른다.

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

private MyHandlerAdapter getHandlerAdapter(Object handler) {
    for (MyHandlerAdapter adapter : handlerAdapters) {
        if (adapter.supports(handler)) {
            return adapter;
        }
    }
    throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. = " + handler);
}
  1. 핸들러 어댑터에게 핸들러 처리 요청
  2. 핸들러 어댑터는 핸들러 호출

핸들러 어댑터는 내부적으로 자신의 핸들러를 호출하고, 필요한 어댑팅을 한다.

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

핸들러 어댑터는 아래와 같다.

public class ControllerV3handlerAdapter implements MyHandlerAdapter {

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

    // 4. handle 호출
    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        ControllerV3 controller = (ControllerV3) handler;

        Map<String, String> paramMap = createParamMap(request);
        // ControllerV3
        return controller.process(paramMap);
    }

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

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;
    }
}
  1. ModelView 반환

어댑터가 힘을 발휘하는 순간이다. 위에 코드에서 두 어댑터의 핸들러는
반환 값이 다르다. handle()메서드에서 controller.process()를 보자.
아래처럼 반환 값이 다르기 때문에 어댑터는 반환 값을 프론트 컨트롤러에서 요구하는
ModelView 타입으로 어댑팅을 해주는 것이다.
마치 110V → 220V로 변환 시키는 어댑터와 비슷하기 때문에 어댑터 패턴이라고 하는 것이다.

// v3 핸들러
return controller.process(paramMap)
// v4 핸들러
String viewName = controller.process(paramMap, model);

6 ~ 7은 동일한 처리기 때문에 설명하지 않겠다.

직접 만든 프레임워크와 스프링 MVC 비교

  • FrontController → DispatcherServlet
  • handlerMappingMap → HandlerMapping(inerterface)
  • MyHandlerAdapter → HandlerAdapter
  • ModelView → ModelAndView
  • viewResolver → ViewResolver(interface)
  • MyView → View(인터페이스)

디스패쳐 서블릿(DispatchServlet)


지금까지 위의 내용은 스프링 MVC를 아주 간단하게 만든 프레임워크이다.

반응형
복사했습니다!