스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 : Custom MVC Framework

jkky98·2024년 7월 15일
0

Spring

목록 보기
10/77

FrontController

이전 포스팅에서 JSP와 Servlet으로 구현한 첫 MVC 패턴의 구조에서 몇 가지 개선할 점을 발견했다.

  • 공통 로직 처리의 부재: 모든 요청에 반복적으로 처리해야 할 로직이 컨트롤러마다 중복되었다.
  • 사용하지 않는 인자 문제: 응답(Response) 객체가 불필요하게 포함되어 코드의 간결성과 효율성을 저해했다.

이를 해결하기 위해 공통 로직을 처리하는 구조를 도입하고, 코드 중복을 최소화하는 방향으로 개선이 필요하다.

위의 사진에서 보듯, 공통 처리 로직은 최대한 FrontController에서 담당하고, 개별 컨트롤러는 특정 비즈니스 로직에만 집중할 수 있도록 구성된다.

프론트 컨트롤러 패턴의 주요 특징은:

  • 프론트 컨트롤러 서블릿 하나만으로 클라이언트 요청을 수신.
  • 요청에 맞는 개별 컨트롤러를 찾아 호출.
  • 프론트 컨트롤러만 서블릿에 의존하며, 개별 컨트롤러는 서블릿과 분리.
    이를 통해 의존성을 분리하고 개별 컨트롤러들이 서블릿 환경에서 독립할 수 있다.

스프링 웹 MVC의 핵심도 바로 이 프론트 컨트롤러 패턴에 있으며,
DispatcherServlet이 이를 구현한 대표적인 예이다.

V1(apply FrontController)

V1에서의 목표는 기존 코드를 최대한 유지하면서 프론트 컨트롤러를 도입하는 것이다. 목표한 구조도는 다음과 같다.

개별 컨트롤러를 인터페이스 아래에서 구현하기 위해 다음과 같은 인터페이스를 도입한다.


public interface ControllerV1 {

    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;

}

기존에는 각각의 컨트롤러들이 HttpServlet 클래스를 상속받아 service() 메서드에 비즈니스 로직을 작성했다. 하지만 FrontController를 도입하면서 다음과 같은 개선이 이루어진다:

  1. 의존성 분리

    • 개별 컨트롤러는 더 이상 HttpServlet을 상속받지 않는다.
    • 이를 통해 서블릿에 의존하지 않고 독립적으로 동작할 수 있다.
  2. 새로운 설계: process 메서드 도입

    • 개별 컨트롤러는 비즈니스 로직을 처리하기 위해 process라는 이름의 메서드를 사용.
    • 프론트 컨트롤러는 요청을 받아 각 컨트롤러의 process 메서드를 호출하여 요청을 처리한다.
  3. 코드 이전

    • 기존 service() 메서드에 작성되었던 코드를 process 메서드로 옮겨 작성.
    • 동일한 로직을 유지하면서 프론트 컨트롤러와 통합될 수 있도록 구조를 변경.

이러한 구조 변경으로 개별 컨트롤러의 의존성을 제거하고, 프론트 컨트롤러가 공통된 요청 흐름을 관리하도록 설계가 단순화된다.(코드 예시 생략)


@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/ 뒤에 오는 모든 URI를 이 프론트 컨트롤러로 매핑하기 위함이다.


주요 구성 요소 설명

  1. controllerMap

    • URI에 따른 개별 컨트롤러 구현체를 저장하기 위한 맵.
    • 인터페이스 아래 구현체들을 관리하므로 인터페이스 타입으로 선언.
  2. 생성자

    • 컨트롤러 맵인 controllerMap에 URI와 해당 URI를 처리할 구현 컨트롤러 객체를 미리 생성하여 저장.
    • 요청에 대한 컨트롤러 매핑을 사전에 준비.
  3. service 메서드

    • 요청 URI를 기반으로 controllerMap에서 매핑된 컨트롤러 객체를 조회.
    • 해당 URI에 매핑된 컨트롤러가 없을 경우, 404 에러 반환.
    • 매핑된 컨트롤러가 있으면, 해당 컨트롤러의 process 메서드를 호출.
      • 이때 requestresponse 객체를 함께 넘겨줘 요청 처리.

