Spring

[Spring] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 섹션4 MVC 프레임워크 만들기

고쩡이 2024. 4. 14. 20:15

본 내용은 인프런 김영한T 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의를 정리한 내용입니다:)

[Spring] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 섹션4 MVC 프레임워크 만들기

◼️ 프론트 컨트롤러 패턴 소개

프론트 컨트롤러 도입 전 / 후

🟢 FrontController 패턴 특징

  • 프론트 컨트롤러 서블릿 하나로 요청을 받음 (나머지 컨트롤러는 서블릿 사용X)
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 입구를 하나로 → 공통 처리 가능

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

◼️ 프론트 컨트롤러 도입 - v1

v1 구조

프론트 컨트롤러를 단계적 도입해보자.

 

컨트롤러 인터페이스를 도입한다.

package hello.servlet.web.frontcontroller.v1;

public interface ControllerV1 {
 void process(HttpServletRequest request, HttpServletResponse response) 
 	throws ServletException, IOException;
}

각 컨트롤러는 위 인터페이스를 구현한다. 각 컨트롤러 내부 로직은 기존 서블릿코드와 거의 같다.

 

회원 폼 컨트롤러

package hello.servlet.web.frontcontroller.v1.controller;

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

회원 등록 컨트롤러

package hello.servlet.web.frontcontroller.v1.controller;

public class MemberSaveControllerV1 implements ControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public void 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);
        request.setAttribute("member", member);
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

 

회원 목록 컨트롤러

package hello.servlet.web.frontcontroller.v1.controller;

public class MemberListControllerV1 implements ControllerV1 {
    private 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 viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

 

프론트 컨트롤러

// ../v1의 모든 하위 요청은 서블릿에서 받아들인다.
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
 private Map<String, ControllerV1> controllerMap = new HashMap<>();
 public FrontControllerServletV1() {
 // 매핑url, 호출될 컨트롤러
 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 {
     System.out.println("FrontControllerServletV1.service");
     String requestURI = request.getRequestURI();
     
     //다형성에 의해 아래 ControllerV1 타입으로 받을 수 있다.
     ControllerV1 controller = controllerMap.get(requestURI); // 실제 호출할 컨트롤러 찾기
     if (controller == null) { // Map에 없다면 404 상태코드 반환
         response.setStatus(HttpServletResponse.SC_NOT_FOUND);
         return;
     }
     controller.process(request, response); // 컨트롤러 찾고 해당 컨트롤러 실행
     }
}

◼️ View 분리 - v2

v2 구조

모든 컨트롤러에 뷰 이동 부분 코드 중복이 존재한다. 이를 분리하기 위해 별도로 뷰를 처리하는 객체를 만들자.

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

MyView 클래스를 만들어준다.

package hello.servlet.web.frontcontroller;

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

V2버전 컨트롤러 인터페이스를 만든다. V1과의 차이점은 MyView를 반환하는 것이다.

package hello.servlet.web.frontcontroller.v2;

public interface ControllerV2 {
    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

이제 각 컨트롤러는 복잡한 dispatcher.forward() 를 직접 생성해서 호출하지 않고, 단순히 MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환하면 된다.

 

회원 등록 폼 컨트롤러

package hello.servlet.web.frontcontroller.v2.controller;

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 {
    private 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);
        request.setAttribute("member", member);
        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}

회원 목록 컨트롤러

public class MemberListControllerV2 implements ControllerV2 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse
            response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();
        request.setAttribute("members", members);
        return new MyView("/WEB-INF/views/members.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();
         ControllerV2 controller = controllerMap.get(requestURI);
         if (controller == null) {
         response.setStatus(HttpServletResponse.SC_NOT_FOUND);
         return;
         }
         MyView view = controller.process(request, response); // 컨트롤러 호출 결과 MyView 반환
         view.render(request, response); // forward 로직 수행, JSP 실행
         }
}

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

◼️ Model 추가 - v3

현재는 자바 Map을 이용하고 있어, 컨트롤러 입장에서 HttpServletRequest, HttpServletResponse이 꼭 필요하지않다.

컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경해보자.

v3 구조

🟢 뷰 이름 중복 제거

아래처럼 중복을 제거하고 단순화된 표현으로 바꿔보자. 폴더 위치가 이동해도 프론트 컨트롤러만 고치도록 바꿔보자.

/WEB-INF/views/new-form.jsp → new-form

위를 위해 아래처럼 수정이 필요하다.

  • 컨트롤러는 뷰의 논리 이름을 반환한다.
  • 프론트 컨트롤러에서 물리 위치 이름을 지정한다.

🟢 서블릿 종속성 제거

지금까지는 HttpServletRequest의 setAttribute()를 통해 데이터를 저장하고 뷰에 전달했다. 서블릿 종속성을 제거해보자.

Model을 직접 만들고, View이름까지 전달하는 객체를 만들자.

 

ModelView

