스프링 MVC - 어댑터 패턴 - V5 그리고 스프링 MVC를 직접 까보자

최지환·2022년 4월 10일
0

스프링

목록 보기
5/12
post-thumbnail

스프링 MVC - 어댑터 패턴 - V5

이제 거의 다 왔다. 이번 글에서는 내용이 좀 길어질 예정..

기존 v4는 아직까지 단점이있다. 바로 유연성이 없다는 것이다.

발젼시켜 온 v1,v2,v3,v4 모두 작동은 한다. 만약 개발자가 어떤 때는 v1을 이용하고, 어떤 때는 v4를 이용해 개발을 하고 싶다면 어떻게 해야할까? 현재 까지는 각 버전들중 원하는 인터페이스만 실행시킬 수가 없다.

이런 문제를 해결 하려면 어떻게 해야할까?

이전에 어탭터 패턴에 대해 알아보자

Adapter pattern (어댑터 패턴)

위키 백과에서 어댑터 패턴을 검색해보자

어댑터 패턴은 클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 패턴이다.

→ 호환성이 없는 인터페이스의 클래스들을 호환성이 있도록 도와주는 것.

즉 위에서 예시로 들었던 V1, V2, V3, V4 처럼 서로 규격이 다른 인터페이스들을 각 상황에 따라 알맞는 버전을 쓸 수 있게 해주는 것이다.

그렇다면 리팩토링 해보자!


예상 구조

V5 구조

이전 버전 v4의 구조와 다르게 핸들러 매핑 정봐와 핸들러 어댑터 목록이라는 기능이 추가 됨을 볼 수 있다. 이를 코드로 확인 해보자.

앞으로 리팩터링 하는 코드V5는 V3과 V4를 사용 할 수있도록 리택토링 할 것이다. 또한 단순 텍스트로는 코드를 이해하기 어려울 것이니 깃 주소를 첨부해 두겠다!
깃 허브 바로가기


구조의 순서를 확인 해보자

  1. HTTP 요청이들어왔을 때, 이를 처리 할 수 있는 url 매핑이 있나 조회를 한다.

  2. 조회 후, 어댑터 목록에서 요청된 url에 해당하는 로직을 처리 할 수 있는 어댑터가 있는지 어댑터 목록 에서 조회한다.

  3. 조회한 어댑터의 handler를 호출한다.

  4. 컨트롤러 실행

  5. ModelView를 반환한다.

  6. viewResover를 통해 반환된 정보(모델 , 상대경로)를 이용해 View 생성한다

  7. Front Controller는 render을 호출


우선 V3를 처리할 수 있는 어댑터를 만들어보자!!

MyHandlerAdapter 인터페이스 코드

public interface MyHandlerAdapter {
    boolean supports(Object handler);

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}

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) throws ServletException, IOException {
        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> paraMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paraName -> paraMap.put(paraName, request.getParameter(paraName)));
        return paraMap;
    }
}

ControllerV3HandlerAdapter는 ControllerV3에 지원이 가능한지 확인하는 supports 메서드와

request,response를 입력받아 ControllerV3 의 process를 호출 할 수 있도록 하는 handle메서드가 있다.

→ V3 컨트롤러는 파라미터로 부터 ParamMap을 입력받아야하는데, 이에 대한 처리 로직이 있다.
즉 ControllerV3HandlerAdapter는 request와 response를 입력 받아 handle을 실행하면 ControllerV3에 해당하는 구현체(MemberFormControllerV3, MemberListControllerV3, MemberSaveControllerV3)들을 모두 실행할 수 있도록 설계 되어있다.

FrontControllerServletV5 소스 코드 - V3만 처리 가능

@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);  
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);
        view.render(mv.getModel(), request, response);
    }

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

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("핸들러 어댑터를 찾을 수 없습니다. handler = " + handler);
    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }
}

front controller의 생성자를 살펴보자 우선 핸들러 매핑에 대한 정보와 핸들러 어댑터에 대한 정보를 입력해준다.

따라서 이후 service 를 실행할 때 front Controller에서 핸들러 매핑 조회와 핸들러 어댑터 목록을 조회할 수 있다. 조회 후 해당 어댑터를 이용하여 로직을 처리하고, 뷰를 반환 할 수 있다.


하지만 아직까지는 V3만 처리할 수 있기 때문에 왜 이렇게 해주는지 이해가 잘 되지 않는다. 이제 V4 어댑터를 추가 해보자