현재 문제점

프론트 컨트롤러를 도입하여 코드 구조를 개선했으나,
여전히 View로 전달하는 로직(viewPath 설정, dispatcher.forward 등)은 반복되고 있다.
이 부분을 더 개선하려면 View 처리 로직의 분리 또는 추가적인 추상화 계층이 필요하다.

V2(View Separation)

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

MyViewviewPath를 필드로 가지고 이를 렌더링하는 역할을 담당하며, 이를 활용하기 위해 컨트롤러 인터페이스의 process 메서드 반환 타입을 MyView로 변경한다. 구현 컨트롤러는 요청 처리 후 viewPath를 기반으로 MyView 객체를 생성해 반환하고, 프론트 컨트롤러는 반환된 MyView 객체의 render 메서드를 호출해 렌더링을 수행한다. 이를 통해 View 렌더링 로직이 MyView로 분리되어 중복 코드가 제거되고, 컨트롤러는 비즈니스 로직에만 집중할 수 있다.

public interface ControllerV2 {

    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
// frontControllerV2

@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);
        view.render(request, response);
    }
}

이와 같은 설계를 통해 각각의 구현 컨트롤러에 존재하던 JSP로 데이터를 보내는 과정(View 로직의 전반부)을 컨트롤러에서 분리할 수 있었다. 이제 JSP로 데이터를 전달하고 시동하는 작업은 컨트롤러가 아닌 MyView에서 통제할 수 있게 되었다.

V3(Add Model)

우리는 지금까지 request 객체를 일시적 데이터 보관소로 활용하여 Model 개념을 적용했다. 하지만 request 객체는 많은 정보를 담고 있어 무겁다. 실제로 process 메서드가 request에서 사용하는 부분은 극히 일부에 불과한데, 꼭 전체 request 객체를 인자로 전달해야 할까?

컨트롤러 입장에서는 서블릿 객체인 requestresponse가 반드시 필요하지 않다. 요청 파라미터 정보는 프론트 컨트롤러에서 미리 처리하여, 개별 구현 컨트롤러에 필요한 데이터만 전달할 수 있다. 이를 통해 컨트롤러가 서블릿 객체에 의존하지 않고 더 가볍고 독립적으로 동작할 수 있도록 설계할 수 있다.

또한 뷰 이름의 중복을 제거하기 위해 뷰의 논리 이름과 실제 물리 위치 이름을 구분할 수 있다.

/WEB-INF/views/new-form.jsp -> new-form
/WEB-INF/views/save-result.jsp -> save-result
/WEB-INF/views/members.jsp -> members

컨트롤러는 요청을 처리한 후 ModelView 객체를 반환하고,
프론트 컨트롤러는 이를 기반으로 viewResolver를 통해 어떤 JSP로 전달할지 결정한다.
결정된 JSP 경로는 MyView를 통해 렌더링(render) 작업을 수행하게 된다.
이를 통해 컨트롤러는 비즈니스 로직과 데이터를 처리하고,
뷰 선택 및 렌더링 로직은 프론트 컨트롤러와 MyView가 담당하도록 역할이 명확히 분리된다.

// ControllerV3 인터페이스
public interface ControllerV3 {

    ModelView process(Map<String, String> paramMap);
}
@Getter
@Setter
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();

    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}

ModelView는 뷰와 데이터를 연결하기 위한 모델로, MyView의 대체가 아니라 별도의 기능을 담당한다.
컨트롤러는 요청을 처리한 후 MyView를 바로 반환하는 것이 아니라,
ModelView를 반환하며 뷰 이름과 모델 데이터를 포함한다.
프론트 컨트롤러는 반환된 ModelView를 사용하여 viewResolver로 뷰를 결정하고,
최종적으로 MyView를 통해 렌더링 작업을 수행한다.
이 설계는 컨트롤러와 뷰 렌더링 로직을 명확히 분리하며, 보다 유연한 구조를 제공한다.

//frontcontroller의 service

    @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 = createPramMap(request);
        ModelView mv = controller.process(paramMap);

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

        view.render(mv.getModel(), request, response);
    }

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

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

