IT recording...

[스프링 MVC1] 4. MVC 프론트 컨트롤러 패턴 본문

Spring

[스프링 MVC1] 4. MVC 프론트 컨트롤러 패턴

I-one 2022. 1. 14. 11:26

원문 링크

https://adorable-aspen-d23.notion.site/MVC1-4-MVC-c091e2d264854b0fbc735b0ae2ab3e96

 

[스프링 MVC1] 4. MVC 프론트 컨트롤러 패턴

Front Controller

adorable-aspen-d23.notion.site

김영한님의 [스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 강의를 듣고 작성한 글입니다.

Front Controller

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받는다.
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출한다.
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
  • 공통 처리가 가능하다.

1. 프론트 컨트롤러 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 {
        String requestURI = request.getRequestURI(); // /front-controller/v1/members/new-form 이런 부분 얻어옴
        ControllerV1 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        controller.process(request, response); //controller 호출
    }
}
  • controllerMap 을 이용하여 url주소와 해당하는 컨트롤러를 매핑해놓는다.
  • 이후 요청이 들어오면 controllerMap을 뒤져 알맞은 컨트롤러를 찾는다.
  • 해당 컨트롤러의 process를 실행한다.
public interface ControllerV1 {
    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
public class MemberSaveControllerV1 implements ControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //argument
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));
				//save
        Member member = new Member(username,age);
        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);
    }
}

2. 프론트 컨트롤러 - V2

: 모든 컨트롤러에 뷰로 이동하는 부분의 중복이 존재하므로 이를 없애보자

→ 뷰를 처리하는 객체를 생성한다.

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 interface ControllerV2 {
    **MyView** process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
  • 각 컨트롤러에서 MyView를 반환한다.
public class MemberSaveControllerV2 implements ControllerV2 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //save
        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")**;
    }
}
@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(); // /front-controller/v2/members/new-form 이런 부분 얻어옴
        ControllerV2 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        **MyView view** = controller.process(request, response);//controller 호출
        **view.render(request,response);**
    }
}

⇒ MyView객체의 render() 호출 부분을 일관되게 처리할 수 있다.

⇒ 각각의 컨트롤러는 MyView객체를 생성만 해서 반환하면 된다.

3. 프론트 컨트롤러 - V3

  • 서블릿 종속성 제거→ 요청 파라미터 정보는 자바의 Map을 이용해서 넘기고, request 대신 별도의 Model을 이용한다.
  • : 모든 컨트롤러가 HttpServletRequest, HttpServletResponse를 필요로 하지는 않는다. 없애보자!
  • 뷰 이름 중복 제거→ viewResolver 사용
  • : prefix, suffix를 제거하고 뷰의 논리적 이름만 넘겨줘도 되게 하자!
@Getter
@Setter
public class ModelView {
    **private String viewName;
    private Map<String,Object> model = new HashMap<>();**

    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}