ControllerV4HandlerAdapter

public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        ControllerV4 controller = (ControllerV4) handler;
        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();

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

        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        return mv;
    }

    private Map<String, String> createParamMap(HttpServletRequest request) {
        Map<String, String> paraMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paraName -> paraMap.put(paraName, request.getParameter(paraName)));
        return paraMap;
    }
}

이때 handle의 로직을 살펴보자.

이전에 ControllerV3HandlerAdapter와는 다른 것을 볼 수 있다.

V4는 V3와는 다르게 paramMap뿐만아니라 model을 컨트롤러에게 넘겨준다.

따라서 ControllerV3HandlerAdapter에서는 request와 Response를 입력받아 V3와 다른 로직을 처리하고 이에 대한 정보(model과 뷰 경로)를 담은 ModelView 를 반환한다.

FrontControllerServletV5 - V4 추가

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

        //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());
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }

    @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);
        //paraMap 넘겨줘야함
        String viewName = mv.getViewName();//논리 이름만 알고 있는 상태
        MyView view = viewResolver(viewName);
        view.render(mv.getModel(), request, response);
    }

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

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("핸들러 어댑터를 찾을 수 없습니다. handler = " + handler);
    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }
}

이제 V4에 대한 정보를 frontController가 알고 있다.

따라서 이후 service 를 실행할 때 front Controller에서 V4에 대한 핸들러 매핑 조회와 핸들러 어댑터 목록을 조회할 수 있다. 조회 후 해당 어댑터를 이용하여 로직을 처리하고, 뷰를 반환 할 수 있다.


로직 순서 예

⇒ 조금 복잡하지만 예를 들어 보겠다. 만약 회원 데이터를 저장하는 save로직을 실행한다고 가정해보자.

  1. 우리는 /front-controller/v5/v3/members/save 에 해당하는 HTTP요청을 받았다.
  2. 프론트 컨트롤러에서 해당 요청이 처리가능한지 핸들러 매핑 정보를 뒤진다. → 이때 `new MemberSaveControllerV3()` (핸들러)가 반환된다.
  3. 이 핸들러 정보(MemberSaveControllerV3())를 처리 해줄 수 있는 어댑터를 찾아야한다. → 핸들러 어댑터 목록에서 찾는다. (front controller에서 getHandlerAdapter 호출 하여 처리 가능한지 확인)
  4. 찾은 핸들러 어댑터의 handle 메소드를 실행시켜 , 해당 컨트롤러를 실행하기 위한 준비(MemberSaveControllerV3를 실행하기위해 paramMap 생성)를 하고 process메서드 호출 후 ModelView 반환
  5. front Controller에서 MyView를 생성하고, HTML 응답

정리

어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계할 수 있었다.

V3, V4어댑터와 마찬가지로, V2 V1에 해당하는 어댑태를 만들어준다면 좀 더 확장성이 넓어지고 유연해진다.

다시한번 순서를 따라가 보자.

  1. HTTP 요청을 받으면 Front Controller는 핸들러 매핑 정보를 기반으로 핸들러를 조회한다.

  2. 조회된 해당 핸들러를 처리할 수 있는 어댑터를 핸들러 어댑터 목록에서 찾는다.

  3. 찾은 핸들러 어댑터를 실행하여, 해당 컨트롤러 실행

  4. ModelView를 반환한다.

  5. FrontControlle에서 viewResover를 통해 반환된 정보(모델 , 상대경로)를 이용해 View 생성한다

  6. Front Controller는 render을 호출

이 순서는 실제 스프링 MVC 프레임 워크와 유사하게 작동한다.


그렇다면 스프링 MVC 프레임 워크와 어떻게 유사한지 확인해보자.

우선 스프링 MVC 프레임 워크의 순서도를 보자

정말 유사해 보인다.

우리가 만들어 두었던 FrontController의 역할을 유사하게 갖고 있는 것이 Dispatcher Servlet이다.

이에 대해 확인해보자!

인텔리 제이에서 커맨드 + O 를 누르고 Dispatcher Servlet라고 검색해보자.

이후 DispatcherServlet의 의존관계를 확인해보기 위해 인텔리제이의 다이어그램 기능을 사용하자

다음과 같은 방식으로 확인 할 수 있다.

DispatcherServlet의 다이어그램

