spring

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

kimkim615 2024. 8. 8. 02:18

프론트 컨트롤러 패턴 소개

프론트 컨트롤러 도입전에는, 공통로직이 필요하면 각각 공통로직을 깔고 별도의 로직을 또 깔았다

프론트 컨트롤러를 도입하면 공통로직을 거기에 몰고 별도의 로직을 따로 깐다.

즉 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받고, 요청에 맞는 컨트롤러를 찾아서 호출한다

입구를 하나로 만들어서 공통 처리가 가능하게 만든 것이다.

 

프론트 컨트롤러 도입 -v1

클라이언트가 http 요청을 하면 서블릿인 Front Controller가 요청을 받는다.

매핑 정보를 가지고 컨트롤러를 호출한다.

 

hello/servlet/web/frontcontroller/v1/ControllerV1.interface

서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다. 

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

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

 

hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1.class

hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1.class

hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1.class

implements ControllerV1으로 구현한다

내부 로직은 서블릿과 거의 같다

 

hello/servlet/web/frontcontroller/v1/FrontControllerServletV1

urlPatterns = "/front-controller/v1/*    -> 하위의 어떤게 들어와도 이 서블릿이 무조건 호출된다

key,value 형식으로 Map에 넣음 key: 매핑 URL value: 호출될 컨트롤러

