Back-end

04. MVC 프레임워크 만들기

-운- 2023. 12. 26. 18:59

FrontController 패턴 특징

프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음

프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출

입구를 하나로!

공통 처리 가능

프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨

 

이번에는 프론트 컨트롤러를 v1~v5까지 만들며 단계적으로 발전하는 형태를 보자.

 

V1

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

 

 

각 버전마다 인터페이스를 만들 것이며, 이번에는 서블릿의 형태를 띄는 인터페이스를 만들었다.

각 컨트롤러는 이 인터페이스를 구현하면 된다.

 

MemberFormControllerV1, MemberListControllerV1, FrontControllerServletV1은 기존 서블릿에 ControllerV1을 implements하도록 하고 로직은 같다.

 

FrontControllerServletV1

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

 

urlPatterns = "/front-controller/v1/*" 을 하

/front-controller/v1 를 포함한 하위 모든 요청은 이 서블릿에서 받아들인다.

 

 

service()

먼저 requestURI 를 조회해서 실제 호출할 컨트롤러를 controllerMap 에서 찾는다. 만약 없다면 404(SC_NOT_FOUND) 상태 코드를 반환한다. 컨트롤러를 찾고 controller.process(request, response); 을 호출해서 해당 컨트롤러를 실행한다.

 

v2 - view의 분리

 

V2에서는 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));
    }
}

 

이러한 View객체가 추가됨으로써 dispatcher.forward() 를 직접 생성해서 호출하지 않아도 된다. 단순히 MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환하면 된다.

 

다음 코드를 살펴보자.

 

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

모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있었는데, MyView를 통해 중복이 사라짐을 확인할 수 있다.

 

이와같이 MemberSaveControllerV2와 MemberListControllerV2 또한 MyView로 인한 중복이 사라졌고 나머지는 V1과 동일하다.

 

 

V3 - Model 추가

컨트롤러 입장에서 HttpServletRequest, HttpServletResponse이 필요가 없다.

request 객체를 Model로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 된다.(Map을 사용하여 요청 파라미터 정보를 넘기도록 하자)

 

ModelView

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 객체를 가지고 있다.

model은 단순히 map으로 되어 있으므로 컨트롤러에서 뷰에 필요한 데이터를 key, value로 넣어주면 된다.

이렇게 되면 이제 컨트롤러는 서블릿을 사용하지 않는다.

 

ControllerV3

public interface ControllerV3 {

    ModelView process(Map<String, String> paramMap);
}

이제 컨트롤러는 ModelView에서 뷰 이름과 전달할 데이터 정보를 포함하는 객체를 반환해주면 된다.

 

MemberFormControllerV3

public class MemberFormControllerV3 implements ControllerV3 {
    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form");
    }
}

 

MemberSaveControllerV3

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

 

MemberListControllerV3

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

 

 

paramMap.get("username"); 파라미터 정보는 map에 담겨있다. map에서 필요한 요청 파라미터를 조회하면 된다. mv.getModel().put("member", member); 모델은 단순한 map이므로 모델에 뷰에서 필요한 member 객체를 담고 반환한다

 

FrontControllerServletV3

@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);
        ModelView mv = controller.process(paramMap);
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);
        view.render(mv.getModel(), request, response);
    }
    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;
    }
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}

 

뷰 리졸버

MyView view = viewResolver(viewName)

컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경한다. 그리고 실제 물리 경로가 있는 MyView 객체를 반환한다. 

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

뷰 객체를 통해서 HTML 화면을 렌더링 한다. 뷰 객체의 render() 는 모델 정보도 함께 받는다

 

 

v4

기본적인 구조는 V3와 같지만 대신, 컨트롤러가 ModelView 를 반환하지 않고, ViewName 만 반환한다.

 

ControllerV4

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

 

ModelView가 인터페이스에 없는 모습이다.

 

MemberFormControllerV4

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

정말 단순하게 new-form 이라는 뷰의 논리 이름만 반환하면 된다.

 

이전 MemberSaveControllerV4와 MemberListControllerV4에서는 직접 Model을 생성했어야 하지만 이제는 생성할 필요 없이 파라미터로 바로 전달된다.

V3에서 생성 코드를 지우고 model.put("member", member);  로 바꿔주면 된다.

 

FrontControllerServletV4는 V3와 거의 동일하다.

 

 

V5 - 유연한 컨트롤러

 

여러 개발자들이 다른 버전으로 개발을 원한다면?

어댑터 패턴을 사용하자

어댑터 패턴 지금까지 우리가 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다. ControllerV3 , ControllerV4 는 완전히 다른 인터페이스이 따라서 호환이 불가능하다.

어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경하자.

 

public interface MyHandlerAdapter {
    boolean supports(Object handler);
    ModelView handle(HttpServletRequest request, HttpServletResponse response,
                     Object handler) throws ServletException, IOException;
}

 

boolean supports(Object handler) : 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드

 

ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)

: 어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환,

이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다.

 

ControllerV3HandlerAdapter

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3);
    }
    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse
            response, Object handler) {
        ControllerV3 controller = (ControllerV3) handler;
        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);
        return mv;
    }
    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;
    }
}

handler를 컨트롤러 V3로 변환한 다음에 V3 형식에 맞도록 호출한다.

ControllerV3는 ModelView를 반환하므로 그대로 ModelView를 반환하면 된다.

 

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() {
        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/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());
    }
    @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);
        MyView view = viewResolver(mv.getViewName());
        view.render(mv.getModel(), request, response);
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }
    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }
    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");
    }
}

private final Map<String, object> handlerMappingMap = new HashMap<>();

매핑 정보의 값이 ControllerV3 , ControllerV4 같은 인터페이스에서 아무 값이나 받을 수 있는 Object 로 변경되었다

핸들러를 매핑하고 핸들러를 처리할 수 있는 어댑터를 조회하는 코드가 추가된 것을 확인할 수 있다.

어댑터 호출 : ModelView mv = adapter.handle(request, response, handler);

'Back-end' 카테고리의 다른 글

03. 서블릿, JSP, MVC 패턴  (1) 2023.12.17
2. 서블릿  (1) 2023.11.28
01. 웹 플리케이션 이해  (0) 2023.11.19
09. 빈 스코프  (1) 2023.11.14
08. 빈 생명주기 콜백  (0) 2023.11.05