다음 관계도를 확인해보면 DispatcherServlet은 FrameworkServlet과 HttpServletBean, HttpServlet 등을 상속 받는 관계인 것을 확인 할 수 있다.

이때 알아둬야 할 점은 DispatcherServlet은 이전의 우리가 만들어 뒀던 front Controller와 마찬가지로 HttpServlet을 상속받는다는 것을 알 수 있다.

참고로 알아둬야 할 점

  • 스프링 부트는 DispacherServlet을 서블릿으로 자동으로 등록 하면서 모든 경로에 대해 매핑한다.
    → 이때 기존에 등록한 서블릿도 함께 동작함.(자세한 경로가 우선 순위가 높기 때문에, 코드로 만들어둔 맵핑들이 먼저 동작함.)

간단 요청흐름

  • 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출
  • 스프링 MVC는 FrameworkServlet에 Service()를 오버라이드 해둠
  • FramworkServlet의 service()가 호출 되면서 DispacherServlet의 doDispatch() 호출

FrameworkServlet의 Service() 확인

FramworkServlet에 Service()가 존재함을 확인 할 수 있다. 정확하진 않으나, 요청된 Http 메소드가 Patch이거나 null인경우 processRequest를 통해 호출한다.

processRequest 소스 코드 확인

→ processRequest 호출시 doService()를 호출함을 확인 할 수 있다.

이때 doService()는 추상 메서드로 구현되어있고

DispatcherServlet에 Override되어 구현되어 있음을 확인할 수 있다.

만약 processRequest()가 호출 되지 않는다면 super.service를 호출한다.

그렇다면 이번엔 super.Service를 클릭하여 함수의 구현을 확인해보자.

다음을 보면 service()는 HttpServlet에 구현되어 있는 것으로 확인된다.

요청 흐름 정리

  1. DispatcherServlet이 호출되면 이의 부모인 FrameworkServlet에서 오버라이드 된 service() 실행

  2. 이때 메소드 요청이 patch나 Null인 경우 processRequest()를 호출한다. → 2-가
    그렇지 않는다면 → 2-나

2-가. FrameworkServlet.ProcessRequest() → 메서드 실행 중 doService() 실행

2-가.doService()는 추상 메서드기 때문에 DispatcherServlet에 오버라이드된 doService() 실행
2-가. doDispatch() 실행 ⇒ doDispatch()가 핵심!!

2-나. supper.service() 실행 → HttpServlet의 service() 실행

위의 흐름을 보았듯이 일반적으로 요청이 들어오면 doDispatch가 실행된다.

doDispatch()를 확인해보자.

doDispatch()

코드가 너무 길지만 간단하게 살펴보자

1043라인을 보면 mappedHandler = getHandler(processedRequest); 를 확인 할 수 있다.

요청을 처리할 수 있는 핸들러를 조회함을 확인 할 수 있다.

또한 1050 라인을 보면 조회한 핸들러를 처리할 수 있는 핸들러 어댑터를 조회하는 것을 확인 할 수 있다.

이후 라인 1067을 확인해보자. 이전에 우리가 조회한 핸들러 어댑터로 핸들러를 실행하고, ModelView를 반환함을 확인 할 수 있다.

또한 doDispatch()내부에서 호출된 processDispatchResult() 함수를 확인해보면

render() 함수를 통해 뷰 렌더링이 호출되고

render 메서드를 확인해보자.

라인 1377을 확인 하여 뷰 리졸버를 통해 뷰를 찾고, 뷰를 한봔함을 확인 할 수 있다.


과정을 요약해보자.

  1. doDispatch() 에서 핸들러를 조회한다.
  2. 이후 핸들러를 처리할 수 있는 핸들러 어댑터를 조회한다.
  3. 핸들러 어댑터를 실행함과 동시에 ModelView를 반환한다.
  4. 이후 doDispatch() 함수 내부에서 processDispatchResult() 메서드가 호출되고, processDispatchResult() 내부에서는 render()를 호출하여 뷰 렌더링을 실행한다.
  5. render() 내부에서는 뷰 리졸버를 통해 뷰를 찾고 뷰를 반환한다.

즉 스프링 MVCsms 기본 구조는 우리가 이전에 만들었던 V5와 같음을 알 수 있다.

위 내용들은 인프런 강의를 듣고 난 내용을 정리한 것 입니다! 제 주관적인 생각도 들어가 있으니, 읽으실 때 주의하세요!
강의 링크

0개의 댓글