public interface ControllerV3 {
    **ModelView** process(Map<String,String> paramMap); //request,response를 삭제함으로써 servlet에 종속적이지 않게 됨
}
public class MemberSaveControllerV3 implements ControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public ModelView process(**Map<String, String> paramMap**) {
        //param 받아오기
        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;
    }
}
@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(); // /front-controller/V3/members/new-form 이런 부분 얻어옴
        ControllerV3 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        **//파라미터 뽑기
        Map<String, String> paramMap = createParamMap(request);**

        //ModelView에 뷰의 논리이름과 + jsp에서 필요로하는 데이터 넣어옴
        **ModelView** mv = controller.process(paramMap);//controller 호출

        //viewResolver호출해서 논리 이름 -> 실제 주소로 변경하기
        String viewName = mv.getViewName();
        MyView myView = **viewResolver(viewName);**

        //렌더링
        myView.render(mv.getModel(),request,response); //model 정보도 함께 전달
    }

    private MyView viewResolver(String viewName) {
        MyView myView = new MyView(**"/WEB-INF/views/" + viewName + ".jsp"**); //중복되는 부분 해결
        return myView;
    }

    private Map<String, String> createParamMap(HttpServletRequest request) {
        //paramMap
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator() //request에 있는 모든 파라미터 정보들 가져오기
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

⇒ 컨트롤러에서 서블릿 종속성을 제거했으므로 구현이 매우 단순해지고, 테스트 코드 작성이 쉬워진다.

→ HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아서 호출해주면 된다.

→ 응답 결과로 뷰 이름과 뷰에 전달할 Model 데이터를 포함하는 ModelView객체를 반환한다.

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

: 개발자 입장에서 항상 ModelView객체를 생성하고 반환해야 하기 때문에 개발자들이 편리하게 사용할 수 있는 버전을 만들어보자!

public interface ControllerV4 {
    /**
     *
     * @param paramMap
     * @param model
     * @return viewName
     */
    String process(**Map<String,String> paramMap, Map<String,Object> model**);
}
public class MemberSaveControllerV4 implements ControllerV4 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        //param 받아오기
        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); //모델은 put으로만, 따로 전달X
        **return "save-result";**
    }
}
@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(); // /front-controller/v4/members/new-form 이런 부분 얻어옴
        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<>(); //추가

        //controller 호출, viewName 리턴해옴, model은 파라미터로 넘어옴
        **String viewName** = controller.process(paramMap,model);//controller 호출

        //viewResolver호출해서 논리 이름 -> 실제 주소로 변경하기
        MyView myView = **viewResolver**(viewName);

        //렌더링
        myView.render(model,request,response); //model 정보도 함께 전달
    }

    private MyView viewResolver(String viewName) {
        MyView myView = new MyView("/WEB-INF/views/" + viewName + ".jsp"); //중복되는 부분 해결
        return myView;
    }

    private Map<String, String> createParamMap(HttpServletRequest request) {
        //paramMap
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator() //request에 있는 모든 파라미터 정보들 가져오기
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

⇒ 각 컨트롤러에서 ModelView를 생성하는 것이 아니라 뷰의 논리 이름만 반환하면 되게 돼서 단순하고 실용적이게 되었다. (By. 기존 구조에서 모델을 파라미터로 넘기고 뷰의 논리 이름 반환 아이디어)

5. 유연한 컨트롤러 - V5

: 어떤 개발자는 ControllerV3를 사용하고 싶고, 어떤 개발자는 ControllerV5를 사용하고 싶다면 어떻게 해야할까?

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

⇒ 어댑터 패턴 사용!

: 다른 인터페이스를 사용하는 컨트롤러들을 한 프론트 컨트롤러에서 사용할 수 있도록 호환이 가능하게 변경해보자.

  • 핸들러 어댑터 : 어댑터 역할을 해주어서 다양한 종류의 컨트롤러를 호출할 수 있다.
  • 핸들러 : (구 컨트롤러) 어댑터가 존재하기 때문에 꼭 컨트롤러의 개념이 아니어도 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있다.
public interface MyHandlerAdapter {
    boolean supports(Object handler);
    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
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;
				
				//V3방식
        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);

        return mv;
    }

    private Map<String, String> createParamMap(HttpServletRequest request) {
        //paramMap
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator() //request에 있는 모든 파라미터 정보들 가져오기
                .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<>(); //추가

				//V4방식
        String viewName = controller.process(paramMap, model);
        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        return mv;
    }

    private Map<String, String> createParamMap(HttpServletRequest request) {
        //paramMap
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator() //request에 있는 모든 파라미터 정보들 가져오기
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }

    private MyView viewResolver(String viewName) {
        MyView myView = new MyView("/WEB-INF/views/" + viewName + ".jsp"); //중복되는 부분 해결
        return myView;
    }
}
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
    private final Map<String,Object> handlerMappingMap = new HashMap<>(); //어떤 버전의 컨트롤러도 다 object에 들어갈 수 있다.
    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);**
        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        //어댑터목록에 존재하는지 확인
        **MyHandlerAdapter adapter = getHandlerAdapter(handler);**

        //어댑터에서 handle 호출
        **ModelView mv = adapter.handle**(request, response, handler);

				//공통 부분
        //viewResolver호출해서 논리 이름 -> 실제 주소로 변경하기
        String viewName = mv.getViewName();
        MyView myView = viewResolver(viewName);

        //렌더링
        myView.render(mv.getModel(),request,response); //model 정보도 함께 전달
    }

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

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI(); // /front-controller/V3/members/new-form 이런 부분 얻어옴
        Object handler = handlerMappingMap.get(requestURI);
        return handler;
    }

    private MyView viewResolver(String viewName) {
        MyView myView = new MyView("/WEB-INF/views/" + viewName + ".jsp"); //중복되는 부분 해결
        return myView;
    }
}

  • handlerMappingMap에 해당하는 url과 해당하는 controller(handler)를 매핑해놓는다.
  • 핸들러를 찾았으면 그 핸들러가 우리가 제공하는 adapter에 적용되는 핸들러인지 검사한다.
  • 어댑터의 handle을 호출한다.
    • 그 handle에서는 해당하는 핸들러의 모양에 맞춰서 process함수를 호출한다.
  • Adapter가 제공한 데이터를 받은 FrontController는 이후 공통적인 부분을 수행한다.

6. 스프링 MVC

: 스프링 MVC는 앞에서 우리가 구축한 프레임워크와 매우 유사하게 설계되어 있다.

다음 시간에는 이 프레임워크를 스프링이 애노테이션을 어떻게 기가막히게 써서 개발하기 편하게 했는지 살펴보자.

Comments