프론트 컨트롤러는 요청이 들어오면 URI를 추출하여,
기존처럼 controllerMap에서 해당 URI에 매핑된 컨트롤러 객체를 가져온다.
또한, paramMap이라는 새로운 로직을 통해 request의 요청 파라미터를 자바의 Map 자료구조에 담는다.

컨트롤러의 process 메서드는 이제 더 이상 서블릿 객체(request, response)에 종속되지 않고,
요청 파라미터가 담긴 Map을 인자로 받아 비즈니스 로직을 처리하게 된다.

process에서 비즈니스 로직을 처리한 후 ModelView를 생성하며:
1. 생성자를 통해 JSP 경로의 논리 이름(viewName)을 설정.
2. 모델 데이터(HashMap)를 채우고, 이를 JSP로 보낼 객체(member, members 등)를 담는다.
3. 완성된 ModelView 객체를 리턴.

프론트 컨트롤러는 컨트롤러에서 반환된 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;
    }
}

프론트 컨트롤러는 컨트롤러에서 반환된 ModelView를 받아,
먼저 논리 이름(viewName)을 꺼낸다.
이 논리 이름을 viewResolver에 전달하면,
viewResolver는 해당 논리 이름을 실제 JSP 경로로 변환한 MyView 객체를 반환한다.

프론트 컨트롤러는 이 MyView 객체의 render 메서드를 호출하며,
ModelView에 담긴 Model 데이터를 함께 전달한다.
이를 통해 JSP로 데이터를 전달하며 렌더링을 수행한다.

Model 데이터를 함께 처리하기 위해,
MyView 클래스는 render 메서드를 약간 수정하여
Model을 포함한 렌더링을 지원하도록 개선된다.

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 static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
        model.forEach((key, value) -> request.setAttribute(key, value));
    }
}

MyView 클래스에 Model 데이터를 받을 수 있는 render 메서드를 추가하며, modelToRequestAttribute 메서드를 별도로 분리하여 Model 데이터를 request 속성으로 변환하는 작업의 가독성을 높인다. v3의 흐름은 프론트 컨트롤러가 요청을 수신해 URI를 기반으로 controllerMap에서 컨트롤러를 찾고, 요청 파라미터를 paramMap에 담아 컨트롤러의 process 메서드를 호출하며 시작된다. 컨트롤러는 비즈니스 로직을 처리한 뒤 ModelView를 반환하고, 프론트 컨트롤러는 viewName을 추출해 viewResolver를 통해 실제 경로의 MyView 객체를 생성한다. 이후, 프론트 컨트롤러는 MyViewrender 메서드를 호출하며, Model과 함께 JSP로 데이터를 전달해 렌더링을 수행한다.

V4(개발자들이 편하게)

v3의 경우, 구현 컨트롤러들이 항상 Model을 직접 생성하여 반환해야 하므로 개발자 입장에서 다소 번거로울 수 있다. 좋은 프레임워크는 아키텍처의 우수성뿐만 아니라 개발자가 단순하고 편리하게 사용할 수 있는 실용성도 중요하다.

v4에서는 이러한 번거로움을 줄이기 위해 Model 생성 단계를 FrontController에서 담당한다. 즉, process 메서드는 더 이상 모델을 생성하지 않고, 프론트 컨트롤러에서 주입받은 Model에 필요한 데이터만 추가하는 방식으로 변경된다. 컨트롤러는 비즈니스 로직을 처리하며 논리 이름(viewName)만 반환하고, Model 생성 및 전달은 프론트 컨트롤러가 처리하는 구조를 채택한다. 이를 통해 개발자는 데이터 구축에만 집중할 수 있어 더 단순하고 효율적인 코드 작성을 가능하게 한다.

@Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();

        ControllerV4 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        Map<String, String> paramMap = createPramMap(request);
        Map<String, Object> model = new HashMap<>();
        String viewName = controller.process(paramMap, model);

        MyView view = viewResolver(viewName);

        view.render(model, request, response);
    }

이전과 다르게 model을 controller로직 작동 전에 우선 생성해준다. 그리고 process에 넘겨 다음과 같이 처리한다.

@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);
        return "save-result";
    }

