Spring/MVC

스프링 MVC 밑바닥부터 만들어보기6 - 어댑터 패턴을 이용한 유용한 컨트롤러

ssung 2022. 4. 20. 22:09

V4까지 만든 프로젝트로 이제는 개발자가 편리하게 사용할 수 있는 형태로 완성되었다. 그런데 만약에 개발자가 다른 버전의 컨트롤러를 사용하고 싶다면 어떻게 해야할까?

 

지금까지 만든 것은 프론트 컨트롤러에서 특정 버전의 컨트롤러를 지정해서 사용하고있었다. 만약 V4를 사용하다가 V3를 사용하고 싶다면 지금의 상황에서는 변경이 불가능하다. 이 불가능을 해결하기 위해서 어댑터 패턴을 도입해보고자 한다.

 

만약 우리가 110V짜리에 220V를 사용할려고 하면 전압이 맞지 않기 때문에 사용 할 수 없을 것이다. 이것을 해결하기 위해 어댑터를 이용해서 전압을 맞춰서 사용해주어야 한다. 이것처럼 서로 다른 2개를 연결시키기 위한 방법을 어댑터 패턴이라고 부른다. 이번에는 이 어댑터를 도입시켜서 V3와 V4중 어떤것이 들어와도 처리할 수 있도록 유연한 컨트롤러를 만들어보도록 하겠다.

 

 

 

어댑터가 들어오면서 요청의 흐름이 변하였다.

추가된 항목이 2개가 있다.

  • 핸들러 어댑터 목록
  • 핸들러 어댑터

 

핸들러 어댑터 목록을 이용해서 V3로 할지 V4로할지를 정하고 해당 컨트롤러를 받아온다.

 

앞에까지는 프론트 컨트롤러에서 바로 컨트롤러를 호출했었지만 이제는 핸들어 어댑터를 거쳐서 호출한다. V3와 V4는 반환타입이 다르므로 이것을 그냥 받아 올 수는 없다. 이런 다른 부분들을 해결하고 처리하기 위해서 핸들러 어댑터를 통해 하나의 형식으로 통일한 뒤 프론트 컨트롤러로 다시 반환한다.

 

잘 보면 앞에 만들었던것과 용어가 조금 바뀐것도 알 수 있다. 앞에까지 컨트롤러라고 부른던것이 핸들러라고 명칭이 바뀌면서 매핑정보와 새로 추가된 부분들에서 전부 핸들러라는 용어를 사용하고있다. 이 점 유의하길 바란다.

 

이전이랑 코드가 많이 달라지고 구조도 더 복잡해졌기 때문에 천천히 하나씩 진행해보도록 하자.

 

 

 

 

 

ControllerV5

먼저 핸들러 어댑터를 만들어야 한다. 핸들러 어댑터에는 매핑된 핸들러가 사용가능한 어댑터인지를 체크해줄 메소드와 핸들러에 따라서 데이터를 변환해줄 메소드가 필요하다. 바로 코드로 살펴보자.

public interface MyHandleAdapter {

    boolean supports(Object handler);

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
  • supports는 핸들러를 파라미터로 받고있다. 프론트 컨트롤러에서 매핑된 핸들러가 사용할 수 있는 어댑터인지를 판단하는 작업이다.
  • handle은 핸들러와 직접 연결해서 필요한 데이터를 보내고 해당 핸들러의 반환값을 받아서 ModelView로 반환할 수 있도록 변환한 후 반환하는 메소드이다. 이 메소드가 바로 서로 다른 전압의 크기를 맞춰주는 작업을 하는 것이다.

 

이제 v3컨트롤러와 v4컨트롤러를 연결해줄 어댑터들을 만들어보자.

public class ControllerV3HandlerAdapter implements MyHandleAdapter {

    @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 = createParam(request);
        ModelView mv = controller.process(paramMap);

        return mv;
    }

    private Map<String, String> createParam(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));

        return paramMap;
    }
}

v3를 연결하는 어댑터이다.

  • supports에서 들어온 핸들러가 v3컨트롤러인지를 확인한다. 이 어댑터는 v3만 사용하는 어댑터이기 때문에 v3가 아니면 flase를 반환한다.
  • 앞에서 만든 v3컨트롤러는 HttpServletRequest가 아닌 프론트 컨트롤러가 만들어서 넘겨주는 파라미터 값을 사용했고 반환값으로 ModelView를 반환했었다. 어댑터에서도 똑같이 동작하도록 만들어준다.

 

혹시라도 코드가 이해가 안될수도 있는데 뒤에서 프론트 컨트롤러까지 모두 만들고 전체코드를 확인하면서 다시 떠올려보면 이해가 될 것이다.

public class ControllerV4HandlerAdapter implements MyHandleAdapter {

    @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 = createParam(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> createParam(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));

        return paramMap;
    }
}

v4를 연결하는 어댑터이다.

  • supports는 v3어댑터와 동일하게 동작한다.
  • v4컨트롤러에서는 파라미터 값과 함께 model도 넘겨받았었다. 그리고 반환값이 ModelView가 아닌 ViewName이기 때문에 어댑터에서 ModelView를 만들어서 반환해주어야 한다.

 

이런식으로 서로다른 파라미터와 반환값을 가진 v3와 v4컨트롤러가 중간에서 동작하는 어댑터 덕분에 최종적으로는 동일하게 ModelView를 반환하게 된다. 이제 프론트 컨트롤러는 ModeView에 대한 처리만 생각하면 된다.

 

 

 

이제 프론트 컨트롤러를 수정해보도록 하자. 핸들러 매핑을 위해서 v4의 URI를 추가하자.

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

