[Spring MVC 1편] 4. MVC 프레임워크 만들기

HJ·2022년 8월 15일
0

Spring MVC 1편

목록 보기
4/8

김영한 님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard


1. Front Controller 패턴

  • Front Controller가 모든 클라이언트의 요청을 받고, 공통 로직을 처리

  • Front Controller 서블릿 하나로 클라이언트의 요청을 받음

  • Front Controller가 요청에 맞는 컨트롤러를 찾아서 호출

  • Front Controller를 제외한 나머지 Controller는 서블릿을 사용하지 않아도 된다

  • 스프링 웹 MVC의 DispatcherServlet이 FrontController 패턴으로 구현되어 있음




2. 1단계> Front Controller 도입

2-1. 동작 방식

  • 매핑 정보 : 어떤 URL이 들어오면 어떤 Controller를 호출해야하는지에 대한 정보

2-2. FrontController 코드

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

    // 매핑 정보
    private Map<String, ControllerV1> controllerMap = new HashMap<>();

    // 요청 URL에 따른 Controller 매핑
    public FrontControllerServletV1() {
        controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
        ...
    }

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

        String requestURI = request.getRequestURI();

        // URI에 해당하는 Controller 찾기
        ControllerV1 controller = controllerMap.get(requestURI);
        ...

        // Controller 호출
        controller.process(request, response);
    }
}
  • HttpServlet을 상속받음

  • @WebServlet을 통해 모든 경로에 대한 요청을 받음

  • Map<String, ControllerV1>을 이용해 경로에 따른 Controller 매핑

    • key = URL

    • value = Controller 생성

  • Controller의 process()를 호출

    • Map에서 요청 URL에 맞는 Controller 구현체를 인터페이스로 받는다 ( 다형성 )

    • process() 호출 시 요청 파라미터 정보가 필요한 Controller가 있기 때문에 request, response 객체를 넘겨준다


2-3. Controller 코드

public class MemberSaveControllerV1 implements ControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // reqest의 요청 파라미터 꺼내기
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        // request에 모델 데이터 저장
        request.setAttribute("member", member);

        // JSP 실행
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
  • Controller 객체는 모두 동일한 Controller 인터페이스 ( ControllerV1 )를 구현

  • Controller 인터페이스의 process()는 FrontController의 service()와 유사한 형태로 작성

    • FrontController를 통해 전달받은 request 객체의 요청 파라미터 정보를 뽑아서 사용

    • request 객체의 임시 저장소를 Model 처럼 활용한다

  • forward()를 통해 JSP를 실행

    • 전달받은 request, response 객체를 함께 전달한다

    • request의 임시 저장소에 저장한 model 정보 조회를 위해 전달

    • response에는 클라이언트에게 제공될 view가 담기게 된다




3. 2단계> View 분리

3-1. 수정 사항

String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
  • Controller에서 View로 이동하기 위해 위의 코드를 사용

  • 모든 Controller에 중복으로 작성됨

  • ➡️ View를 처리하는 객체를 만들어서 해결


3-2. 수정 후 동작 방식

  • Controller ( ControllerV2 )

    • 직접 forward()하지 않고 MyView 객체를 만들어서 반환

    • MyView 객체를 생성하면서 viewPath를 넘겨준다

    • 생성된 MyView 객체를 반환한다

  • Front Controller

    • Controller의 process() 실행 결과로 MyView 객체를 받는다

    • 반환된 MyView 객체의 render()를 호출

  • MyView

    • viewPath 필드를 가짐 ( JSP 파일의 위치 )

    • 생성자를 통해 viewPath를 전달받음

    • render()메서드에서 forward()를 담당




4. 3단계> Model 추가