process 메서드는 주입받은 Model 객체에 뷰로 전달할 데이터를 구성하고, 어떤 뷰로 이동해야 하는지 알려줄 논리 이름(String)을 반환한다. 프론트 컨트롤러는 반환된 논리 이름을 뷰 리졸버(viewResolver)에 전달하여 MyView 객체를 생성하고, process에서 구성된 데이터를 포함한 Model을 사용해 .render 처리를 수행한다.

이 방식으로 컨트롤러를 개발하는 입장에서는 새로운 객체를 생성할 필요 없이, 주입받은 Model에 데이터만 추가하고 논리 이름만 반환하면 된다. Model 생성 로직이 사라지고, 프론트 컨트롤러가 한 번(Model 생성) 처리하면 모든 컨트롤러에서 이를 주입받아 사용할 수 있다. 결국, 프론트 컨트롤러의 작업량이 소폭 증가(1 task)하는 대신, 구현 컨트롤러의 작업량은 크게 감소(N tasks)하여 개발 효율성을 높일 수 있다.

V5(flexible controller)

현재까지 만든 버전에서는 Controller 인터페이스를 통해 다형성을 활용하여 여러 구현 컨트롤러를 사용할 수 있었지만, Controller 인터페이스에 종속된 한계가 있다.
만약 다른 인터페이스를 사용하는 구현 컨트롤러도 쉽게 적용 가능하다면, 컨트롤러 시스템은 더욱 유연해질 것이다.

이를 위해 프론트 컨트롤러가 특정 인터페이스에 의존하지 않고,
컨트롤러의 타입이나 구조와 무관하게 호출과 처리가 가능하도록 설계해야 한다.
이러한 설계가 이루어진다면 다양한 종류의 컨트롤러를 손쉽게 통합할 수 있는 유연한 컨트롤러 시스템을 구축할 수 있다.

어댑터 패턴

현재 FrontControllerV4 코드는 ControllerV4 인터페이스에 강하게 의존적이다.
다른 ControllerVX 인터페이스를 사용하는 컨트롤러 지원이 용이하게끔 설계하고 싶다.
하지만 현재 구조에서는 이를 구현할 방법이 없다.

이 문제는 어댑터 패턴(Adapter Pattern)을 도입함으로써 해결할 수 있다.


어댑터 패턴의 구성 요소

  1. Handler

    • 기존의 "Controller"라는 이름을 더 범용적인 Handler로 변경.
    • 어댑터 패턴을 통해 다양한 인터페이스의 컨트롤러를 처리할 수 있으므로,
      더 넓은 의미의 용어를 사용하여 확장 가능성을 나타냄.
  2. HandlerAdapter

    • 프론트 컨트롤러가 다양한 종류의 컨트롤러를 호출할 수 있도록 중간 역할을 수행.
    • 각 컨트롤러 타입에 적합한 어댑터를 구현하여,
      프론트 컨트롤러가 컨트롤러의 상세 구조에 의존하지 않고 호출 가능.

어댑터 패턴의 효과

  • 프론트 컨트롤러는 특정 인터페이스에 종속되지 않고,
    다양한 구조의 컨트롤러를 처리할 수 있는 유연성을 가지게 된다.
  • 새로운 컨트롤러 타입이 추가되더라도,
    해당 컨트롤러를 처리할 어댑터만 구현하면 기존 시스템에 쉽게 통합 가능.
  • 결과적으로, 확장 가능한 컨트롤러 시스템을 구축할 수 있다.

어댑터 인터페이스


public interface MyHandlerAdapter {

    boolean supports(Object handler); // 들어온 핸들러가 사용가능한지 검증

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
  • supports : 들어온 핸들러가 사용가능한 핸들러인지 검증한다.
  • handle : 핸들러로 하여금 로직을 처리하는 부분이다. process와 비슷process와 관련 로직을 한번 더 감싼 메서드

FrontControllerV5


@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() {
        initHandler();
        initHandlerAdapters();
    }

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

    private void initHandler() {
        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());
    }

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

        ModelView mv = adapter.handle(request, response, handler);

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

        view.render(mv.getModel(), request, response);
        }

    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();
        return handlerMappingMap.get(requestURI);
    }

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

