프론트 컨트롤러 패턴 소개
프론트 컨트롤러 도입전에는 이전 포스팅과 같이 유저가 요청을 서버로 보내면, 해당하는 서블릿에서 로직이 수행되었다.
그렇기 때문에 공통로직을 처리해야하면, 서블릿 컨테이너에 있는 모든 서블릿에 각각 공통처리 로직이 들어가 있어야 했다.
프론트 컨트롤러 패턴이 적용된 후에는
서블릿 하나로 클라이언트의 요청을 받는다.
프론트 컨트롤러가 공통처리 후에 요청에 맞는 컨트롤러를 찾아서 호출해준다.
즉 입구가 하나!
프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
이게 무슨말이냐 하면, 이전에 입문편과 기본편에서는 서블릿에 대해서 설명하지도 않았고, 그냥 컨트롤러에다가 RequestMapping으로 그냥 url만 적어주면, 요청 보낸 URL에 딱 맞게 알아서 매핑되서 컨트롤러가 호출되었다.
여기서 Spring의 앞단인 디스페쳐 서블릿이 처리를해서 컨트롤러로 맵핑해준것이다.
즉 Spring MVC 프레임워크에서는 프론트 컨트롤러인 DispatcherServlet이 모든 웹 요청을 받아서 적절한 컨트롤러로 요청을 전달한다.
따라서 개별 컨트롤러는 직접 서블릿을 구현할 필요가 없다.
이렇게 되면, 컨트롤러는 HTTP요청을 처리하는데 집중할 수 있으며 서블릿 api에 직접 의존하지 않아도 된다.
즉, 프론트 컨트롤러 패턴을 사용하므로서 springMvc는 개발자가 서블릿을 직접 다루는 복잡성에서 벗어나 더 높은 수준에서 웹 애플리케이션을 구현할 수 있게 해준다.
단계적으로 프론트 컨트롤러를 도입하도록 해보자.
V1구조
클라이언트가 보내는 모든 HTTP 요청들이 우선 FrontController로 들어가게 된다.
그다음, 해당하는 URL 패턴으로 컨트롤러를 조회해서 그 컨트롤러에서 로직을 수행한 뒤에 JSP Forward해줘서 HTML 응답을 한다.
ControllerV1
일단 서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다.
갑자기 일단 서블릿과 왜 비슷한 모양을 도입하냐? 궁금할 수 있는데 잠깐 save 서블릿을 보자
여기서 save 서블릿을 보면 해당 url로 들어오면 이 service메서드가 실행된다.
그래서,
ControllerV1 인터페이스를 만들고, 이 process를 구현하면,
해당 컨트롤러에서 process메서드를 실행시, 서블릿의 service메서드가 호출되는것과 비슷하게 만들었다.
이런식으로,
일단 인터페이스를 사용하여서 왜 다형성을 사용했는지 이해가 안되더라도 뒤에 코드를 보면서 이해할 수 있으니 넘어가보자.
MemberFormControllerV1
process매서드를 오버라이드 해줬다. process메서드의 로직은 MemberFormServlet과 동일하다.
MemberSaveControllerV1
process로직은 MemberSaveServlet과 동일하다.
MemberListControllerV1
FrontControllerServletV1
이제 프론트 컨트롤러를 만들어보자.
우선 처음에 서블릿이 등록되어 서블릿 컨테이너에 올라갈때, key, vlaue로 HashMap에 등록해준다.
이러면 나중에 만약 "/front-controller/v1/members/new-form"을 key로 접근하면, HashMap에 있는 MemberFormControllerV1인스턴스에 접근 가능하다.
서블릿에서는 service메서드가 실행되는데-> url요청이 들어오면,
그래서 /front-controller/v1/이후에 들어오는 모든 요청들을 frontcontrollerservletv1이 처리 하기 위해서 urlPatterns에 /front-controller/v1/*설정을 하였다.
이러면, /front-controller/v1/a /front-controller/v1/a/b 모든 요청들을 이 frontControllerServletV1에서 처리하게 된다.
service메서드
간단하다. 우선 request.getRequestURI로 요청 URL을 가져온다.
그 URL을 바탕으로 controllerMap.get메서드를 호출하면, 해당 URL에 필요한 객체 인스턴스를 가져올 수 있다.
이제 왜 다형성을 사용해서 ControllerV1인터페이스를 구현했는지 알겠는가?
만약 ControlerV1이 없으면, if문을 3개만들어서
if(requestURI=="/front-controller/v1/members/new-form"){
MemberFormController memberFormController = controllerHashMap.get(requestURI);
}
이런식으로 3개의 if문을 만들어서 할 수도 있지만, 이러면 너무 귀찮고, 빼먹는게 생길 수도 있으니까.
다형성을 활용하여서 부모의 인터페이스로 process를 호출하였다.
당연히 부모의 process메서드를 호출했으니까 인터페이스를 구현한 구현체의 오버라이드 된 process메서드를 실행하게 된다.
모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔끔하지 못하다.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
그러므로 부분을 별도로 처리하는 객체를 만들어보자.
V2구조
이제는 controller에서 jsp forward로 jsp파일로 가는게 아니라,
그냥 Myview객체만 반환하고, MyView객체서 render()메서드 호출을 통해서 JSP forward를 해준다.
MyView
Myview객체에서는 JSP파일 경로에 해당하는 viewPath를 생성자를 통해서 주입받고, 아까 반복되었던 dispatcher.forwad부분을 render메서드를 통해서 구현하였다.
ControlelrV2
여기서 중요한 점은 이전에는 그냥 void형으로 process를 실행하였지만, 위에서 모식도를 봤듯이, 우리는 비즈니스 로직을 처리하고 MyView객체를 반환할것이다.
MemberFormControllerV2
여기서는 특별한 비즈니스 로직이 필요없이 그냥 view만 랜더링 해주면 되니까, 오버라이딩한 process메서드에서 Myview객체를 만들때, 해당하는 경로의 viewPath를 넣어준다.
MemberSaveControllerV2
여기서는 비즈니스 로직을 동일하게 처리한후에, Myview객체를 만들어서 return해준다. viewpath를 해당 save jsp파일 경로와 맞게 설정한다.
MemberListControllerV2
FrontControllerV2
controllerMap부분에서 ControllerV2로 Map의 value부분을 바꿔주고, Map에 넣을때도 해당버전에 맞게 넣는다.
그다음에 중요한부분은 service메서드의
Myview view = controller.process(request,response);
이 부분이다. controller.process를 진행하면, save로 예시를 들면,
이런식으로 viewPath를 가지고있는 Myview가 반환이 된다.
그러므로, view.render를 통해서 Myview class의 메서드 render안에 있는 dispatcher를 사용할 수 있다.
이렇게 프론트 컨트롤러의 도입으로 MyView 객체의 render()를 호출하는 부분을 모두 일관되게 처리할 수 있다. 각각의 컨트롤러는 MyView객체를 생성만 해서 반환하면된다.
우리는 두가지의 구조변화를 살펴볼것이다.
1번에 대해서 살펴보면, 컨트롤러 입장에서 HttpServletRequest,HttpServletResponse를 알 필요가 없다.
그러니까, 저장에서 보면, 당연히 request.getParameter로 username에 해당하는 value를 가져와야하지만, HttpServletRequest없이
그냥 key username, value kim 이런식으로 값만 알면된다는것이다.
2번에 대해서 살펴보자면, 컨트롤러는 이제 view이름만 반환하게 한다. 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 한다.
이전에는 Myview객체에다가 /web-inf--어쩌구 이 전체 경로를 넣어줬는데 이제는 그냥
/web-inf/views/new-form.jsp -> new-form으로만 변경하게 한다.
V3구조
이제는 컨트롤러에서 ModelView를 반환한다.
ModelView
지금까지 컨트롤러는 서블릿에 종속적인 HttpServletRequest를 사용하고, Model도 request.setAttribute로 데이터를 저장해서 view에 전달했다.
서블릿 종속성을 제거하기 위해서 request가 아닌 모델을 직접만듥고, 추가로 view이름을 전달하는 객체를 만든다.
viewResolver
이렇게 반환된 ModelVeiw객체를 통해서 viewResolver를 호출하여 해당경로에 해당하는 전체경로를 가진 Myview를 만들고, 이 Myview가 render함수를 호출하여 렌더링 한다.
처음에는 어려울수 있지만, 하나하나 차근차근 알아보자.
이런경우, 모식도를 따라가는게 굉장히 좋은데 이번에는 순서를 바꿔서, 모식도대로 따라가면서 설명을 해보겠다.
처음에 save를 기준으로 설명을해보겠다.
FrontController로 요청이 들어옴
FrontController
일단 여기까지는 앞과 동일하다. 그냥 바뀐게 있다면, v2에서 v3로 경로가 바뀌고, 당연히 HashMap에 들어갈 인스턴스들도 새롭게 V3로 만들꺼니까 V3로 설정하면된다.
컨트롤러 조회
항상 서블릿으로 요청이 들어오면, service메서드가 실행된다고 말하였다.
requestURI에 해당하는 ControllerV3을 가져온다.
다시한번 ControllerV3인터페이스를 활용하여 다형성을 사용한 이유는 이렇게 안하면, if문 3개 만들어서 if requestUri=="memberForm"뭐 이런식으로 확인을 일일해서 사용해야하니까.
그다음에 createParamMap이 등장하였다.
자 여기서 왜 createParamMap이 등장한걸까?
우리는 더이상 컨트롤러에서 request를 사용하지 않는다고 하였다.
이말이 뭐냐면, 결국, request.getParameter를 컨트롤러에서 사용하지 못한다는 말이고,
결국 username: kim age :20을 가져와서 저장하기 위해서
Map에다가 각각 key를 string으로 하여서 Map에다가 kim,20을 저장하고
이 Paramap에 해당하는 Map을 process메서드의 인자로 넘겨주는것이다.
requst에서 각 파라미터를 꺼내와서 Map에 넣어준다.
컨트롤러 실행
자 이제 이 컨트롤러의 process메서드를 실행할것인데,
중요하게 봐야하는것은 이제 파라미터에 서블릿에 종속적인 HttpServletRequest, HttpServletResponse같은게 없다. 왜? 이제는 request.getParameter가 아닌, 이제 그냥 Map에서 get으로 가져오면되니까,
그렇게 저장하는 로직을 수행하고, 모식도처럼 이제 ModelView를 반환한다.
ModelView
여기서 살펴볼것은 두가지 이다.
하나는, ModelView에서 어떤, Jsp file을 불러올지 알아야하니까, 아까 ModelView에서 인자로 넘겨줬던, save-result path를 viewName으로 생성자 주입으로 만들어주는것이고, 두번째는 username:kim age 20을 보여줘야하니까,
우리는 더이상 request를 모델로 사용하지 않으므로,
Map을 만들어서 kim과 20을 넣어준다.
그래서 Object인거다 value가 String이 아니라, 왜? 숫자가올지 문자가 올지 모르니까,
여기까지 하면, ModelView에 viewName=save-result
그리고 Map에 username:kim, age:20이 담겨있는상태이다.
viewResolver실행
다시 FrontController로 돌아와보면 결국 우리는 controller.process 메서드를 통해서 반환되는 ModelView까지만 얻은것이다.
그러면, viewResolver를 통해서 현재 ModelView에있는 상대경로 save-result를 가지고 실제 렌더링할 Myview를 만들어줘야한다.
Myview
우선 위에서 viewResolver메서드를 통해서 전체경로를 만드는데 Myview객체의 인자로 넘겨준다.
myview객체를 통해서 viewPath에 이제는 전체 경로인 /web-inf/views/save-result.jsp가 들어가게 된다.
render메서드 호출
다시 FrontController로 오면, view에서 render메서드를 호출할때, 당연히 ModelView에 담겨져 있는 Model를 가져와야한다.
왜냐하면, 현재 Myview에는 저장결과를 보여줄 kim,20이 없고, 이건 ModelView에있기 때문이다.
그래서 인자로, mv.getModel을 통해 아까
Map<String,Object> model로 만들어져있는걸 넘겨주는 것이다.
이제 render메서드를 살펴보면, 우선 modelToRequestAttribute가 호출된다. 당연히 렌더링을 하기위해서는 request에다가 내가 model에 담겨있는 kim과 20을 담아야 한다.
그러므로 forEach를 돌면서, request에다가 username:kim, age:20을 전부 담아준다.
그 다음에 이 담겨져있는 request를 사용해서 이전과 동일하게 getRequestDispatcher와 forward를 사용하면된다.
나머지
좋은점
이렇게 해서 좋은점이 일단, 컨트롤러들이 서블릿 종속성에서 벗어난다는 것이다.
이말이 뭐냐면, 이전에는 내가 얻고자하는 data가 결국 request에 담겨있기 때문에, 컨트롤러에서 data를 꺼내서 비즈니스 로직을 수행하기 위해서는 반드시, 서블릿에 종속적이게 컨트롤러 파라미터에 HttpRequest request 이런게 있어서 request.getParameter로 꺼내왔는데,
이제는 이렇게 안하고 request없이 Map에서 꺼내오면 된다.
그다음 좋은점은 그냥 viewResolver를 사용하는건데, 이건 아 이전처럼 그냥 컨트롤러에서 전체경로 만들어서 그냥 render메서드 호출하면 되는거 아닌가? 왜이렇게 복잡하게 viewResolver를 만들었지? 하면,
만약에 /web-inf/view라는 폴더 이름이 변경된다 치자,
이러면, 컨트롤러를 하나하나 뒤져가면서 다 바꿔줘야한다.
그런데 이제는 어차피 save-result만 넘겨주면 되니까,
만약 경로가 변경되더라도 viewResolver메서드에서 이 한군데만 고치면 된다.
앞서 만든 v3컨트롤러는 서블릿 종속성제거, 뷰 경로 중복 제거등 잘 설계되었다.
그런데 개발자 입장에서, ModelView객체를 항상 생성하고 바환하는 부분이 조금 번거롭다. 수정해보자
V4구조
이제는 중요한게, 호출을할때, model을 넘겨주고 컨트롤러에서 ModelView객체를 반환하는게 아닌 ViewName만 반환한다.
ControllerV4
MemberFormControllerV4
이제는 viewname만 반환한다.
MemberSaveControllerV4
여기서도 viewname만 반환한다. 그러나 살펴봐야하는게, 파라미터로 넘어온 model에다가 put메서드로 우리가 만든 member를 넣어준다.
여기서 model이 어떻게 쓰이는지는 뒤에 FrontController에서 살펴 볼 것이다.
MemberListController
여기서 보면, model.put으로 MeberList를 넣는것을 볼수 있다.
반환형은 viewname이다.
FrontController
대망의FrontController부분이다. 여기서 궁금증이 있다. 컨트롤러 부분에서 model에다가 put으로 넣어주는건 알겠다. 근데 이 model을 FrontController가 어떻게 사용하냐? 이건데
코드를 봐보자
변한건 여기 하나다.
바로, Map<String,Object>model = newHashMap으로 model을 frontController에서 만드는것이다. 그리고, 이 model을 process메서드에 넘겨준다.
이건 기본형이 아니라 참조형이므로,
process메서드를 실행하면서 아까 봤던 그 파라미터에있던 model에다가, username:kim, age:20이 들어가게된다.
그래서 view.render할때 이제는 ModelView를 넘기는게 아니라, model만 넘기면된다.
이제 최종버전을 만들어보자.
만약에 어떤 개발자는 앞에서 v3버전으로 개발하고 싶고, 어떤 개발자는 v4버전으로 개발하고싶다.
그런데 생각해보면, controllerv3은 ModelView를 반환하고, controllerV4는 viewname을 반환했다.
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
앞에서는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있지만, 이제는 어댑터 패턴을 사용하여서, 프론트 컨트롤러가 다양한 패턴을 처리할 수 있게 만들 것이다.
V5
여기서 핸들러를 컨트롤러라고 생각하면 편하다.
MyHandlerAdapter
supports: MyHandleerAdapter가 ControllerV3를 처리할 수 있는지 없는지 확인한다.
ModelView handle: ControllerV3를 보면, process메서드 후에 ModelView를 반환한다.
그러므로 MyHandlerAdapter의 handle메서드를 실행하고 난 뒤에는 이 ModelView객체를 반환해줘야한다.
ControllerV3HandlerAdaptor
ControllerV3HandlerAdaptor는 MyHandlerAdaptor를 구현해야한다.
왜일까? 지금 내가 만약 MemberSaveControllerV3 인스턴스를 사용하고 싶다고 생각해보자, 그러면 ControllerV3HandlerAdaptor와 ControllerV4Handler가 이 MemberSaveControllerV3를 처리할 수 있는 어댑터인지 확인해야하기 때문이다.
고로 도식표를 그려보자면,
CotnrollerV3HandlerAdaptro->MemberFormControllerV3,MemberSaveControllerV3,MemberListControllerV3
CotnrollerV4HandlerAdaptro->MemberFormControllerV4,MemberSaveControllerV4,MemberListControllerV4
이러하다.
그러므로, supprots메서드에서 해당 어댑터가 해당 컨트롤러를 처리할 수 있는지
확인한다.
그리고, controllerV3이면 반환형이 ModelView이므로, handle메서드에서 반환형을 ModelView로 설정하고, Object handler에 지금 MemberSaveControllerV3이 들어있다 생각하면, 다형성을 통해서 ControllerV3에 캐스팅이 되고,
그다음에 원래 이전의 v3버전에 맞게 createParamMap을 통해서 username:kim, age: 20을 뽑아서, MemberSaveControllerV3의 process 메서드 호출후에 반환되는 modelView객체 mv를 반환하면 된다.
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());
}
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);
}
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");
}
}
코드를 보면서 이해해보자.
우선 handlerMappingMap 에다가는 당연히 이전처럼 동일하게 MemberFromControllerV3, MemberSaveControllerV3, MemberListControllerV4를 담아야한다.
그다음에, Myhandler는 support,handle메서드가 있는 인터페이스이고
이 Myhandler의 구현체인 ControllerV3HandlerAdator에서 보면,
위의 각 컨트롤러들은 ControllerV3의 인터페이스의 구현체들이기때문에,
ControllerV3HandlerAdaptor가 작업을 수행하기 위해서,
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
ControllerV3의 구현체인지 확인하는 과정이 있었다.
그래서 initHandlerAdaptors메서드를 통해서 handlerAdapters에다가 ControllerV3HandlerAdaptor를 넣어준다.
실제 이제 service메서드를 따라가보면,
일단 class 최상단이 Object니까 자바에서는 getHandler(request)를 통해서 save라 가정하면 Object handler = MemberSaveControllerV3가 된다.
그다음에, getHandlerAdaptor를 통해서 MemberSaveControllerV3를 처리 할 수 있는놈을 꺼내온다.
getHandlerAdaptor메서드를 보면, 이제야 왜? MyHandler인터페이스를 만들었는지 이해할 수 있다. 왜냐면, 만약 MyHandler가 없다면,
문론 이런식으로 만들수는있다.
List< ControllerV3HandlerAdaptor> controllerv3HandlerAdaptor
List< ControllerV4HandlerAdaptor> controllerv4HandlerAdaptor
이렇게 두개를 만들어놓고,
그다음에 이 controllerV3HandlerAdaptor.supports(handler);
controllerV4HandlerAdaptor.supports(handler);
이런식으로 각각 전부 만들려면 할 수 있는데 이것보다. 다형성을 활용하는게 필수적이다.
이렇게 MyHandlerAdapter adapter = ControllerV3HandlerAdaptor 되고,
그다음에 ControllerV3HandlerAdaptor의 handle메서드를 부른다. 당연히 여기서 이 어댑터에서, newForm을 할껀지 save를 할건지 List를 할건지 알아야하니까, 3번째 인자로 해당하는 MemberSaveControllerV3를 준다.
이렇게 되면 반환형이, ModelView였고, 그다음에 뒤에 로직은 동일하게 Myview로 viewResolver로 전체 경로만들고, 렌더링 하면된다.
여기서 부터 어댑터의 진가가 나타나게된다.
V5의 모식도를 다시 가져와서 살펴보자
여기서 어딜 봐야하면, 핸들러 어댑터가 ModelView를 반환하고, Frontcontroller가 ModelrView를 통해서 render메서드를 호출하는겄이다. render(model) 인자로 넣어줬음.
그런데 이러면, 생각이 드는게 어? 그러면 ControllerV4Adaptor가 MemberSaveControllerV4컨트롤러가 맞으니까 handle메서드를 호출해서 내부의 process메서드를 호출해가지고, string Viewname을 반환하는건 알겠어,
근데 중요한게 V4버전에서는 viewname만 반환했는데 ModelView를 어떻게 반환해?
여기서 나온게 핸들러 어댑터다.
ControllerV3는 운좋게 반환형이 ModelView여가지고 그냥 쓰면 되었지만,
ControllerV4는 String Viewname을 반환하니까, 이 ControllerV4Adaptor가 반환된 String Viewname을가지고 ModelView를 만들어서 FrontController한테 전달해야한다.
한번 모식도 대로 FrontController부터 따라가보자.
일단 이전에 v3추가할때 처럼 추가만 해주면된다.
그 밑에 여기 로직은 아에 손도 대지 않고, 그대로 둔다.
왜? 어차피 컨트롤러가 어떤식으로 반환을 하던지간에 핸들러 어댑터가 ModelView로 바꿔서 반환해줄꺼니까.
그러면 순서대로 ControllerV4Adaptor의 handle메서드를 봐보자.
supprots부분은 동일하다. ControllerV4의 instance인지 확인하는것이다.
이러면 아까 FrontControllerV5에서 getHandlerAdaptor메서드에서
우선 ControllerV3HandlerAdaptor를 꺼내고 이건 근데 ControllerV3의 instance니까 false 그다음에 ControllerV4HandlerAdaptor를 꺼냈는데 ControllerV4의 instance가 맞아 그러면, 이제 ControllerV4HandlerAdaptor가 선택되어 handle메서드를 호출하는것이다.
그다음에 중요한게, 여기 핸들러 어댑터에서, model 과 paramMap을 만들어서 process메서드의 인자로 넘겨준다.
그러면 paramMap에는 username:kim, age:20이 담겨있고, 이 model에는 처음에는 비어져 있지만, process메서드를 수행하고, username:kim과 age: 20이 담겨있을 것이다.
그러면 이 v4어댑터에서 ModelView를 만들어서 여기다가, viewName과 model를 넣어서 반환해준다.
즉 중요한점은,
어댑터가 호출하는 ControllerV4는 뷰의 이름을 반환한다. 그런데 어댑터는 뷰의 이름이 아니라
ModelView 를 만들어서 반환해야 한다. 여기서 어댑터가 꼭 필요한 이유가 나온다.
ControllerV4 는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식을 맞추어
반환한다. 마치 110v 전기 콘센트를 220v 전기 콘센트로 변경하듯이!
이번 시간 부터는 실제 스프링 MVC구조와 비교를 하면서 MVC에 대해서 알아볼 것이다.
직접만든 Mvc프레임워크 구조
스프링 MVC구조
우리가 만든 스프링 MVC구조와 실제 스프링 MVC구조가 매우 비슷한 것을 볼 수 있다.
그렇다면, 실제 DispatcherServlet이 어떤 역할을 하는지 살펴보자.
스프링 MVC도 프론트 컨트롤러 패턴으로 구현되어있고, 이 프론트 컨트롤러가 DispatcherServlet이다.
DispatcherServlet
DispatcherServlet도 부모 클래스에서 HttpServlet을 상속받아서 사용하고, 서블릿으로 동작한다.
이전부터 서블릿이 호출되면, HttpServlet이 제공하는 service()가 호출된다고하였다.
그러면, DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해뒀는데, 여기서 시작되면서 DispatcherServlet.doDispatch()가 호출된다.
과정을 보자면,
DispatcherServlet에 doService가 있고, FrameworkServlet에서 service가 실행되면서, do Service가 호출된다.
그리고 do Service메서드 내부에
doDispatch메서드가 있고,
do Dispatch메서드를 살펴보면
핸들러 조회
핸들러를 처리할 수 있는 어댑터 조회
핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView반환
이렇게 되면, 현재 ModelandView에 있는 mv에는 username:kim과 age:20 그리고 상대경로, save-form이 들어있으므로, viewResolver를 호출해서 절대 경로로 만들어야한다.
여기서 render메서드가 호출되고,
그다음에 render메서드를 가보면,
앞에서 v34버전에서 했던것처럼 get viewname을 가지고 resovlveViewName을 통해서 View객체를 반환받는다.
마지막으로 render메서드를 부를때 우리도 mv.getModel을 통해서 username:kim, age:20을 넣어준것처럼
view.render의 인자에 mv.getModelInternal메서드로 username:kim, age:20을 넣어줘서 render메서드를 호출했다.
그러면, 실제 MVC프레임워크에서 핸들러 매핑과 핸들러 어댑터를 어떻게 처리할까?
다시 간단히 remind 하자면,
핸들러 매핑은, 현재 요청된 URL에 해당하는 핸들러를 찾는것이다.
예를 들어 이전에 memberFormServlet같은것이다.
핸들러 어댑터는 이 memberFormServlet을 처리할 수 있는, ControllerV3HandlerAdapter 같은 것이다.
참고로, Controller인터페이스는 @Controller애노테이션과는 전혀 다르다.
이게 이전에 했던 memberFormServlet과 동일하다 보면된다.
우선 컴포넌트에 url패턴을 넣어서 이 url 패턴으로 요청이오면, OldController 가 handleRequest메서드를 통해서 ModelAndView를 반환한다.
그런데 이 핸들러 조회를 통해서 핸들러를 가져왔는데, 우리는 이 핸들러를 바로 실행하는게 아니라, 핸들러 어댑터가 이 핸들러의 handle메서드를 실행하였다.
고로, 우리는 두가지가 필요한데
1. HandlerMapping
핸들러 매핑에서 이 컨트롤러를 찾아야한다. 여기서는 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
2. HandlerAdapter
핸들러 매핑을 통해서 찾은 핸들러를 실행 할 수 있는 핸들러 어댑터가 필요하다.
Controller인터페이스를 실행 할 수 있는 핸들러 어댑터를 찾고 실행해야한다.
스프링 부트가 자동으로 등록하는 핸들러 매핑과 핸들러 어댑터
Handler맵핑 부터 보자, 우리는 요청된 URL을 보고, 핸들러를 찾아야한다.
제일 높은 우선순위는 애노테이션 기반 컨트롤러인 @RequestMapping이다. 그런데 우리는 이런걸 만든적이 없다.
그러면, 넘어간다.
그다음에 스프링 빈의 이름으로 핸들러를 찾는데, 우리가 이전에 URL이름으로 Component애노테이션으로 빈으로 등록해두었다.
그러므로 일단 OldController 핸들러를 반환해준다.
그다음에 핸들러 어댑터를 살펴보면, 애노테이션, HttpRequestHandler를지나서 Controller인터페이스에서 handlerAdaptor를 찾게된다.
실제 SimpleControllerHandlerAdaptor를 살펴보면
우리 이전에 Supports메서드가 기억나는가?
여기서도 instanceof를 통해서 Controller의 구현체가 맞는지 확인한다 Oldcontroller는 Controller구현체가 맞으므로 통과하나다.
그다음에 여기서 SimpleControllerHandlerAdaptor가 handle메서드를 실행하면서 OldController를 인자로 넘겨주는데->handle(handler) 모식도에서
여기서 보면, OldController의 handleRequest를 호출한다.
OldController에서 handleRequest가 실제로 수행된다.
보너스로 다른 핸들러를 알아보자,
HttpRequesthandler
이것도 애노테이션방식이 아닌 빈으로 등록한다.
그래서 해당 url패턴으로 핸들러를 매핑한다.
핸들러 매핑으로 핸들러 조회를 할때,
애노테이션이 아니므로, 이름 그대로 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping이 실행하게 되고, 성공하면, MyHttpReuqestHandler를 반환한다.
그다음에 핸들러 어댑터를 조회할때,
HandlerAdaptor의 supports가 순서대로 호출한다.
HttpRequestHandlerAdaptor가 HttpRequesthandler인터페이스를 지원하므로 대상이 된다.
여기까지 하면서 핸들러와 핸들러 매핑에 대해서 살펴 보았다.
그런데 기본편과 입문편을 봤던 사람이라면, 우리는 애노테이션 방식으로 컨트롤러를 사용했는데 그럼 이건 어떻게 처리하냐?
바로, RequestMappingHandlerMapping과 RequestMappingHandlerAdaptor이다.
이 방식도 뒤에서 알아볼 것이다.
이제는 View를 조회할 수 있도록 해보자
핸들러 에서 ModelAndView를 반환하도록 해보자
실제로 해보면, Whitelabel Error Page가 나오고, 콘솔에 OldController.handleRequest가 출력된다.
그러니까, 컨트롤러는 정상 호출이 되고, ModelAndView도 반환이 되었는데 Whitelabel Error가 발생하는것이다.
application.properties
prefix와 suffix를 추가하자.
여기서 알 수 있을 것이다. 우리는 이전에 Myview 객체를 생성하면서, viewresolver를 호출하면서 상대경로를 절대경로로 바꿔줬다.
그런데 우리는 전체 경로로 바꿔주는 과정이 없었기 때문에, new-form만 가지고는 jsp파일이 없기때문에 whitelable이 뜬 것이다.
스프링 부트는 InternalResouceViewResolver라는 뷰리졸버를 자동으로 등록하는데, 이때 application.properties에 등록한 spring.mvc.view.prefix,suffic 설정정보를 사용해서 등록한다.
과정을 다시 살펴보자.
일단 5번까지 ModelAndView반환까지 했다고 치자, 그런데 상대경로가 들어있고,우리는 뷰 리졸버를 이제 호출해야한다.
우리는 new-from이라는 빈 이름으로 뷰를 만든게 없으므로,
InternalResourceViewResolver가 호출된다.
여기서 호출된 뷰리졸버를 통해서 전체경로를 얻게 된다.
그다음에 internalResourceViewResolver가 View에 해당하는 internalResoucreView를 반환한다.
그리고 InternalResouceView에서
jsp처럼 forward를 호출해서 처리해서 JSP를 실행한다.
지금까지 앞에서부터 서블릿을 사용하고, 빈이름으로 핸들러를 사용하고 했지만, 대부분 컨트롤러는 @RequestMapping을 사용하여 애노테이션을 활용한다.
앞서 보았듯이 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping,RequestMappingHandlerAdaptor이다.
이제 본격적으로 애노테이션 기반의 컨트롤러를 사용해보자.
SpringMemberFormControllerV1
우선 컴토넌트 애노테이션을 통해서 스프링 빈으로 등록한다.
해당 url로 요청이 오면 모식도 처럼 핸들러 매핑을 해주는데 이 RequestMapping애노테이션을 보고, RequestMappingHandlerMapping이 이 핸들러를 맵핑한다.
그다음 RequestMappingHandlerAdaptor가 handle매서드를 호출할때 이 RequestMappingHandlerMapping을 아규먼트로 넣어서, 해당하는 컨트롤러의 process메서드가 실행된다.
참고 ★
이건 이론적인 이야기 이고, 스프링 부트 3.0이상부터는 클래스 레벨에 @RequestMapping이 있어도 컨트롤러로 인식하지 않는다. 그래서 @Controller만 붙여야한다.
SpringMemberSaveControllerV1
그러니까, @Controller내부에 @RequestMapping이 있으니까 @RequestMapping보고 RequestMappingHandlerMapping이 맵핑을 하고, 이 핸들러를 처리할 수 있는 RequestMappingHandlerAdaptor가 handle메서드를 호출할 시에, 이 핸들러의 @RequestMapping url패턴을 보고 해당하는 process메서드를 호출하는 형태이다.
SpringMemberListControllerV1
@RequestMapping을 살펴보면, 메서드 단위에 적용이 된것을 볼수 있다. 그러므로 이 메서드들을 하나의 컨트롤러에 넣을 수 있다.
훨신 깔끔해진 것을 볼 수 있다.
이제는 알 수 있을것이다? 왜? 클래스위에 RequestMapping이 있는거지? 그리고 왜? ReqeustMapping애노테이션 이 메서드 위에 붙어있지?
결국 Controller애노테이션의 component에 의해서 소문자하나 바뀐걸로 스프링 빈에 등록이 되고, 해당 url로 요청이오면 RequestMappingHandlerMapping이 @Controller 어노테이션을 스캔하여, 그 정보를 바탕으로 요청 URL과 처리할 메서드 간의 매핑 정보를 생성한다.
이 과정에서 클래스 레벨의 @RequestMapping에 정의된 경로와 메서드 레벨의 @RequestMapping에 정의된 경로를 합쳐 최종적으로 요청을 처리할 메서드의 경로를 결정하게된다.
그런데 여기서 문제가 V3같은 경우에는 ModelAndView를 개발자가 직접 생성해서 반환했기 떄문에 불편했다.
그래서 우리는 V4로 실용적으로 개선하였다.
지금부터 개선을 해보자.
우선 반환형이 String으로 전부 바뀌었다.
이제는 ModelAndView로 반환하는것이 아닌 앞단에서 필요한 정보들은 Model에다가 addAttribute로 담아서 준다.
또한 이제는 HttpServletRequest 뿐만아니라, @RequestParam이나, Model등의 파라미터를 받아서 메서드를 실행한다.
그러면 궁금한게있다.
순서대로 가보자면, 일단 URL로 요청이 들어오고, @Controller애노테이션을 가지고 RequestMappingHandlerMapping에서 딱 맵핑을 한다음에, ReqeustMappingHandlerAdaptor가 딱 맵핑을 한다음에
그뒤가 문젠데, hadle메서드를 호출하는것이다.
우리 앞에서 OldController부분을 다시 가보자
핸들러 어댑터의 코드에서 보면 httpservlet request, response, 핸들러를 파라미터로 받는다.
그러면 Ok 그럼 handleRequest호출하는 구현체인 OldController에서 request,response가 필요하니까 파라미터로 넣어서 호출해줬구나.
즉, 핸들러에서 process에 해당하는 메서드(newForm,save,members)에서 필요한 파라미터들을, 핸들러 어댑터가 handle메서드에서 파라미터로 동일하게 받아와서 process메서드(newForm,save,members)를 호출할때 넣어주는구나 라고 알 수 있다.
그러나, 우리가 실행해야할 process메서드는 결국 newform,save,members 3개이다.
심지어 그러면, 핸들러 어댑터에서 newform,save,members를 호출할 수 있게 만들었다 쳐도, 어떤거는 파라미터가 없고, 어떤거는 @RequestParam이 있고 또 어떤건 Model을 쓰고, 도대체 RqeustMappingHandlerAdaptor에서 어떤식으로 핸들러를 호출할지 모를것이다.
이건 다음장에서 설명할 것이다.
어쨋든 기나긴 이번 포스트 내용을정리를 해보자면,
굳이 컨트롤러라고 하지 않고 핸들러라고 한 이유를 알 수 있을 것이다.
핸들러에는 webServlet방식이 있을 수도, Controller인터페이스 방식, HttpRequesthandler방식, Controller방식 등등 여러가지 방식이 있다.
순서를 매겨보면
1. URL패턴으로 요청이 들어온다.
2. URL 패턴과 동일한 핸들러 맵핑을 한다.