public class ModelView {
    private String viewName; // 뷰이름
    private Map<String, Object> model = new HashMap<>(); // 뷰 렌더링에 필요한 model 객체
    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;
    }
}

위 모델 뷰는 뷰 이름, 뷰 렌더링에 필요한 모델객체를 가지고 있다.

 

ControllerV3

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

위 V3 컨트롤러는 서블릿 기술을 전혀 사용하지 않는다.

비교를 위해 이전 V2 컨트롤러를 보자.

public interface ControllerV2 {
    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

 

이전 V2는 단순히 viewpath를 반환했다. V3는  뷰 이름과 뷰에 전달한 Model 데이터를 포함하는 ModelView 객체를 반환한다.

또한 V2는 request.setAttribute()등 서블릿관련메서드를 직접 호출했었다. 반면 V3는 프론트 컨트롤러가 HttpServletRequest가 제공하는 파라미터는  paramMap에 담은 후 V3를 호출한다.

 

회원 등록 폼 컨트롤러

public class MemberFormControllerV3 implements ControllerV3 {
    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form"); //view 논리적 이름지정. 물리이름은 프론트컨트롤러가 처리
    }
}

회원 저장 컨트롤러

  • 파라미터 정보는 map 에 담겨있다. (프론트컨트롤러가 서블릿 종속성 해결)
  • 뷰에 필요한 객체를 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;
    }
}

회원 목록 컨트롤러

public class MemberListControllerV3 implements ControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public ModelView process(Map<String, String> paramMap) {
        List<Member> members = memberRepository.findAll();
        ModelView mv = new ModelView("members");
        mv.getModel().put("members", members);
        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();
         ControllerV3 controller = controllerMap.get(requestURI);
         if (controller == null) {
             response.setStatus(HttpServletResponse.SC_NOT_FOUND);
             return;
         }
         Map<String, String> paramMap = createParamMap(request); // request->paramMap
         ModelView mv = controller.process(paramMap); // viewpath,model반환
         String viewName = mv.getViewName();
         MyView view = viewResolver(viewName); // 실제 viewpath로 변환
         view.render(mv.getModel(), request, response); // 렌더링
     }

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
 /*
 HttpServletRequest에서 파라미터 정보를 꺼내서 Map으로 변환
 */
 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;
     }
}

createParamMap(): HttpServletRequest에서 파라미터 정보를 꺼내서 Map으로 변환

ViewResolver(): 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다.

view.render(): 뷰 객체에 모델정보를 전송해, HTML 화면을 렌더링한다.

 

MyView

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 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)
    {
        model.forEach((key, value) -> request.setAttribute(key, value));
    }
}

JSP는 request.getAttribute() 로 데이터를 조회한다. 따라서 모델 데이터를 request.setAttribute()로 담아둔다.

그 후 JSP로 포워드 해서 렌더링한다.

 

정리하면 전체 흐름은 다음과 같다.

  1. 프론트컨트롤러에서 HttpServletRequest 파라미터를 paramMap에 담는다.
  2. 해당 URL에 대한 컨트롤러를 호출한다.
  3. 컨트롤러는 로직을 처리한 후 ModelView를 반환한다 (논리 viewpath + model).
  4. 프론트컨트롤러에서 viewResolver를 통해 물리 viewpath를 가진 MyView를 생성한다.
  5. MyView.render(받은ModelView의 모델, request, resonpse)를 호출한다.
  6. MyView.render는 model map을 request attribute로 변환하여 렌더링한다.

🟢 헷갈려 정리해본 V3에서 각각의 역할

프론트 컨트롤러 (Front Controller)

  • 클라이언트의 요청을 받아서 처리할 적절한 컨트롤러를 찾아서 호출
  • HttpServletRequest 파라미터를 추출하고, 이를 이용하여 paramMap에 담는다.
  • 논리 view path → 물리 view path 변환

컨트롤러 (Controller)

  • 실제 비즈니스 로직을 처리
    데이터를 받아서 비즈니스 로직을 실행하고, 결과를 ModelView로 반환

ModelView

  • 컨트롤러가 처리한 결과를 담는 객체
  • 논리적 view path + 모델 데이터
  • 뷰에 전달할 모델 데이터를 담고 있어서, 뷰가 이를 활용하여 화면을 렌더링할 수 있도록 한다.

MyView

  • 실제로 클라이언트에게 보여지는 뷰를 나타내는 클래스.
  • ModelView에 담긴 모델 데이터를 request attribute로 변환
  • 받은 모델 데이터를 이용하여 실제 화면을 생성하고, 이를 클라이언트에게 반환

ViewResolver

  • 논리적인 view path를 실제 view 객체로 변환하는 역할

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

v4 버전. 컨트롤러가 ModelView를 반환 X, ViewName만 반환

아래는 컨트롤러 인터페이스이다.

model 객체는 파라미터로 전달하고, 결과는 뷰 이름만 반환한다.

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

 

회원 등록 폼 컨트롤러