4-1. 수정 사항

  • Servlet 종속성 제거 ( Servlet 기술을 사용하지 않도록 )

    • 요청 파라미터 정보가 필요한 Controller가 있어서 FrontController에서 Controller를 호출할 때 request, response 객체를 넘주었는데 모든 Controller가 요청 파라미터 정보가 필요한 것은 아님

    • ➡️ FrontController가 request에서 요청 파라미터 정보를 뽑아 Map 객체를 만들고 Controller 호출 시 Map 객체를 전달

    • Controller에서 Model에 담을 정보를 request 객체의 임시 저장소에 저장했는데 request 객체를 전달받지 않음

    • ➡️ 별도의 Model 객체를 만들어서 반환


  • View 이름 중복 제거

    • Controller에서 MyView 객체를 생성할 때, view의 경로를 전부 작성해주어야 했음

    • ➡️ Controller에서 View의 논리 이름 ( 경로를 제외한 파일 이름 )을 반환하고

    • ➡️ FrontController에서 실제 경로(위치)를 처리하도록 변경


4-2. 수정 후 동작 방식

  • ModelView

    • view 페이지의 논리 이름 + model ( Map 객체 )을 가짐

    • model을 만들고 view 이름까지 전달하는 객체

  • FrontController

    • request에 담긴 요청 파라미터 정보들을 Map 객체에 저장

    • Controller 호출 시 Map 객체를 전달

  • Controller ( ControllerV3 )

    • 전달받은 Map 객체 ( 요청 파라미터 정보 )를 이용하여 동작 수행

    • ModelView 객체 생성 및 반환

      • view 페이지의 논리 이름 설정

      • model ( Map 객체 )에 정보를 삽입

  • FrontController

    • Controller에서 반환된 ModelView가 가진 논리 이름을 viewResolver()로 전달

    • viewResolver()

      • 논리이름을 물리이름으로 변환

      • 변환된 물리 이름으로 MyView 객체 생성 및 반환

    • MyView의 render() 호출

      • ModelView가 가진 model과 request, response를 같이 전달
  • MyView

    • 전달된 model ( Map 객체 )에 담긴 데이터를 request의 데이터 저장소에 저장

      • JSP는 request.getAttribute()를 사용해 데이터를 조회하기 때문
    • forward() 실행




5. Controller 수정 ( ModelView 사용 X )

  • FrontController

    • request에 담긴 정보들을 Map 객체로 복사

    • Controller가 model을 담을 수 있도록 Map 객체 생성 ( 빈 객체 )

    • Controller를 호출하면서 model을 담을 Map 객체를 함께 전달

  • Controller ( ControllerV4 )

    • ModelView를 반환하지 않고 viewName만 반환하도록 변경

    • 전달받은 Map 객체에 put()을 이용하여 model 정보 삽입

  • FrontController

    • Controller에서 반환된 view 페이지 논리 이름을 viewResolver()를 통해 물리 이름으로 변환하면서 MyView 객체 생성

    • MyView의 render() 호출

      • Controller가 담은 model ( Map 객체 )과 request, response를 함께 전달



6. 어댑터 패턴

6-1. 수정 사항

public class FrontControllerServletV4 extends HttpServlet {

    private Map<String, ControllerV4> controllerMap = new HashMap<>();
}
  • FrontController를 보면 URL에 따른 Controller를 매핑 해주기 위해 위의 ControllerV4 처럼 특정 Controller 인터페이스를 지정했음

  • 즉, 한 가지 방식의 인터페이스만 사용 가능했음

  • 한 가지 방식의 Controller 인터페이스만 사용 가능한 것이 아니라 다른 Controller 인터페이스도 사용 가능하도록 변경하기 위해

  • ➡️ 어댑터 패턴을 이용


6-2. 수정 후 동작 방식

  1. 핸들러 매핑 정보를 보고 핸들러 조회

    • 핸들러 = Controller 라고 생각

    • 어떤 핸들러(Controller)인지 확인

  1. 핸들러 어댑터 목록에서 처리할 수 있는 어댑터 조회

    • 조회된 핸들러(Controller)를 처리할 수 있는 어댑터를 가져온다

    • ex> 핸들러가 ControllerV4이면 ControllerV4를 처리할 수 있는 어댑터를 가져온다

  1. FrontController는 핸들러 어댑터 호출

    • 이전까지는 FrontController가 바로 핸들러(Controller)를 호출했지만 핸들러 어댑터를 통해 핸들러(Controller)호출

    • FrontController가 핸들러 어댑터를 호출하면서 핸들러(Controller)를 넘겨준다

  1. 핸들러 어댑터가 대신 핸들러(Controller)를 호출한다
  1. 핸들러(Controller) 실행 결과를 핸들러 어댑터가 받아서 FrontController에게 전달

  • 핸들러 어댑터

    • 중간에서 어댑터 역할을 함

    • 핸들러 어댑터가 인터페이스로 존재하고 핸들러마다 핸들러 어댑터를 구현

    • 핸들러마다 핸들러 어댑터를 구현할 수 있기 때문에 다양한 종류의 컨트롤러를 호출할 수 있다 ( 처리할 수 있다 )

  • 핸들러

    • 어댑터가 있기 때문에 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있다