controllerMap.put('/front-controller/v1/members/new-form", new MemberFormControllerV1()); 처럼 저장해놓는다

 

String requestURI = request.getRequestURI();  -> URI 그대로 받을 수 있다. 그리고 이게 /front-controller/v1/members

그러면 Map에서 키가 같은 /front-controller/v1/members를 찾고 가져오는게 MemberListControllerV1이다

ControllerV1 controller = controllerMap.get(requestURI); 부모는 자식을 담을 수 있다

controller.process(request,response); 값 있으면 프로세스 호출

 

그럼 process는 override된 메서드 호출

process에서 jsp로 넘김

 

과정을 정리하자면

프론트 컨트롤러가 요청 받고, 매핑 정보로 매핑된 컨트롤러를 찾고 호출하고, view로 포워드한다.

그럼 jsp가 랜더링되면서 클라이언트는 html 응답을 받게 된다.

 

View 분리 -v2

모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고 깔끔하지 않았음

 

프론트컨트롤러가 매핑정보로 컨트롤러를 찾아와서 호출하는데, 컨트롤러에서 이제 더이상 직접 jsp 포워드 하지않고

MyView라는 객체를 만들어서 반환하면 프론트 컨트롤러가 대신 MyView에 render() 호출하면 MyView가 jsp로 포워드하도록 할 것이다. 

 

hello/servlet/web/frontcontroller/MyView.class

view-path를 가진다

 

hello/servlet/web/frontcontroller/v2/ControllerV2.interface

기존 v1과 똑같은데, 기존에는 void였고 컨트롤러가 알아서 다 forward로 이동했는 이번엔 반환타입이 MyView이다.

 

hello/servlet/web/frontcontroller/v2/controller/MemberFormcontrollerV2.class

implements controllerV2한다.

기존에는 위 코드였는데 이제는

 

hello/servlet/web/frontcontroller/v2/FrontControllerServletV2.class

 

기존에서는 controller.process(request,response) 였는데

 

이렇게 코드를 바꾼다.

 

/front-controller/v2/members/new-form 가 들어오면

1. 프론트컨트롤러에서 urlPatterns= "/front-controller/v2/*" 이므로 서블릿이 호출이 된다

2. 그러면 controller에서 formcontrollerv2를 찾는다 

3. 찾아와서 process를 호출한다

그러면 for-controllerv2에 들어가서 new myview를 생성해서 넘겨준다

4. 그러면 반환 결과는 new Myview("/WEB-INF/views/new-form.jsp"); -> 생성했던 결과 인스턴스에 주소가 넘어온다

그러면 view.render를 호출하는거다 

5. render에는 viewpath가 들어가있는데, request.getRequestDispatcher(viewPath) 즉 WEB-INF/views/new-form.jsp가

그대로 들어간다. 그니까 newform.jsp로 dispatcher forward가 되는거다

6. 그래서 jsp에서 이제 실제 html 결과가 이제 응답으로 클라이언트에 내려간다

 

이제 컨트롤러는 MyView객체만 생성해서 반환해준다 그러면 front 컨트롤러에서 공통 로직을 호출하고 처리해준다

 

Model 추가 -v3

1. 서블릿 종속성 제거

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

요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다

또 request를 model로 사용하는 대신 별도의 model 객체를 만들어서 반환하면 된다

 

2. 뷰 이름 중복 제거

/WEB-INF/views/ ... 이러한 중복이 발생함

컨트롤러는 뷰의 논리 이름을 반환하고 실제 위치의 이름은 프론트 컨트롤러에서 처리하도록 한다

 

 

 

기존에는 view를 반환했는데 모델과 뷰가 합쳐진 modelview를 반환할 것이다

지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다

그리고 model도 request.setAttribute()를 통해 데이터를 저장하고 뷰에 전달했다

서블릿의 종속성을 제거하기 위해 model을 직접 만들고 추가로 view 이름까지 전달하는 객체를 만들어보려고한다

 

hello/servlet/web/frontcontroller/ModelView.class

viewName 뷰의 논리적 이름

Map<String,Object> model 

 

hello/servlet/web/frontcontroller/v3/ControllerV3.interface

ModelView process(Map<String,String> paramMap)

V2랑 비교해보면 서블릿 관련 기술이 아무것도 없다. 단순하고 우리가 직접 만드는거다

 

hello/servlet/web/frontcontroller/v3/controller/MemberFormControllerV3.class

return new ModelView("new-form") 논리적 이름을 넣었다

 

hello/servlet/web/frontcontroller/v3/FrontnControllerServletV3/.class

기존 Myview view = controller.process(request,response);  view.render(request, response); 에서

paramMap을 넘겨줘야되니까

 

 

getParameterNames로 모든 파라미터 이름을 다 가져오고 돌리면서 key의 변수명을 잡고 value는 request.getParameter로 다 꺼내온다

너무 디테일한 로직이기때문에 createParamMap 메서드로 뽑았다.

 

 

viewResolver가 기존경로 + viewname + .jsp 로 논리이름과 다 더해서 만들어준다

그래서 논리이름을 가지고 실제 물리 이름을 만들면서 실제 물리경로가 있는 myView를 반환해줄 수 있다

 

MyView에 아래 코드를 추가한다

 

model에 있는 value를 forEach로 다 꺼내서 request의 값을 다 담아놓는다

 

렌더가 오면 모델에 있는 값을 다 꺼내서 httpServletrequest에다가 setAttribute로 다 넣는다

다 끝나고오면 jsp forward가 되면서 jsp가 requestgetattribute를 쓴다

 

처음부터 해보자면 v3에 members/new-form 이 경로가 들어온다 그러면 frontControllerServlet에서

urlPatterns=/front-controller/v3/* 으로 매핑되어있으므로 MemberFormControllerV3가 호출이 됨

그전에 CreateParamMap을 가지고 HttpServletRequest에 있는 파라미터를 다 뽑아서 ParameterMap을 만들어 반환해줌

 

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

앞서 만든 컨트롤러에서는 항상 ModelView 객체를 생성하고 반환하는 부분이 좀 번거로워서 이를 개선한 컨트롤러이다

 

hello/servlet/web/frontcontroller/v4/ControllerV4.interface

String process(Map<String,String> paramMap, Map<String,Object> model);

여기선 ModelView가 없고 model객체는 파라미터로 전달되니까 결과로 뷰의 이름만 반환해주면 됨

 

hello/servlet/web/frontcontroller/v4/controller/MemberFormControllerv4.class

그냥 return "new-form";

 

hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerv4.class

파라미터에서 값 꺼내고, 내 비즈니스 로직 실행하고, 모델에다가 값 put으로 넣어주고 반환을 문자로 해주면 끝

모델이 파라미터로 전달되기 때문에, 모델을 직접 생성하지않아도 된다

 

hello/servlet/web/frontcontroller/v4/frontControllerServletV4.class

Map<String,Object> model = new HashMap<>(); -> 모델 추가

모델 객체를 프론트컨트롤러에서 생성해서 넘겨주는거다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 담긴다

 

String viewName = controller.process(paramMap, model);

MyView view = viewResolver(viewName);

컨트롤러가 직접 뷰의 논리 이름을 반환하니까 이 값으로 실제 물리 뷰를 찾을 수 있음

 

유연한 컨트롤러1 - v5

어댑터 패턴이란 지금까지 위에서 개발한 프론트 컨트롤러는 한 가지 방식의 컨트롤러 인터페이스만 사용할 수 있는데,

다양한 방식(v3,v4...)의 컨트롤러를 처리할 수 있도록 하는 패턴이다.

 

 

핸들러라는 개념은 컨트롤러라고 생각하면 된다. (v4를 처리하는 어댑터, v3를 처리하는 어댑터 ...)

이제 어댑터를 통해서 컨트롤러를 호출해야한다.

 

hello/servlet/web/frontcontroller/v5/MyHandlerAdapter.interface

boolean supports(Object handler)

어댑터가 해당 핸들러(컨트롤러)를 처리할 수 있는지 판단하는 메서드이다

처리할 수 있으면 true 반환, 아니면 false 반환

 

handle 메서드는 말 그대로 핸들러를 호출해주고 반환할 때 Model&View를 맞춰서 반환해준다

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

 

hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter.class

implements MyHandlerAdapter 해서 구현한다

 

V3가 넘어오면 참을 반환한다. 다른게 넘어오면 False 반환

 

 

이미 supports를 통해 controllerv3로 걸러졌기때문에 타입 변환은 걱정없이 controllerv3로 하면 된다

 

hello/servlet/web/frontcontroller/v5/FrontControllerServletV5.class

기존 코드 : private Map<String, ControllerV4> controllerMap = new HashMap<>();

바꾼 코드 : private final Map<String, Object> handlerMappingMap = new HashMap<>();

-> controller를 다 지원하기 위해서 Object를 넣었다

모든 자바 객체 최상위에는 항상 object가 있다 그래서 어떠한 자바 객체던 다 담길 수 있다

 

private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

-> 어댑터가 여러개 담겨있고 하나를 찾아서 써야하기 때문에 이 코드를 추가했다

 

 

핸들러 매핑 정보인 handlerMappingMap에서 URL에 매핑된 핸들러 객체를 찾아서 반환한다: 핸들러 찾기

 

 

handler를 처리할 수 있는 어댑터를 adapter.supports(handler)를 통해 찾는다

handler가 ControllerV3 인터페이스를 구현했으면, ControllerV3HandlerAdapter 객체가 반환된다

 

어댑터의 handle(request,response,handler) 메서드를 통해 실제 어댑터가 호출되다

어댑터는 핸들러를 호출하고, 그 결과를 어댑터에 맞춰서 반환한다

 

1. 클라이언트 요청하면 일단 핸들러 매핑 정보를 찾는다 getHandler로 찾아와서

이 핸들러를 처리할 수 있는 핸들러 어댑터를 getHandler 어댑터에다가 이 핸들러를 던지면서

v3 컨트롤러의 핸들 어댑터를 찾아온다

 

2. 핸들 어댑터를 호출해준다. 그러면 이 핸들 어댑터가 내부적으로 이 핸들러를 호출하게 된다.

그러면 그 속에서 v3 컨트롤러를 바꾼다음에 얘가 컨트롤러의 process를 호출해준다 

실제 컨트롤러 호출을 하고 ModelView를 반환해준다

 

3. 그다음에 viewResolver 호출해주고 View Name을 가져온다음에 View의 render호출해주면서 모델을 넘겨준다

 

유연한 컨트롤러2 - v5

FrontControllerServletV5에 앞에서는 v3에 관련된것만 포함했는데 v4에 관련된것도 추가해본다

 

hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter.class

 

handler가 ControllerV4인 경우만 처리하는 어댑터이다

 

 

handle 메서드 로직이다

handler를 ControllerV4로 캐스팅하고, paramMap, model을 만들어서 해당 컨트롤러를 호출한다

그리고 viewName을 반환받는다

 

 

ModelView를 어댑터에서 생성해준다.

이 과정이 어댑터 역할을 하는것이다 110V 꽂은거를 220V로 바꿔주는 걸 해주는것임

뷰도 세팅해주고 모델도 세팅해준다

모델뷰 자체를 반환해주면 끝난다

 

어댑터가 호출하는 ControllerV4는 뷰의 이름을 반환하는데, 어댑터는 뷰의 이름이 아니라 ModelView를 만들어서

반환해야한다. 그래서 ModelView로 만들어서 형식을 맞춰서 반환한 것이다.