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

블랑·2023년 3월 11일
0

스프링 인프런

목록 보기
2/4

복습 : MVC 패턴이란?

MVC(Model-View-Controller)는 소프트웨어 개발에서 사용되는 디자인 패턴 중 하나로, 애플리케이션을 세 가지 요소로 분리하여 각각의 역할을 할당한다.

  1. Model
    모델은 애플리케이션의 데이터와 비즈니스 로직을 담당한다. 데이터를 관리하고 필요한 처리를 수행한다.

  2. View
    뷰는 모델이 처리한 데이터를 사용자에게 보여주는 역할을 수행한다. 사용자가 요청한 결과를 출력하는 단계이다.

  3. Controller
    컨트롤러는 모델과 뷰를 연결해주는 역할이다. 사용자의 요청을 받아 모델을 조작하고 뷰에 전달한다.
    컨트롤러는 사용자와 모델, 뷰 간의 인터페이스 역할을 한다.

프론트 컨트롤러 패턴 소개

지금까지는 공통 로직을 깔고 별도의 컨트롤 로직을 깔았었다. 간단히 이야기 하자면, 쓸데없이 코드가 길었다는 이야기다.

모든 컨트롤러에 공통된 로직들은 낭비다.

공통 로직들을 프론트 컨트롤러에 담아두고 이를 수문장처럼 공유하는 개념이 프론트 컨트롤러 패턴이다.

프론트 컨트롤러 역시 Servlet에 포함된다.

이전에는 요청마다 서블릿을 만들었다면, 이제는 공통된 프론트 컨트롤러만 쓰고 공통 처리를 할 수 있게 만든다.

프론트 컨트롤러 도입 - v1

코드


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

