본문 바로가기
Spring/스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

MVC 프레임워크 만들기 3 - 유연한 컨트롤러

by hk27 2022. 1. 24.
여러 종류의 컨트롤러를 사용하고 싶다면 어떻게 할까요?

 

안녕하세요!

오늘은 유연한 컨트롤러를 도입한 MVC 프레임워크를 만들어 보겠습니다.

지금까지 MVC 프레임워크를 두 단계로 발전시켜왔습니다.

첫 번째로 프론트 컨트롤러 패턴을 적용하였고, 두 번째로 모델을 추가하고 실용적인 컨트롤러를 만들었습니다.

관심 있으신 분은 아래 게시글을 참고해주세요. 

https://passionate.tistory.com/40

 

MVC 프레임워크 만들기 1 - 프론트 컨트롤러

안녕하세요! 오늘은 MVC 패턴을 따르는 프레임워크를 만들어보겠습니다. 지난 시간에 MVC 패턴을 적용해서 회원 관리 웹을 만들어보았습니다. 관심 있으신 분은 참고해주세요! https://passionate.tistor

passionate.tistory.com

https://passionate.tistory.com/41

 

MVC 프레임워크 만들기 2 - 모델 추가, 실용적인 컨트롤러

안녕하세요! 오늘은 지난 게시글에 이어서 MVC 프레임워크를 발전 시켜 보겠습니다. 지난 게시글에는 프론트 컨트롤러 패턴을 도입해서 코드 중복을 제거하였습니다. 관심 있는 분들은 아래 게

passionate.tistory.com

 

지난 게시글의 마지막 부분에서 만약 어떤 개발자는 ControllerV3 방식으로 개발하고 싶고, 어떤 개발자는 ControllerV4 방식으로 개발하고 싶다면 어떻게 해야 할지 고민해보았습니다.

지금까지 개발한 프론트 컨트롤러는 한 가지 방식의 컨트롤러 인터페이스만 사용할 수 있습니다.

ControllerV3와 ControllverV4는 완전히 다른 인터페이스라서, 호환이 불가능합니다.

마치 V3는 110v, V4는 220v 전기 콘센트 같은 것입니다.

이럴 때 사용하는 것이 바로 어댑터입니다. 

오늘은 어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경해봅시다. 

 

V5 - 어댑터 추가

V5의 구조를 봅시다.

중간에 어댑터 역할을 하는 핸들러 어댑터가 추가되었습니다.

프론트 컨트롤러는 적절한 핸들러 어댑터를 찾아서 핸들러 어댑터를 호출하고, 핸들러 어댑터가 핸들러(컨트롤러)를 호출합니다. 기존에 프론트 컨트롤러가 직접 컨트롤러를 호출했던 방식과는 다릅니다.

핸들러 어댑터는 ModelView를 반환합니다. 만약 핸들러(컨트롤러)가 ModelView를 반환하지 않으면 핸들러 어댑터가 ModelView를 만들어서 반환해야 합니다.

 

핸들러는 컨트롤러보다 넓은 의미의 단어입니다.

앞에서는 컨트롤러만 요청을 처리할 수 있었지만, 이제는 핸들러 어댑터만 있으면 어떠한 것이든 요청을 처리할 수 있습니다. 앞으로 컨트롤러와 핸들러라는 단어를 혼용해서 사용하겠습니다. 

 

핸들러 어댑터 인터페이스

먼저 핸들러 어댑터 인터페이스 코드를 봅시다.

public interface MyHandlerAdapter {
    boolean supports(Object handler);
    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler);
}

supports와 handle 메소드가 있습니다. supports 메소드는 인자로 받은 object를 처리할 수 있는지를 boolean으로 반환합니다. handle 메소드는 handler를 작동시켜서 ModelView를 반환합니다. 

 

ControllerV3 핸들러 어댑터

ControllerV3를 지원하는 어댑터 코드입니다.

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) {
        ControllerV3 controller = (ControllerV3) handler;
        Map<String, String> paramMap = createParamMap(request);
        return controller.process(paramMap);
    }

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

supports 메소드에서 인자로 전달된 handler가 ControllerV3의 객체인지 확인합니다.

handle 메소드에서는 paramMap을 만들어서 컨트롤러를 호출합니다. 원래 프론트 컨트롤러가 이 부분을 담당하였는데, 이제는 핸들러 어댑터에서 담당합니다.

 

9번째 줄에서 ControllerV3 controller = (ControllerV3) handler; 로 다운 캐스팅 하고 있습니다.

V3 핸들러 어댑터에 들어오는 handler는 모두 ControllerV3 구현 클래스의 객체로 만들어졌기 때문에 다운 캐스팅 할 수 있습니다. 

 

ControllerV4 핸들러 어댑터

ControllerV4를 지원하는 어댑터 코드입니다.

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) {
        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> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName->paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

ControllerV4는 ModelView가 아니라 viewPath 스트링만 리턴합니다.

따라서 어댑터에서 ModelView를 만들어서 반환합니다. 

 

프론트 컨트롤러

프론트 컨트롤러는 어댑터를 사용해서 여러 버전의 컨트롤러를 사용할 수 있습니다.

@WebServlet(name="frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
    private final Map<String, Object> handlerMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
    public FrontControllerServletV5(){
        initHandlerMap();
        initHandlerAdapters();
    }
    private void initHandlerMap(){
        handlerMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
        handlerMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
        handlerMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
        handlerMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV3());
        handlerMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV3());
        handlerMap.put("/front-controller/v5/v4/members", new MemberListControllerV3());
    }
    private void initHandlerAdapters(){
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        Object handler = handlerMap.get(requestURI);
        MyHandlerAdapter adapter = getHandlerAdapter(handler);

        ModelView mv = adapter.handle(request, response, handler);
        MyView view = viewResolver(mv.getViewName());
        view.render(mv.getModel(),request, response);
    }
    private MyHandlerAdapter getHandlerAdapter(Object handler){
        for (MyHandlerAdapter handlerAdapter : handlerAdapters) {
            if(handlerAdapter.supports(handler)){
                return handlerAdapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. ");
    }

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

 

프론트 컨트롤러는 요청이 들어오면 적합한 핸들러를 찾고, 핸들러를 처리할 수 있는 핸들러 어댑터를 찾습니다.

핸들러 어댑터의 handle 함수를 호출하여 컨트롤러 호출을 맡기고, ModelView 객체를 리턴받습니다.

ModelView 객체의 정보로 MyView 객체를 만들고 뷰 로직을 호출합니다. 

 

V5에서는 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계하였습니다.

저희가 만든 V5 프레임워크의 구조는 사실 Spring MVC의 구조와 비슷합니다. 

다음 게시글에서는 Spring MVC 구조를 자세히 알아보겠습니다.

 

인프런  '스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 강의를 듣고 공부하며 정리한 자료입니다. 

잘못된 부분은 피드백 주시면 감사하겠습니다. 

글 읽어주셔서 감사합니다 :-)

 

참고 자료

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술, 섹션 4. MVC 프레임워크 만들기 https://www.inflearn.com/course/스프링-mvc-1

 

 

댓글