기존의 V3V4 컨트롤러 클래스를 활용하며, 어댑터 패턴을 도입한 프론트 컨트롤러의 주요 메서드는 다음과 같다:


주요 메서드 설명

  1. initHandler()

    • 요청 URI를 키로, 구현 컨트롤러를 값으로 매핑한 handlerMappingMap을 초기화한다.
    • URI와 핸들러(컨트롤러)를 매핑하는 역할로, 기존의 controllerMap과 유사하다.
  2. initHandlerAdapters()

    • 다양한 핸들러를 처리할 수 있는 핸들러 어댑터 구현체를 보관하는 리스트를 초기화한다.
    • 예: V3 컨트롤러용 어댑터, V4 컨트롤러용 어댑터를 리스트에 추가.
  3. getHandler()

    • 요청 URI를 받아, handlerMappingMap에서 매핑된 핸들러(컨트롤러)를 반환한다.
    • 핸들러 초기화(initHandler) 덕분에 URI와 컨트롤러를 쉽게 매핑 가능.
  4. getHandlerAdapter()

    • 핸들러를 인자로 받아, initHandlerAdapters를 통해 구성된 어댑터 리스트를 순회.
    • supports() 메서드를 호출해 해당 핸들러를 처리할 수 있는 어댑터를 검증하고 반환.
  5. adapter.handle()

    • 어댑터와 핸들러를 확보한 후, 어댑터의 handle() 메서드에 핸들러를 주입.
    • 비즈니스 로직 처리 결과로 ModelView mv를 생성 및 반환.
  6. 렌더링 처리

    • 반환된 ModelView의 논리 이름을 사용해 뷰 리졸버(viewResolver)로 실제 경로의 MyView를 생성.
    • 생성된 MyViewrender() 메서드를 호출해 모델 데이터를 렌더링.

어댑터의 효능 - 확장성

private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        // 수정사항 (추가 v4)
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }

    private void initHandler() {
        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());
    }

v3의 구현 컨트롤러들 뿐만 아니라 v4를 추가하기 위해 FrontController에 가한 수정사항은 위와 같으며, ControllerV4Handler를 MyHandlerAdapter에 맞게 구현해주면 된다.

	// ControllerV4Handler의 handler메서드
	@Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        ControllerV4 controller = (ControllerV4) handler;
        Map<String, String> pramMap = createPramMap(request);
        HashMap<String, Object> model = new HashMap<>();
        String viewName = controller.process(pramMap, model);

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

        return mv;
    }

V4의 특징V3의 프론트 컨트롤러에서 Model을 생성하던 역할을 어댑터로 위임한 점에 있다.
이에 따라, V4 구현 컨트롤러는 수정하지 않았고, 프론트 컨트롤러 더 이상 Model 생성 역할을 하지 않는다.
이제 Model 생성은 어댑터가, Model에 데이터를 추가하는 작업은 컨트롤러(핸들러)가,
그리고 최종적으로 ModelViewModel을 담는 작업은 어댑터가 다시 담당한다.

  • 아쉬운 점: Model 생성 및 데이터 추가가 어댑터와 핸들러에 분리되어 있어 역할이 분산되었다. (간단한 예제 프로젝트이기 때문에 이 구조에서 완벽한 튜닝을 기대하기는 어렵다.)

  • 좋은 호환성:

    • V3 컨트롤러는 process 메서드에서 ModelView를 반환하고,
      V4 컨트롤러는 Model에 데이터를 추가한 뒤 논리 이름(String)만 반환한다.
    • 어댑터 도입을 통해 프론트 컨트롤러가 요구하는 조건(ModelView 반환)을
      각 구현 어댑터에서 적절히 처리할 수 있게 되었다.
    • 이를 통해 V3의 220V 전원코드와 V4의 110V 전원코드
      프론트 컨트롤러라는 하나의 콘센트에 연결할 수 있는 유연성을 확보하였다.

정리

