Home MVC 프레임워크 제작(프론트 컨트롤러의 도입)
Post
Cancel

MVC 프레임워크 제작(프론트 컨트롤러의 도입)

프론트 컨트롤러

서블릿을 컨트롤러, JSP를 뷰로하는 MVC 패턴을 만들어봤다. 공통 처리가 힘들어 반복되는 작업이 많은 문제가 있었다.

이를 해결하기 위해 프론트 컨트롤러를 도입해보자.

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받는다.
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아 호출한다.
  • 공통 처리가 가능하다.
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.

스프링 웹 MVC의 DispatcherServlet이 프론트 컨트롤러 패턴으로 구현되어 있다.

프론트 컨트롤러 도입

프론트 컨트롤러를 단계적으로 도입해본다.

v1

기존 코드를 최대한 유지하면서 프론트 컨트롤러를 도입한다.

HTTP 요청을 프론트 컨트롤러 서블릿에서 받고 해당 컨트롤러가 URL 매핑정보를 이용해 실제 컨트롤러를 호출한다.

ex) /abcd URL은 A컨트롤러가 필요하다. 이런식의 매핑 정보이다.

우선 서블릿과 비슷한 모양의 인터페이스를 도입한다. 각 컨트롤러들은 이 인터페이스를 구현하게 된다.

Form 컨트롤러, Save 컨트롤러, List 컨트롤러 모두 위와 같이 ControllerV1 인터페이스를 상속받아 구현한다. 상속받아 구현하는 로직은 이전과 동일하다.