<코드 분석>
1.urlPatterns
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
/* : 하위 모든 것들을 이 서블릿에서 받아들인다.

2.HashMap controllerMap : key - 매핑 URL, value - 호출되는 컨트롤러

정리

  1. controllerMap이라는 명칭의 HashMap<String, ControllerV1>을 선언한다.
  2. 해당 HashMap에는 각각 v1의 요소의 주소값(String)과 각각 class의 새로운 객체들이 생성되어 저장된다.
  3. 이후 요청이 들어올 때마다 해당 HashMap에서 키값을 찾아 정보를 얻게 된다. (컨트롤러의 역할을 한다.)

이를 다시 풀어서 설명하자면 다음과 같다.

  1. 클라이언트에서 프론트 컨트롤러로 HTTP 요청을 전송한다 (강의에서는 url에 직접 작성을 통해 요청을 전송한다.)
  2. 프론트 컨트롤러(HashMap)는 매핑된 값들에서 요청 값을 정리한다.
  3. 매핑 값이 있는 경우 해당 컨트롤러를 호출한다. (MemberFormControllerV1 등의 객체 값으로 HashMap에 저장했었다.)
  4. 해당 컨트롤러는 뷰를 호출하고 JSP를 렌더링한다.

여기서 각 컨트롤러는 인터페이스 구현을 통해 로직의 일관성을 유지하였다.

View 분리 - v2

코드

v1에서의 HashMap controllerMap의 내부 요소들에는 각각의 컨트롤러가 존재했다.
이러한 컨트롤러 중 하나를 살펴보자.

< 기존 컨트롤러 >
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"; 
        //모든 컨트롤러마다 각각 뷰로 이동하는 String 객체를 선언해 주어야 한다.
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

각각의 컨트롤러에서 뷰로 이동하는 객체인 viewPath를 선언하는 것은 비효율적이다.
이를 공통 분모로 묶어 해결해보자.

< 컨트롤러 공통 로직을 하나의 클래스로 만들어보자 >
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);
    }

기존에 JSP로 이동하는 과정을 render() 메서드에 집어넣을 것이다.
즉, MyView 클래스는 기존에 각각의 컨트롤러마다 존재했던 객체를 선언받아 이를 dispatcher를 통해 JSP로 이동하는 일련의 과정을 하나의 공통된 로직으로 세분화시킨 것이다.

이를 위에 있는 컨트롤러에 적용한다면 다음과 같다.

< 간략해진 new 컨트롤러 >
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");
    }
}

해당 과정을 요약하면 다음과 같다.

정리

  1. 프론트 컨트롤러에서 해당 컨트롤러를 호출한다.
  2. 새로운 MyView를 리턴한다.
  3. 프론트 컨트롤러 내부에 MyView가 리턴되고, 리턴된 값의 render() 메서드를 실행한다.

Model 추가 - v3

변경 사항

추가할 내용은 다음과 같다.

  1. 서블릿 종속성 제거 :
    프론트 컨트롤러는 서블릿의 세세한 기술들을 몰라도 동작할 수 있게 종속성을 제거할 수 있다.

  2. 뷰 이름 중복 제거 :
    뷰의 viewPath에서 항상 /WEB-INF/views.. 꼴의 중복 내용이 항상 발생하는 것을 알 수 있다. 전체 경로를 생략하고, 핵심 내용(new-form 등)만 반환하도록 수정할 수 있다.

Model 추가

지금까지 컨트롤러에서는 서블릿에 종속되는 HttpServletRequest를 사용했다. Model 역시도 동일했다. 이러한 종속성을 제거하기 위해 Model과 View의 이름을 전달하는 객체를 만들어 보겠다.


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

생성자와 getter/setter로 이루어진 단순한 클래스이다.
내부 파라미터는 viewName과 HashMap인 model로 이루어져 있다.
해당 클래스를 컨트롤러에 적용한다면? 다음은 FormController의 예시이다.


< V2 컨트롤러 >
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");
    }
}

--------------------

< V3 컨트롤러 >

public class MemberFormControllerV3 implements ControllerV3 {

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

지금처럼 Servlet의 종속성을 제거한 모습을 볼 수 있다.
어떻게 가능한가? 프론트 컨트롤러의 구문을 확인해보자.

< 프론트 컨트롤러 >

@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();     //논리 이름(ex:new-form)만 가져옴
        MyView view = viewResolver(viewName);   //viewResolver 메서드를 통해 전체 이름으로 변경

        view.render(mv.getModel(), request, response);  //렌더 함수 적용(JSP 화면 구성)
        // render 함수에 mv.getModel()의 새로운 인자가 들어감으로, myView 역시 새로 오버로딩해서 개정해야 함.
    }

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

    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 메서드를 통해 서블릿 종속성을 제거하고, viewResolver를 통해 불필요한 경로명을 압축할 수 있었다.
비교적 간단한 함수인 viewResolver를 제외하고, createParamMap 메서드의 역할을 조금 더 자세히 요약하자면 다음과 같다.

< 정리 >

해당 메서드로 서블렛 API 객체인 HttpServletRequest에서 전달받은 파라미터를 Map으로 변환한다.

  1. HttpServletRequest의 getParameterNames() 메서드를 호출하여 요청에 포함된 모든 파라미터의 이름을 Enumeration 타입으로 반환.

  2. asIterator() 메서드를 호출하여 Enumeration을 Iterator로 변환하고, forEachRemaining() 메서드를 사용하여 모든 파라미터의 이름을 하나씩 순회하며 Map 객체에 이름과 값을 추가.

  3. 마지막으로, 모든 파라미터의 이름과 값을 Map에 추가한 후에 해당 Map을 반환.

+ myView 클래스 오버로딩

또한, render 함수에 mv.getModel()의 새로운 인자가 들어감으로, 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);
    }
	
    //새로 추가 : 모델 객체가 들어오면 modelToRequestAttribute 메서드 실행
    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));
    }
}

단순하고 실용적인 컨트롤러 - V4

v3를 변형해서 실제 개발자들이 매우 편리하게 개발할 수 있는 v4버전을 개발해보자.
구조 자체는 v3과 같되, 컨트롤러가 ModelView를 반환하지 않고 Viewname만 반환한다.

프레임워크 입장에는 별로 고친 것이 없지만 개발자 입장에서는 구현이 훨씬 쉬워졌다.

  • 모델 객체 전달 : 모델 객체를 직접 프론트 컨트롤러에서 생성하여 파라미터 인자로 넘겨준다.
    Map<String, Object> model = new HashMap<>(); //추가
  • 뷰의 논리 이름을 직접 반환 : 컨트롤로가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해서 실제 물리 뷰를 찾을 수 있다.

파라미터를 직접 전달한다는 것만으로도 개발자는 직접 객체를 생성할 필요가 없어진다.

Adapter(유연한 컨트롤러) - V5

인터페이스 제약을 해소한다. 어떠한 컨트롤러든지 호출할 수 있다.

지금까지는 controllerMap의 파라미터에 해당 인터페이스를 직접 박았었다. (예를 들면 ControllerV4)
하지만 경우에 따라 V1도, V3도, V4도 .. 여러 종류의 컨트롤러를 전부 쓰고 싶다.

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

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

Q. 여러 가지의 컨트롤러를 전부 쓰고 싶지만, 
파라미터가 달라서 하나의 인터페이스를 쓰지는 못한다.
A. 핸들러 요소를 도입한다. 
= 하나의 공통된 인터페이스와, 기존 컨트롤러를 계승한 어댑터들의 집합을 추가한다.

이럴 경우 기존 구조에 핸들러 어댑터 목록을 추가한다.
핸들러 매핑 정보를 받았을 때, 만약 V4라면 어댑터에서 V4의 핸들러 어댑터를 받아 어댑터를 통해 핸들러(컨트롤러)를 호출한다.

MyHandlerAdapter

  • supports boolean 변수 : 핸들러 매핑 정보에서 핸들러 어댑터 목록을 찾을 때, 맞는 어댑터라면 true를 아닐 경우 false를 반환한다.

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

//MyHandlerAdapter 인터페이스

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

정리

  1. v1 : 프론트 컨트롤러 도입
  • 기존 구조를 유지하며 컨트롤러의 도입
  1. v2 : 뷰 로직 분리

  2. v3 : 모델 추가로 인한 서블릿 종속성 제거 및 뷰 이름 중복 제거

  3. v4 : v3와 유사하나 프론트 컨트롤러가 모델뷰를 직접 생성해 더욱 편리한 인터페이스를 제공

  4. v5 : 유연한 컨트롤러 : 어댑터의 개념 도입

출처 : 김영한 인프런(스프링 MVC 1편)

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

profile
안녕하세요.

0개의 댓글