public class MemberFormControllerV4 implements ControllerV4 {
    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        return "new-form";
    }
}

 

회원저장 컨트롤러

public class MemberListControllerV4 implements ControllerV4 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        List<Member> members = memberRepository.findAll();
        model.put("members", members);
        return "members";
    }
}

회원목록 컨트롤러

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); // 모델이 파라미터로 전달되기때문에, 모델직접생성X

        return "save-result";
    }
}

프론트 컨트롤러

     @Override
     protected void service(HttpServletRequest request, HttpServletResponse response) 
     throws ServletException, IOException {
     ...
         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);
         }

 

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

프론트 컨트롤러는 아래와같이 버전에따라 한 가지 방식의 컨트롤러 인터페이스만 사용가능하다.

프론트컨트롤러에서 다양한 버전의 컨트롤러를 처리할 수 있도록 변경해보자.

 

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
     private Map<String, ControllerV3> controllerMap = new HashMap<>(); // V3 사용
     }

 

v5 구조.핸들러 어댑터로, 다양한 버전 컨트롤러를 호출 할 수 있다.

 

 

 

MyHandlerAdapter 인터페이스

public interface MyHandlerAdapter {
    boolean supports(Object handler); // 어댑터가 해당 handler(컨트롤러) 처리할 수 있는지 판단
    
    //실제 컨트롤러 호출, ModelView반환
    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) 
    	throws ServletException, IOException;
}

이제 ControllerV3를 지원하는 어댑터를 구현해보자.

 

ControllerV3HandlerAdapter

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) { // ControllerV3 처리할 수 있는 어댑터인지 판단
        return (handler instanceof ControllerV3);
    }
    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        ControllerV3 controller = (ControllerV3) handler; //Object->컨트롤러V3으로 형변환
        Map<String, String> paramMap = createParamMap(request); 
        ModelView mv = controller.process(paramMap); // V3형식에 맞도록 호출
        return mv; //컨트롤러V3는 ModelView반환,그대로 ModelView 반환
    }
    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;
    }
}

FrontControllerServletV5

@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(); // 어댑터 초기화
    }
    private void initHandlerMappingMap() { // url, 해당 컨트롤러 매핑정보
        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 void initHandlerAdapters() { //현재는 V3핸들러어댑터만 존재
        handlerAdapters.add(new ControllerV3HandlerAdapter());
    }
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Object handler = getHandler(request); // URL 해당 컨트롤러 찾기 ex_MemberSaveControllerV3()
        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        MyHandlerAdapter adapter = getHandlerAdapter(handler); // 핸들러 처리 어댑터 조회 ex_ControllerV3HandlerAdapter()
        ModelView mv = adapter.handle(request, response, handler); // 실제 어댑터 호출 ex_ControllerV3HandlerAdapter.handle(..ex_MemberSaveControllerV3())
        MyView view = viewResolver(mv.getViewName()); // ModelView->MyView
        view.render(mv.getModel(), request, response); // 렌더링
    }

    // 핸들러 매핑정보에서 URL 에 매핑된 핸들러 객체를 찾아 반환
    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI); // url 매핑 컨트롤러 반환
    }
    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) { // 핸들러 버전에 맞는 어댑터 반환 ex_핸들러==MemberSaveControllerV3()
                return adapter; // 처리가능 어댑터 반환 ex_ControllerV3HandlerAdapter 반환
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
    }
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}

◼️ 단순하고 실용적인 컨트롤러 - v5

ControllerV4 를 사용할 수 있도록 기능을 추가해보자. 프론트컨트롤러에 아래를 추가한다.

  • ControllerV4 를 사용하는 컨트롤러를 추가
  • 해당 컨트롤러를 처리할수 있는 어댑터인 ControllerV4HandlerAdapter 추가
private void initHandlerMappingMap() { // url, 해당 컨트롤러 매핑정보
    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 void initHandlerAdapters() {
    handlerAdapters.add(new ControllerV3HandlerAdapter());
    handlerAdapters.add(new ControllerV4HandlerAdapter()); // V4 추가
}

ControllerV4HandlerAdapter 클래스를 만들어준다. 이때, ControllerV4는 V3와 달리 viewName을 반환한다.

따라서 어댑터에서 ModelView를 만들어, 형식을 맞추어 반환한다.

ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;

◼️ 정리

지금까지 v1 ~ v5로 점진적으로 프레임워크를 발전시켜 왔다.

  • v1: 프론트 컨트롤러를 도입
  • v2: 단순 반복 되는 뷰 로직 분리
  • v3: Model 추가 (서블릿 종속성 제거,뷰 이름 중복 제거)
  • v4: 프론트컨트롤러가 ModelView를 직접 생성 (컨트롤러가 ModelView를 반환 X, ViewName만 반환)
  • v5: 유연한 컨트롤러(어댑터 도입)

스프링 MVC는 지금까지 학습한 내용과 거의 같은 구조를 가지고 있다.