버전별 개선 요약

  • v1 - 프론트 컨트롤러 도입
    기존 구조를 유지하면서, 전체적인 큰 구조만 변경.
    프론트 컨트롤러를 도입한 후, 세부 구조는 이후 단계에서 변경하기로 함.

  • v2 - View 분리
    반복적으로 사용되던 View 렌더링 로직을 분리하여 재사용성을 높임.

  • v3 - Model 추가

    • setAttribute(Servlet 종속성)을 중단하고,
      데이터를 담는 구조를 Map으로 변경.
    • View 이름 중복을 제거하기 위해 뷰 리졸버(viewResolver) 도입.
  • v4 - 실용적 관점의 리팩토링

    • Model 생성 역할을 프론트 컨트롤러에서 담당.
    • 구현 컨트롤러는 프론트 컨트롤러에서 주입받은 Model(Map)에 데이터만 추가.
    • 프론트 컨트롤러가 ModelView를 생성하여 View에 전달.
  • v5 - 유연한 컨트롤러
    어댑터 패턴을 도입하여 다양한 구현 컨트롤러(V3, V4 등)를
    프론트 컨트롤러가 처리할 수 있도록 확장성 제공.
    이를 통해 과거 버전의 컨트롤러도 쉽게 통합 가능.

느낀점

다형성을 여럿 엮어서 사용한 미니 프로젝트였다. 프로젝트를 진행하며 프론트 컨트롤러에서 장고의 config 앱의 urls.py가 다른 앱으로 URL 처리를 위임하는 방식과 비슷하다는 느낌을 받았다. 하지만 자바가 장고보다 조금 더 자유롭다는 생각이 들었다. 스프링 MVC 쪽으로 가면 편의를 위해 제약이 생기므로, 장고와 비슷한 느낌이 들지도 모르겠다. 논리 이름(Logical Name)과 관련해서도 장고의 urls.py에서 엔드포인트의 name을 사용하는 방식이 자바와 유사하게 느껴졌다.


MTV와 MVC의 차이

학습 이전에는 MTV(Model-Template-View)와 MVC(Model-View-Controller)가 비슷한 개념이라고 생각했지만, 학습 후에는 꽤 다르다는 것을 느꼈다. 특히, MTV의 M(Model)은 Model과 Repository를 합친 느낌으로 다가왔다. 자바의 MVC에서 모델은 데이터와 관련된 역할만 담당하고 Repository는 별도로 분리된 반면, 장고에서는 모델이 데이터베이스 연동과 로직까지 포함하는 역할을 수행한다는 점이 흥미로웠다.


서비스 계층(Service Layer)의 이해

컨트롤러에서 비즈니스 로직을 분리하기 위해 서비스 계층(Service Layer)을 만든다고 학습했는데, 이는 장고 프로젝트에서도 유사하게 적용될 수 있었다. 장고에서 views.py에 비즈니스 로직이 많아지면 이를 service.py로 분리하여 views.py는 단순히 서비스를 호출하도록 설계하는 방식과 비슷해 이해가 잘 되었다. (아직 스프링의 서비스 계층을 학습하지는 않았지만, 전반적으로 비슷한 개념이라는 것을 느꼈다.)


다형성 활용과 코드 복잡성

다형성을 학습하고 나니 개념적으로 이해하는 데 큰 문제는 없었다. 다만, 프로젝트 구조가 복잡해지면서 과거에 작성한 메서드나 로직의 흐름을 놓치는 경우가 있었다. 특히 "여기로 왜 보내는 거지?"라는 의문이 실시간으로 생기는 경우가 있었는데, 이는 강의 속도가 빨랐기 때문이라고 생각한다. 하지만 강의 이후 코드를 다시 차근히 읽어보니 이해에는 큰 문제가 없었다.


느낀 점과 앞으로의 방향

이번 프로젝트를 통해 장고와 자바의 구조적 차이와 공통점을 비교할 기회가 되었다. 특히 MTV와 MVC의 차이점, 서비스 계층의 활용, 다형성의 실질적 활용에서 많은 것을 배울 수 있었다. 앞으로는 코드를 작성할 때 구조적 복잡성을 줄이고, 더 명확한 역할 분리를 통해 협업이나 유지보수가 용이한 설계를 지향하고자 한다.

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

profile
자바집사의 거북이 수련법

0개의 댓글