섹션4 중) V5의 주요 개념과 실행 흐름 따라가기

개발새발log·2022년 5월 12일
0

스프링 MVC 1편

목록 보기
3/3

✅ 김영한 님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술을 공부하며 정리한 글입니다.

앞서 V1 ~ V4로 리팩토링 하면서 사용하는 함수의 파라메터나 반환 타입의 차이 등이 발생했다. 예를 들어:

  • V3의 ControllerV3 인터페이스
public interface ControllerV3 {

    ModelView process(Map<String, String> paramMap);
}
  • V4의 ControllerV4 인터페이스
public interface ControllerV4 {

    //뷰 이름만 넘김
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

그런데 만약 V4에서 V3로 바꾸고 싶다면??
이런 경우 스펙에 맞춰 갈아 끼울 수 있게 도와주는 어댑터 패턴이 등장한다.

V5 아키텍처

  • 핸들러 어댑터는 다양한 종류의 컨트롤러를 호출할 수 있도록 지원한다.
  • 핸들러는 컨트롤러의 구현체에 해당한다. 코드를 보면 더 이해가 쉽다.

V5의 클래스 다이어그램 소개

이해를 돕기 위해 코드를 바탕으로 클래스 다이어그램을 그려보았다.

위 아키텍처와 클래스 다이어그램을 참고하여 코드 구성을 뜯어보자.

FrontControllerServletV5 코드 뜯어보기

package hello.servlet.web.frontcontroller.v5;

@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

    //모든 컨트롤러를 다 지원하기 위해 Object 형
    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());

        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 myAdapter = getHandlerAdapter(handler);

        ModelView mv = myAdapter.handle(request, response, handler);

        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);

        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를 찾을 수 없습니다");
    }

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

}

편의상 import 문은 다 생략했다.

우선 FrontControllerServletV5의 프로퍼트와 메소드를 정리하면 다음과 같다:

  • handlerMappingMap: Map<String, Object>
  • handlerAdapters: List<MyHandlerAdapter>

handlerMappingMap에는 다음과 같은 url과 이에 매핑된 컨트롤러가 들어있다.
"/front-controller/v5/v3/members/new-form": MemberFormControllerV3의 객체
"/front-controller/v5/v3/members/save": MemberSaveControllerV3의 객체
"/front-controller/v5/v3/members": MemberListControllerV3의 객체
...

그리고 handlerAdapters에는 ControllerV3와 ControllerV4에 해당하는 핸들러 어댑터를 담고 있다. 이 어댑터 목록으로부터 특정 핸들러를 처리할 수 있는 어댑터를 조회하게 된다.

service 코드를 따라가 보자.

  1. 우선 handlerMappingMap 매핑 정보로부터 특정 핸들러를 꺼낸다.
    Object handler = getHandler(request);

그러면 handler에는 MemberFormControllerV3의 객체와 같은 특정 컨트롤러 구현체를 담게 된다.

상위 타입인 Object에 담겨서 다형성을 구현할 수 있다.

  1. 해당 핸들러의 타입에 맞는 어댑터를 찾는다.
    MyHandlerAdapter myAdapter = getHandlerAdapter(handler);

결국 myAdapter 변수는 핸들러에 맞는 어댑터 정보를 가지게 된다. 예를 들어 handler가 MemberFormControllerV3의 객체였다면, ControllerV3HandlerAdapter를 가지게 될 것이다.

  1. 그러면 어댑터의 handle을 호출하여, 특정 컨트롤러를 호출하게 된다.
    ModelView mv = myAdapter.handle(request, response, handler);

그러면 어떤 handle 메소드가 실행될까? 여기서 다형성의 활용을 엿볼 수 있다. 아마도 myAdapter의 구체 클래스인 ControllerV3HandlerAdapter의 handle이 호출될 것이다. 위 클래스 다이어그램을 참고하면 쉽다.

이 다음부터는 이전 V4까지의 흐름과 똑같다. ModelView를 반환하여 viewResolver를 호출하여 물리 이름으로 바꾼 뒤, 뷰 렌더링을 진행한다.

다만, 여기서 주목할 점은 ControllerV3와 ControllerV4의 파라메터와 반환 타입이 다르다는 것이다. (클래스 다이어그램 참고)

이러한 차이를 어댑터 패턴을 적용하여 어떻게 극복했는지 살펴보자.

public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
	public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        //...

        //어댑터 변환
        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        return mv;
    }

}

분명 handler 메소드는 ModelView를 반환한다. 그러나 기존 V4는 String 타입인 뷰 이름만 반환한다. V4의 스펙을 어댑터 스펙에 맞추는 변환 과정이 필요하다. ModelView를 반환하기 위해 해당 뷰 이름과 모델을 담은 ModelView를 만들어 반환하면 된다.

정리

이렇게 V3나 V4의 구현 방식을 선택하여 갈아끼울 수 있는 어댑터 패턴까지 적용해보았다. 이러한 설계가 어떤 면에서 좋은지 짚어보자면:

  • 역할과 구현이 분리가 되어 설계적으로 뛰어나다. 인터페이스를 적절히 활용하여 다형성과 어댑터 패턴을 잘 구현해낸 설계라고 할 수 있다.
  • 확장성이 좋다. 구현체를 변경하려면, 메인 코드를 거의 변경하지 않고 쓸 수 있다.
profile
⚠️ 주인장의 머릿속을 닮아 두서 없음 주의 ⚠️

0개의 댓글