Spring/[인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술]

[인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] MVC 프레임워크 만들기

h2boom 2024. 10. 3. 16:12

MVC 프레임워크 만들기

프론트 컨트롤러 패턴

  • Front Controller : 공통 로직을 프론트 컨트롤러가 모두 처리를 하며 클라이언트의 요청에 맞는 컨트롤러를 찾아서 호출해준다.
    • 입구를 하나로 할 수 있으며 공통 로직을 처리할 수 있게 된다.
    • 기존과 달리 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.

  • 스프링 MVC의 DispatcherServlet이 Front Controller 패턴으로 구현되어 있다.

프론트 컨트롤러 도입 - 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();
        ControllerV1 controller = controllerMap.get(requestURI);

        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        controller.process(request, response);
    }
}
  • urlPattern에서 v1/* 이라고 하면 v1을 포함한 v1 하위 URI까지 모두 포함하게 된다.
  • 각 Controller들은 인터페이스를 구현해서 만들어졌기에 다형성을 활용할 수 있다.
    • 해당 URI와 일치하는 컨트롤러를 Map에서 찾아서 참조를 반환해줌으로 다형성을 활용했다.

 

  • 리팩토링을 할 때 주의할 점
    • 같은 레벨끼리만 개선을 해야한다.
      • 구조를 개선할 때는 구조만 개선하고 기존 코드는 최대한 유지시켜야 한다.
      • 이후 문제가 없을 경우 코드 내 세부적인 것들을 개선해 나가는 것이 좋다.

View 분리 - V2

  • V1 코드와 달리 각 Controller에서 포워드를 하는 것이 아닌 MyView라는 포워드를 해주는 별도의 클래스를 사용한다.
    • 각 컨트롤러의 ViewPath 관련 중복 코드를 제거

Model 추가 - V3

  • FrontController에서만 서블릿을 사용하여 공통 로직을 처리하므로 각 컨트롤러는 request, response가 필요없기에 서블릿 종속성 제거
    • 요청 파라미터 정보는 Map으로 대신 넘긴다.
    • request의 Model 대신 별도의 Model을 만들어서 사용
  • View 경로 중 중복을 제거하고 컨트롤러에서 뷰의 논리 이름을 반환하도록 처리

 

  • 로직 중 디테일한 로직들은 별도의 메소드로 추출해서 사용하는 것이 좋다.

 

//Model 역할과 forward 역할을 하는 클래스
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;
    }
}
  • 별도의 model을 만들어서 데이터를 담아 view에 전달해준다.

 

//FrontController
@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;
        }

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

 

  • paramMap은 request의 파라미터로 넘어온 쿼리 파라미터 형식의 데이터를 저장하는 Map이다.
  • viewResolver() 메소드는 중복을 제거한 view 논리명을 view 물리 명으로 바꿔주는 메소드이다.
    • 나중에 경로가 변경되더라도 각 controller의 코드를 고치지 않고 viewResolver() 코드만 고치면 되기에 유지보수에 좋다. 
  • 직접 만든 model을 MyView로 넘겨주면 JSP에서 데이터를 사용하기 위해서 MyView에서 model의 모든 데이터를 꺼내 request.setAttribute()를 통해 뷰 렌더링에 필요한 model로 변환시켜주는 작업을 한다.

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

  • 단순하고 편리함을 위해 ModelView를 사용해서 반환해주지 않고 FrontController에서 빈 model 객체를 미리 넘겨주고 ViewName만 반환하도록 설계
    • FrontController에서 넘겨준 model 객체를 각 컨트롤러에서 값을 넣으면 그대로 담겨져 있다.

유연한 컨트롤러1 - V5

  • 어댑터 패턴을 통해 다양한 버전(방식)의 컨트롤러를 처리할 수 있도록 설계
  • 어댑터 패턴 : 호환되지 않는 인터페이스들을 연결하는 디자인 패턴이다.
  • 핸들러 어댑터 : 어댑터 역할을 하며 다양한 종류의 컨트롤러를 호출할 수 있도록 해준다.
  • 핸들러 : 컨트롤러보다 포괄적인 개념으로 유사하다.
  • 이전 버전들에서는 FrontController가 실제 컨트롤러를 호출했지만 V5에서는 어댑터를 통해서 실제 컨트롤러가 호출된다.

 

//어댑터 인터페이스
public interface MyHandlerAdapter {
    boolean supports(Object handler);

    ModelView handle(HttpServletRequest request, HttpServletResponse response,
                     Object handler) throws ServletException, IOException;
}
  • supports()는 컨트롤러를 파라미터로 넘겨서 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메소드이다.
  • handle()은 실제 컨트롤러를 호출하고 ModelView를 반환한다.

출처 : [인프런 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술]

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의 | 김영한 - 인프런

김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습

www.inflearn.com