기존에는 v3이거나 v4이거나 한가지의 종류만 선택해서 URI를 만들었지만 이제는 어떤 URI가 들어와도 받을 수 있어야 하기 때문에 v3와 v4의 URI를 모두 추가해준다.

 

그리고 URI에 따라서 v3또는 v4의 컨트롤러가 map에 들어가야한다. 기존에는 특정 컨트롤러를 지정해줬지만 지금은 유연성을 위해서 Object로 지정해주자.

private Map<String, Object> handlerMappingMap = new HashMap<>();

이렇게하면 핸들러 매핑은 해결되었고 이제는 URI에 따라서 어떤 핸들러 어댑터를 사용할지를 찾을 수 있어야 한다.

핸들러 어댑터를 저장할 List와 핸들러 어댑터의 종류를 저장하는 로직을 만들어보자.

 

 

private List<MyHandleAdapter> handleAdapters = new ArrayList<>();		// 핸들러 어댑터를 담을 List


handleAdapters.add(new ControllerV3HandlerAdapter());
handleAdapters.add(new ControllerV4HandlerAdapter());

여기서는 v3와 v4만 사용하기 때문에 v3와 v4의 어댑터를 저장해준다.

 

핸들러 매핑부분과 어댑터 부분을 초기화하는 코드를 메소드로 분리해서 작업시키자.

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

    private Map<String, Object> handlerMappingMap = new HashMap<>();
    private List<MyHandleAdapter> handleAdapters = new ArrayList<>();

    public FrontControllerServiceV5() {
        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() {
        handleAdapters.add(new ControllerV3HandlerAdapter());
        handleAdapters.add(new ControllerV4HandlerAdapter());
    }
    
    ....   
}

v3와 v4중 어떤 URI라도 처리가 가능하도록 준비가 되었다. 나머지 코드도 작성해보자.

 

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

	...

    @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;
        }

        MyHandleAdapter adapter = getHandlerAdapter(handler);

        ModelView mv = adapter.handle(request, response, handler);
        MyView view = viewResolver(mv);

        view.render(mv.getModel(), request, response);
    }

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

    private MyHandleAdapter getHandlerAdapter(Object handler) {
        for (MyHandleAdapter adapter : handleAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
    }

    private MyView viewResolver(ModelView mv) {
        return new MyView("/WEB-INF/views/" + mv.getViewName() + ".jsp");
    }
}
  • getHandler : 핸들러 매핑을 하는 작업이다. 요청URI를 받아서 필요한 핸들러를 가져온다.
  • getHandlerAdapter : 핸들러 어댑터를 가져오는 작업이다. getHandler를 통해서 매핑된 핸들러를 어댑터의 메소드인 supports를 이용해서 어떤 어댑터에 적절한지를 찾아내는 작업이다.

 

어댑터덕분에 모든 요청의 반환값은 ModelView로 받게 되었다. 프론트 컨트롤러에서는 ModelView를 받아서 처리하는 로직만 만들어주면 어떤 종류의 컨트롤러라도 처리할 수 있게 되었다.

 

 

 

이렇게해서 어떤 종류의 컨트롤러라도 받을 수 있는 유연한 컨트롤러를 만드는 작업이 완료됐다.

 

이때까지 만든 MVC의 구조는 현재 Spring의 구조와 매우 비슷하다. 여기서 조금더 확장성이 있고 복잡하게 구조되어있지만 결국 큰 틀은 지금의 모습과 유사하다. 현재 Spring에서 사람들이 가장 많이 사용하는 방식은 어노테이션 방식일 것이다. 어노테이션 방식의 컨트롤러도 위와 비슷하게 설계해서 구현할 수 있을 것이다.

 

 

마지막으로 진짜 스프링 MVC가 어떤 구조로 되어있는지 간단하게 살펴보도록 하자.

 

 

 

 

 

Spring MVC

용어는 조금씩 다른 부분이 있지만 우리가 마지막으로 만든 컨트롤러의 구조와 완전히 똑같은 것을 알 수 있다.

 

DispatcherServlet이 우리가 만든 프론트 컨트롤러의 역할을 하고있다. 실제로 스프링에서 DispatcherServlet파일을 들어가보면 FrameworkServlet을 상속받고 그 안에서는 HttpServletBean을 상속받는데 그 안에 들어가보면 HttpServlet을 상속받고있는 것을 알 수 있다.

 

그리고 우리가 프론트 컨트롤러에서 구현한 service의 역할을 DispatcherServlet에서는 doService에서 처리하고 있다.

내부에 코드가 많이 복잡하지만 잘 보면 핸들러 어댑터를 가져오는 부분이나 ModelView를 반환받는 부분이나 forward를 하는부분이 모두 동일한 것을 알 수 있다.

 

스프링MVC의 구조를 자세히 앎으로서 개발시에 문제가 생겼을 때 어디부분에서 문제가 생겼는지를 정확히 캐치할 수 있을 것이고, 필요한 부분을 확장하는 것도 가능할 것이다.

 

 

 

스프링에서 사용하는 핸들러 매핑, 핸들러 어댑터, viewResolver, view는 우리가 만든것들보다 훨씬 복잡하게 이루어져 있다. 모두 인터페이스로 만들어서 확장성을 넓혀놓았고 운용되고있는 구현체들도 상당히 많은 편이다. 이런 자세한 부분이 알고싶으신분들은 더 깊게 스프링에 대해서 공부해시기를 권장한다.

 

 

 

 

 

 

모든 내용은 인프런의 김영한님의 강의를 참고하여 작성되었습니다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

 

 

 

사용된 전체 코드는 깃허브에서 보실 수 있습니다.

https://github.com/ssung0810/spring_study/tree/main/spring_mvc_study

 

GitHub - ssung0810/spring_study

Contribute to ssung0810/spring_study development by creating an account on GitHub.

github.com