우선 프론트 컨트롤러의 urlPatterns = “/front-controller/v1/* 이다. 이것이 의미하는 바는 /front-controller/v1 하위 URL은 일단 다 front-controller를 호출하게 되는 것이다.

front-controller는 생성자를 통해 해당 하위 url들과 컨트롤러를 매핑해놓은 map을 구현한다. 이 map을 이용해 url에 맞는 컨트롤러를 꺼내쓰는 것이다.

프론트 컨트롤러는 ControllerV1 인터페이스를 호출해 구현과 관계없이 로직의 일관성을 가져갈 수 있다.

만약 클라이언트가 /front-controller/v1/members를 요청한다고 가정해보자.

/front-controller/v1의 하위 url이므로 프론트 컨트롤러를 호출하게 되고, 프론트 컨트롤러에서는 requestURI를 통해 /front-controller/v1/members를 얻는다.

map에서 해당 url을 key로 검색하면 그에 맞는 컨트롤러가 나타나게 되고, 그 컨트롤러의 로직을 호출해내면 정상작동 되는 것이다.

v2 - View의 분리

위처럼 프론트 컨트롤러를 도입해도 각각의 컨트롤러를 보면 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔끔하지 않다.

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

이를 개선해보자.

클라이언트가 요청하면 프론트 컨트롤러에서 받고 프론트 컨트롤러가 각 컨트롤러를 호출하는 것 까지는 똑같다.

하지만 컨트롤러에서 JSP를 직접 호출하지 않고 MyView라는 객체를 반환하고 프론트 컨트롤러가 MyView 객체의 render()를 호출하면 JSP forward가 실행되는 것으로 차이가 있다.

우선 MyView 객체를 만든다. viewPath를 받아 중복된 코드였던 dispatcher.forward를 해주는 객체이다.

컨트롤러 인터페이스는 이전의 void에서 MyView를 반환하도록 바꿔준다.

각 컨트롤러의 로직은 같지만 마지막에

이 부분을 MyView 객체에 Path를 반환하는 것으로 수정한다.

프론트 컨트롤러에서는 각 컨트롤러를 호출한다.

그 컨트롤러의 process 함수를 실행하면 MyView 객체를 반환하는 것이기 때문에 MyView 객체의 render()를 호출하면 되는 구조이다.

MyView 객체에 중복된 코드(dispatcher)를 포함시킴으로써 각 컨트롤러는 중복된 부분을 작성하지 않아도 된다.

v3 - Model 추가

save 컨트롤러를 봐보자.

컨트롤러 입장에서 HttpServletRequest와 HttpServletResponse가 꼭 필요할까?

요청 파라미터 정보(username, age)는 자바의 Map으로 대신 넘기도록 한다면 지금 구조에서는 컨트롤러가 서블릿 기술을 몰라도 작동할 수 있다.

그리고 request 객체를 View를 위한 Model로 사용하는 대신 별도의 Model 객체를 만들어 반환하면 된다.

이렇게 하면 구현 코드도 매우 단순해지고, 테스트 코드 작성도 쉽다.

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

추가로 현재는 “/WEB-INF/views/members.jsp” 처럼 뷰 이름에 중복이 있는데 이를 컨트롤러에서는 “members” 와 같이 뷰의 논리 이름을 반환하도록 바꿔주고 위와 같은 실제 위치는 프론트 컨트롤러에서 처리하도록 한다.

이렇게 하면 뷰의 폴더 위치가 변경되어도 프론트 컨트롤러만 수정하면 된다.

어떠한 변경이 일어났을 때 한 곳에서만 변경이 일어나도 된다면 좋은 설계인 것이다.

지금까지는 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했고 Model도 request.setAttribute()를 통해 데이터를 저장하고 뷰에 전달했다.

서블릿의 종속성을 제거하기 위해 Model을 직접 만들고 추가로 View 이름까지 전달하는 객체를 만든다.

ModelView를 만든다. 이는 컨트롤러가 반환할 객체이며 request를 대체할 model, View 이름 중복을 대체하기 위한 viewName이 들어간다.

컨트롤러 인터페이스는 ModelView를 반환하도록 한다.

Save 컨트롤러이다. ModelView 객체에 viewPath를 대신하기 위한 save-result를 넣는다.

ModelView에서 Map으로 Model을 대체하기 때문에 .getModel()로 Model Map을 가져오고 거기에 setAttribute(“member”, member) 와 같이 put을 해주어 request의 Model과 같은 역할을 하도록 한다.

그리고 paramMap에서 파라미터를 받아온다.

이 paramMap은 어디서 받아오는가?

프론트 컨트롤러이다.

request 객체를 이용해 paramMap을 만든다. 모든 파라미터의 Name을 불러와 그 name을 키로 name에 맞는 파라미터를 value로 매핑시키는 것이다.

그리고 ModelView에 저장된 Model과 View 이름으로 MyView를 통해 render해주는 작업이 필요하다. ModelView는 Model과 View이름을 저장하기 위한 수단일 뿐이다.

View 이름을 이용해 온전한 경로를 넣어 MyView 객체를 만든다.

그리고 render()를 할 때 Model을 사용한다.

원래 이전 버전에서는 각각의 컨트롤러에서 request.setAttribute()를 이용했었다. 하지만 지금은 컨트롤러에서 setAttribute를 사용하지 않는 구조로 바꾸고 있다.

따라서 MyView에서 render() 할 때 ModelView의 Model과 request.setAttribute()를 이용해 데이터를 저장하고 보여준다.

ModelView에 있는 Model (Map)을 전부 꺼내 한번에 request.setAttribute() 해준다.

(스프링 MVC를 제대로 모르고 조금만 사용해봤음에도 이 복잡한 구조를 대신해주는게 정말 크다는게 느껴진다.)

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

앞의 v3은 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다.

그런데 실제 컨트롤러 인터페이스를 구현하는 개발자 입장에서 보면 항상 ModelView 객체를 생성하고 반환하는 부분이 조금 번거롭다.

좋은 프레임워크는 아키텍처도 중요하지만 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다.

단순하고 실용적으로 바꿔본다.

기본적인 구조는 v3과 같지만, 컨트롤러가 ModelView 객체가 아닌 ViewNameㄱ만 반환한다.

ModelView 객체를 사용하지 않는다. 컨트롤러 인터페이스를 보면 요청 파라미터 정보를 받을 수 있는 ParamMap, 그리고 Model을 파라미터로 받는다.

그리고 반환형이 String인데 이는 ViewName을 바로 반환한다.

각 컨트롤러를 보자.

요청 파라미터 정보는 똑같이 받고, model을 파라미터로 받아 거기에 정보를 집어넣는다.

그리고 viewName을 반환한다.

프론트 컨트롤러를 봐보자. model을 파라미터로 주어야하기 때문에 선언하고 넣어준다.

controller.process(paramMap, model)을 실행하면 model에는 필요한 정보가 put되고, viewName 반환값을 받을 수 있다.

해당 viewName을 이용해 MyView의 render()를 실행하면 된다. 단, ModelView에서 getModel()로 받아왔던 model을 단순히 프론트 컨트롤러에서 제공할 수 있다.

컨트롤러에서도 훨씬 단순하고, 프론트 컨트롤러에서 사용하기도 훨씬 쉬워진 것을 볼 수 있다.

모델을 파라미터로 넘기고 뷰의 논리이름을 반환한다는 작은 아이디어에서 이런 결과가 나왔다. 프레임워크가 점진적으로 발전하는 과정 속에서 이런 방법이 발견 된 것이다.

v5 - 유연한 컨트롤러

일단은 보기에 v4 구조가 가장 사용하기 쉽지만 상황에 따라 v3 컨트롤러 구조 등을 사용하고 싶을 수 있다. 이런 경우를 대비해 어떤 컨트롤러를 사용해도 괜찮도록 구조를 바꾸어 본다.

어댑터 패턴의 사용

  • 핸들러 어댑터 : 중간에 어댑터가 추가되었다. 여기서 어댑터 역할을 해줌으로써 다양한 종류의 컨트롤러를 호출할 수 있다.
  • 핸들러 : 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다.
    • 이제 어댑터가 있기 때문에 꼭 컨트롤러의 개념 뿐 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.

어댑터 인터페이스이다.

  • boolean supports(Object handler)
    • handler는 컨트롤러를 말한다.
    • 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드이다.
  • ModelView handle()
    • 어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환해야 한다.
    • 실제 컨트롤러가 ModelView를 반환하지 못하면 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다.
    • 이전에는 프론트 컨트롤러가 실제로 컨트롤러를 호출했지만 이제는 이 어댑터를 통해 실제 컨트롤러를 호출한다.

위는 V3어댑터이고 아래는 V4어댑터이다.

프론트 컨트롤러에서 supports를 먼저 호출해 알맞은 컨트롤러인지 체크를 먼저 하고 handle을 불러오기 때문에 handle()에서는 handler를 알맞게 캐스팅해서 사용해도 된다.

그리고 이 캐스팅한 handler, 즉 컨트롤러를 이용해 process를 하면 원하는 ModelView를 얻을 수 있다.

(createParamMap()은 아래에 있어야 한다.)

ModelView가 필요한 v3이든 필요없는 v4이든 핸들러에서는 ModelView를 반환해야 한다.

그래서 v4 어댑터에서는 ModelView를 따로 생성해준다.

이제 이 둘을 다루는 프론트 컨트롤러를 보자.

생성자에서 handlerMappingMap과 handlerAdapters를 초기화 해준다.

handlerMappingMap은 각 컨트롤러에 맞는 URL을 등록해 초기화한다.

handlerAdapters는 아까 만들어둔 어댑터들을 List에 넣어준다.

우선 핸들러를 가져와야 한다.

핸들러는 requestURI를 이용해 아까 handlerMappingMap에 저장해놓은 대로 알맞은 컨트롤러를 불러온다. (핸들러 = 컨트롤러)

그리고 이 핸들러를 이용해 알맞은 어댑터를 가져온다.

이 어댑터의 handle()을 호출하면 ModelView를 호출할 수 있고 그 ModelView를 이용해 view를 렌더링하는 과정은 V3과 같다.

정리

프론트 컨트롤러만 보면 메인 service 부분은 어떤 컨트롤러가 추가 되어도 바꿀필요 없고, 위의 생성자 부분에만 각 URL에 맞는 컨트롤러 주입과 어댑터만 넣어주면 된다.

전체적으로 본다면 역할과 구현이 분리되어 있는 잘 설계된 구조이다.

스프링 MVC는 이러한 구조와 거의 같은 구조를 가지고 있다.

스프링 MVC를 다음 포스트부터 다루어 본다.

This post is licensed under CC BY 4.0 by the author.