온라인 강의/Spring Framework

[스프링 MVC 1편] MVC 프레임워크 만들기

코드몬스터 2024. 5. 19. 12:33
728x90

정리

  • f else도 좋지만, 다형성으로 작성해보는 것도 좋다.

프론트 컨트롤러 패턴

FrontController 패턴 특징

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

프론트 컨트롤러 패턴 전/후

 

스프링 웹 MVC와 프론트 컨트롤러

  • 스프링 웹 MVC의 핵심도 바로 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 req, HttpServletResponse resp) throws ServletException, IOException {

        String requestURI = req.getRequestURI();

        ControllerV1 controllerV1 = controllerMap.get(requestURI);  // 부모(ControllerV1)는 자식(MemberController)들을 받을 수 있다.
        if (controllerV1 == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        controllerV1.process(req, resp);
    }

}

 

 


View 분리 - v2

모든 컨트롤러에서 뷰로 이동하는 부분이 중복이 있고, 깔끔하지 않다.

 

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

 

 

v2 구조

 

순서 1. Front Controller 호출

  • /front-controller/v2/*
@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 req, HttpServletResponse resp) throws ServletException, IOException {

        String requestURI = req.getRequestURI();

        ControllerV2 controllerV2 = controllerMap.get(requestURI);  // 부모(ControllerV1)는 자식(MemberController)들을 받을 수 있다.
        if (controllerV2 == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyView process = controllerV2.process(req, resp);
        process.render(req, resp);
    }

}

 

순서 2. Controller 호출

  • Myview 를 return
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");

    }
}

 

 

순서 3. 뷰 호출

  • Controller 에서 return 받은 Myview 인스턴스의 함수 render 실행
MyView process = controllerV2.process(req, resp);
process.render(req, resp);

 

public class MyView {

    private String viewPath;

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

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

    }

}

 

 


Model 추가 - v3

 

서블릿 종속성 제거

  • 현재 컨트롤러 입장에서는 HttpServletRequest, HttpServletResponse가 필요없다.
  • 요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면, 지금 구조에서 컨트롤러가 서블릿 기술을 몰라도 동작 가능하다.
  • request 객체를 Model로 사용하는 대신에 별도의 model 객체를 만들어서 반환하면 된다.
  • 즉, 우리가 구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경하자.
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    return new MyView( "/WEB-INF/views/save-result.jsp");
}

 

뷰 이름 중복 제거

  • 컨트롤러는 뷰의 논리 이름을 반환하고, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화
  • 이렇게 하면 향후 뷰의 폴더 위치가 함게 이동해도 프론트 컨트롤러만 고치면 된다.

 

v3 구조

 

ModelView

  • 지금까지 컨트롤러에서 서블릿에 종속적이 HttpServletRequest를 사용
  • Model도 request.setAttribute()를 통해 데이터를 저장하고 뷰에 전달했다.
  • 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View 이름까지 전달하는 객체를 만들어보자.
  • 논리이름을 물리이름으로 변경하는 것은 viewResolver가 된다.
public interface ControllerV3 {

    ModelView process(Map<String, String> paramMap);
}
@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 req, HttpServletResponse resp) throws ServletException, IOException {

        String requestURI = req.getRequestURI();

        ControllerV3 controllerV3 = controllerMap.get(requestURI);  // 부모(ControllerV1)는 자식(MemberController)들을 받을 수 있다.
        if (controllerV3 == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        HashMap<String, String> paramMap = createParamMap(req);
        ModelView mv = controllerV3.process(paramMap);

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

        myView.render(req, resp);

    }

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

    private static HashMap<String, String> createParamMap(HttpServletRequest req) {
        HashMap<String, String> paramMap = new HashMap<>();
        req.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName)));
        return paramMap;
    }

}

 

 


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

  • v3에서는 서블릿 종속성을 제거하고  뷰 경로의 중복을 제거를 했다
  • 컨트롤러 인터페이스를 구현하는 개발자 입장에서는, 항상 ModelView 객체를 생성하고 반환해야한다.
  • 개발자들이 편리하게 개발할 수 있는 v4 버전을 개발하자.
  •  v4는 ModelView를 반환하지 않고 ViewName 만 반환한다.

V4 구조

 

public interface ControllerV4 {

    String process(Map<String, String> paramMap, Map<String, Object> model);
}

 

public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    modelToRequestAttribute(model, request);

    RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
    requestDispatcher.forward(request, response);
}

 

public class MemberFormControllerV4 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);

        // return 한다.
        model.put("member", member);
        
        return "save-result";
    }

}

 

 


유연한 컨트롤러1 - v5

  • ControllerV3, ControllerV4는 완전히 다른 인터페이스이다. 따라서 호환이 불가능하다.
  • 어떤 개발자는 ControllerV3 방식으로 개발하고 싶고, 어떤 개발자는 ControllerV4 방식으로 개발하고 싶으면 어떻게 해야할까?

 

어댑터 컨트롤러

  • 핸들러 어댑터: 중간에 어댑터 역할을하는 핸들러 어댑터가 추가되었다. 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트로러를 호출 할 수 있다.
  • 핸들러: 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경되었다.

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

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 01. 핸들러 매핑 정보
        Object handler = getHandler(req);

        if (handler == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 02. 핸들러 어댑터 목록
        MyHandlerAdapter myHandlerAdapter = gethandlerAdapter(handler);

        // 03. 핸들러 어댑터 -> 핸들러 ->  ModelView 반환
        ModelView mv = myHandlerAdapter.handler(req, resp, handler);


        // 04. viewResolver
        String viewName = mv.getViewName();
        MyView myView = viewResolver(viewName);

        //05. render 호출
        myView.render(req, resp);
    }

    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 req) {
        String requestURI = req.getRequestURI();
        Object handler = handlerMappingMap.get(requestURI);  // 부모(ControllerV1)는 자식(MemberController)들을 받을 수 있다.

        return handler;
     }

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

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


}

 


유연한 컨트롤러2 - 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();
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // 01. 핸들러 매핑 정보
        Object handler = getHandler(req);

        if (handler == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 02. 핸들러 어댑터 목록
        MyHandlerAdapter myHandlerAdapter = gethandlerAdapter(handler);

        // 03. 핸들러 어댑터 -> 핸들러 ->  ModelView 반환
        ModelView mv = myHandlerAdapter.handler(req, resp, handler);

        // 04. viewResolver
        String viewName = mv.getViewName();
        MyView myView = viewResolver(viewName);

        //05. render 호출
        myView.render(req, resp);
    }

    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 req) {
        String requestURI = req.getRequestURI();
        Object handler = handlerMappingMap.get(requestURI);  // 부모(ControllerV1)는 자식(MemberController)들을 받을 수 있다.

        return handler;
     }

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

    private void initHandlerMappingMap() {

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


}