개발일기

MVC 프레임워크 만들기 정리 및 회고 (V1, V2) 본문

Spring/(김영한님)스프링 MVC 1편 - 백엔드 웹 개발 핵심기술

MVC 프레임워크 만들기 정리 및 회고 (V1, V2)

한둥둥 2024. 3. 6. 00:25

프론트 컨트롤러 패턴 소개 dispatcherServlet의 역할을 해주는 프론트 컨트롤러를 만들어보며 SpringMVC의 큰그림을 그려보자..!

 

프론트 컨트롤러 도입 전, 우리는 각각의 RequestDispatcher를 통한 forward해주는 공통 코드를 계속해서 중복적으로 만들어주어야 했다. 이러한 부분들을 개선해보자. 

여기서 강의에서 꿀팁은 준위 (레벨별) 비슷한 코드끼리 먼저 리팩토링해서 바꾸는게 중요하다고 했다. 그렇다면 구조적인 부분부터 먼저 바꾸고 세밀한 부분은 추후에 개선하는 방향으로 코드를 수정해야한다. 

 

전에 만들었던 코드는 각각의 코드가 따로 따로 호출되어 ControllerA, B , C 를 호출하여 View보여주었다. 

 

 

프론트 컨트롤러를 도입하면

화면의 그림처럼 공통 코드를 걸쳐서 ControllerA, B , C에 각각 호출하게 된다. 

 

해당 코드는 ControllerV1코드이다.

package hello.servlet.web.frontcontroller.v1;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

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

interface로 만들어준 이유는 각각의 Controller에서 ControllerV1으로 process를 implements하여 override하여 사용하기 때문이다.

 

실질적인 FrontControllerServletV1코드

package hello.servlet.web.frontcontroller.v1;

import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@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 {
        System.out.println("FrontControllerServletV1.service");

        String requestURI = request.getRequestURI();
        ControllerV1 controller = controllerMap.get(requestURI);
        if(controller == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return ;
        }

        controller.process(request,response);
    }
}

 

map에다가 URI를 넣어주어 각각에 requestURI를 통하여 일치하는 것을 맵에서 꺼내주어 매칭시켜주는 방법으로 구현했다. 

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

import hello.servlet.web.frontcontroller.v1.ControllerV1;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

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

해당 부분을 보면 아직도 코드가 중복되는 것을 확인할 수 있음 이 부분을 V2에서는 해결할 것이다. MyView라는 클래스를 통해서 관리해줌 

 

ControllerV2 

FrontController에서 Controller를 호출한 후, Class로 작성한 MyView를 반환 해주고, render를 호출하여 MyView에서 JSP로 해당 코드를 forward해준다.   

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

해당 부분에 해당하는 코드를 리팩토링 해주는 것이다. 

 

 

MyView에 해당하는 코드이다. 

package hello.servlet.web.frontcontroller;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Map;

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

 

render메서드를 통해서 request, response 파라미터로 받아온 부분을 dispatcher를 통해서 forward를 해주어 데이터를 보내준다. 

 

 

ControllerV2

package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

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

 

기존과 동일하지만 return값을 위에서 만들어준 MyView로 받아준다. 

 

FrontControllerServlet

package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v1.ControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

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

해당 부분은 FrontController부분이고 controller.process에서 MyView클래스를 리턴하고 MyView에서 render하는 코드가 있기에 process안에 중복적으로 viewPath를 작성하지 않아도 된다. 

 

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

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.List;

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

 

해당 코드를 보면 아직도 MyView객체를 만들어서 forward하는 부분을 공통적으로 사용하고 있지만, MyView부분에서 FullPath가 들어간다. 이 부분을 간소화할 것이며 V3에서는 Model을 통하여 request, response를 간소화 해줄 것이다. 

다음편에서 뵙겠습니다.