6-3. 핸들러 어댑터 인터페이스

public interface MyHandlerAdapter {

    // 1. 어댑터가 전달된 컨트롤러를 처리할 수 있는지 판단하는 메소드
    boolean supports(Object handler);

    // 2. Controller를 호출하고 결과를 받는 메서드
    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
  • 처리할 핸들러( Controller )마다 핸들러 어댑터 인터페이스를 구현

  • handle()은 핸들러( Controller )를 호출하고, 결과를 ModelView로 받는다

  • 반환형이 ModelView이기 때문에 위의 인터페이스를 구현한 핸들러 어댑터가 직접 ModelView 객체를 생성해서라도 반환형을 맞춰야함

    • ex> ControllerV4의 경우에는 String을 반환하기 때문에 ControllerV4에 해당하는 핸들러 어댑터가 직접 viewName을 이용하여 ModelView 객체를 생성하고 model을 등록해주어야 함

6-4. 핸들러 어댑터 구현

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 객체 생성
        Map<String, String> paramMap = createParamMap(request);

        // model 정보를 담을 Map 객체 생성 ( 빈 Map 객체 )
        HashMap<String, Object> model = new HashMap<>();

        // 핸들러 ( Controller ) 호출
        String viewName = controller.process(paramMap, model);

        // 반환형을 맞춰주기 위해 ModelView를 생성
        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        return mv;
    }
}
  • 기존 FrontController가 하던 일을 핸들러 어댑터가 처리

    • url에 맞는 Controller 호출 및 render()호출 제외

    • 제외된 기능은 FrontController에서 계속 수행


6-5. FrontController 코드


@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

    // 핸들러 매핑 정보
    // 모든 Controller를 받을 수 있어야 하기 때문에 특정 컨트롤러 인터페이스가 아닌 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/v4/members/new-form", new MemberFormControllerV4());
        ...
    }

    // 핸들러 어댑터 목록 초기화
    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);
        ...

        // 핸들러 어댑터 찾기 ( 찾아진 Controller가 처리할 수 있는 Controller인지 확인 )
        MyHandlerAdapter adapter = getHandlerAdapter(handler);

        // 어댑터의 handle을 호출
        // 어댑터의 handle은 Controller를 호출해 process()를 수행하도록 함
        // 수행 결과로 ModelView가 반환됨
        ModelView mv = adapter.handle(request, response, handler);

        // view 페이지 논리 이름 -> 물리 이름
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);

        // modelView의 model( Map 객체 )을 전달하면서 view 페이지 렌더링
        view.render(mv.getModel(), request, response);
    }

    // 요청 url을 보고 어떤 Controller인지 조회 및 Object 형식으로 반환
    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    // 핸들러 어댑터 목록을 반복하면서
    // 처리할 수 있는 Controller이면 핸들러 어댑터 반환
    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에 맞는 Controller 호출은 이제 하지 않음 ( 기존의 FrontController가 하던 일 )

    • url에 맞는 핸들러 찾기

    • 찾아진 핸들러를 처리할 수 있는 핸들러 어댑터가 있는지 확인

    • 핸들러 어댑터 호출

    • 반환된 ModelView의 render() 호출

  • FrontController는 결과적으로 일치하는 핸들러 어댑터를 찾아서 호출하는 역할

profile
공부한 내용을 정리해서 기록하고 다시 보기 위한 공간

0개의 댓글