본문 바로가기
Spring Tutorial

[스프링MVC 26] MVC 프레임워크 만들기: 유연한 컨트롤러

by 미소5 2023. 8. 2.

[스프링MVC 25] MVC 프레임워크 만들기: 단순하고 실용적인 컨트롤러 (tistory.com)

 

[스프링MVC 25] MVC 프레임워크 만들기: 단순하고 실용적인 컨트롤러

[스프링MVC 24] MVC 프레임워크 만들기: Model 추가 (*중요*) (tistory.com) [스프링MVC 24] MVC 프레임워크 만들기: Model 추가 (*중요*) [스프링MVC 23] MVC 프레임워크 만들기: View 분리 (tistory.com) [스프링MVC 23] MVC

joly156.tistory.com

[스프링MVC 24] MVC 프레임워크 만들기: Model 추가 (*중요*) (tistory.com)

 

[스프링MVC 24] MVC 프레임워크 만들기: Model 추가 (*중요*)

[스프링MVC 23] MVC 프레임워크 만들기: View 분리 (tistory.com) [스프링MVC 23] MVC 프레임워크 만들기: View 분리 [스프링MVC 22] MVC 프레임워크 만들기: 프론트 컨트롤러 (FrontController) (tistory.com) [스프링MVC 22

joly156.tistory.com

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

근데, 어떤 개발자는 ControllerV3방식으로 개발하고 싶고, 어떤 개발자는 ControllerV4방식으로 개발하고 싶다면 어떻게 해야할까?

 

public interface ControllerV3 {
 ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
 String process(Map<String, String> paramMap, Map<String, Object> model);
}

 

ControllerV3 , ControllerV4완전히 다른 인터페이스이므로, 호환이 불가능하다. (v3는 110v이고, v4는 220v인 전기 콘센트 같은 것) 이럴 때 사용하는 것이 바로 어댑터이다.

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

 

 


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

구조

  • 핸들러 어댑터: 중간에서 어댑터 역할을 해주는 덕분에, 다양한 종류의 컨트롤러를 호출할 수 있다.
  • 핸들러: 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다.

 


  • MyHandlerAdapter
    • 어댑터는 이렇게 구현해야 한다는 어댑터용 인터페이스
public interface MyHandlerAdapter {

    boolean supports(Object handler); //해당 컨트롤러(handler)를 처리할 수 있는지 판단

    ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws ServletException, IOException;
    //실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환
    // 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 대신 ModelView를 직접 생성해서라도 반환해야 한다.

}

이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만, 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다.

 


먼저 ControllerV3를 지원하는 어댑터를 구현해보자.

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3); //ControllerV3만을 처리할 수 있는 어댑터
    }

    @Override
    public ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws ServletException, IOException {
        ControllerV3 controller = (ControllerV3) handler; //handler를 ControllerV3로 캐스팅

        Map<String, String> paramMap = createParamMap(req); //V3 형식에 맞도록 호출
        ModelView mv = controller.process(paramMap);
        return mv;
    }

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

}

 

  • 해당 컨트롤러(handler)를 처리할 수 있는지 판단
public boolean supports(Object handler) {
 return (handler instanceof ControllerV3);
}
  • ControllerV3만을 처리할수 있는 어댑터이다. handler가 ControllerV3인 경우에만 true를 반환한다.
  • supports()를 통해 ControllerV3만 지원하기 때문에, 타입 변환은 걱정없이, handler를 ControllerV3로 캐스팅

 


ControllerV4를 지원하는 어댑터도 구현해보자.

public class ControllerV4HandlerAdapter implements MyHandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4); //handler가 ControllerV4인 경우에만 처리하는 어댑터
    }

    @Override
    public ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws ServletException, IOException {
        ControllerV4 controller = (ControllerV4) handler;   //1. handler를 ControllerV4로 캐스팅

        //2. paramMap과 model을 만들어서 해당 컨트롤러를 호출
        Map<String, String> paramMap = createParamMap(req);
        Map<String, Object> model = new HashMap<>();

        String viewName = controller.process(paramMap, model);  //3. viewName을 반환 받는다

        /**뷰의 이름을 ModelView로 만들어서 형식을 맞추어 반환한다.*/
        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        return mv;
    }

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

}
  • 어댑터 변환 (*중요*)
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
  • 어댑터가 호출하는 ControllerV4는 뷰의 이름을 반환한다. 그런데 어댑터뷰의 이름이 아니라 ModelView를 만들어서 반환해야 한다. → ControllerV4는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식(타입)을 맞추어 반환한다. (*어댑터가 꼭 필요한 이유*) 
    • 어댑터를 사용해서, 110v 전기 콘센트를 220v 전기 콘센트로 변경하듯이!
public interface ControllerV4 {
 String process(Map<String, String> paramMap, Map<String, Object> model);
}

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

 


  • 컨트롤러(Controller) → 핸들러(Handler) 
    • 이전에는 컨트롤러를 직접 매핑해서 사용했다. 그런데 이제는 어댑터를 사용하기 때문에, 컨트롤러 뿐만 아니라 어떤 것이라도 어댑터가 지원하기만 하면(해당하는 종류의 어댑터만 있으면),  URL에 매핑해서 사용할 수 있다. 

 

  • 매핑 정보의 값이 ControllerV3 , ControllerV4 같은 인터페이스에서 아무 값이나 받을 수 있는 Object 로 변경되었다.
private final Map<String, Object> handlerMappingMap = new HashMap<>();

 

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

    private final Map<String, Object> handlerMappingMap = new HashMap<>(); //매핑 정보 Object
    private final List<MyHandlerAdapter> handlerAdapters=new ArrayList<>();

    public FrontControllerServletV5(){
        initHandlerMappingMap();    //핸들러 매핑을 초기화(등록)
        initHandlerAdapters();      //어댑터를 초기화(등록)
    }

    private void initHandlerMappingMap() {  //핸들러 매핑정보( handlerMappingMap )에 컨트롤러를 추가
        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 req, HttpServletResponse resp) throws ServletException, IOException {

        Object handler=getHandler(req); /**1. 핸들러 찾기*/
        if (handler == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyHandlerAdapter adapter=getHandlerAdapter(handler);  /**2. 핸들러를 처리할 수 있는 핸들러어댑터 찾기*/

        ModelView mv = adapter.handle(req, resp, handler);    /**3. 실제 어댑터 호출*/
        //4. 어댑터는 handler(컨트롤러)를 호출하고, 그 결과를 어댑터에 맞추어(ModelView) 반환

        MyView view= viewResolver(mv.getViewName());
        view.render(mv.getModel(), req, resp);
    }


    private Object getHandler(HttpServletRequest req) {
        String requestURI = req.getRequestURI();
        return handlerMappingMap.get(requestURI);
        //핸들러 매핑정보(handlerMappingMap)에서, URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {  //핸들러를 처리할 수 있는 어댑터가 있는지
                return adapter;
                //핸들러가 ControllerV3인터페이스를 구현했다면 (supports()가 true이면),
                // ControllerV3HandlerAdapter 객체가 반환된다
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없어요! handler=" + handler);
    }

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

}
  1. 핸들러 매핑정보(handlerMappingMap)에서, 핸들러 조회
  2. 핸들러어댑터 목록(handlerAdapters)에서, 핸들러(해당 컨트롤러)를 처리할 수 있는 핸들러어댑터 조회 
  3. (2에서 찾은) 실제 핸들러어댑터가 호출된다.
  4. 핸들러어댑터핸들러(컨트롤러)를 호출하고 
  5. 그 결과를 어댑터에 맞추어(ModelView) 반환
  6. viewResolver 호출하면
  7. viewResolver가 MyView 객체를 반환
  8. render(model) 호출

 


여기에 애노테이션을 사용해서 컨트롤러를 더 편리하게 발전시킬 수도 있다. 만약 애노테이션을 사용해서 컨트롤러를 편리하게 사용할 수 있게 하려면 어떻게 해야할까? 바로 애노테이션을 지원하는 어댑터를 추가하면 된다!

다형성과 어댑터 덕분에 기존 구조를 유지하면서, 프레임워크의 기능을 확장할 수 있다.

 

스프링 MVC의 핵심 구조를 파악하는데 필요한 부분은 모두 만들어보았다! 지금까지 작성한 코드는 스프링 MVC 프레임워크의 핵심 코드의 축약 버전이고, 스프링 MVC와 거의 같은 구조이다.

 

[스프링MVC 27] 스프링 MVC 구조 (tistory.com)

 

[스프링MVC 27] 스프링 MVC 구조

[스프링MVC 26] MVC 프레임워크 만들기: 유연한 컨트롤러 (tistory.com) [스프링MVC 26] MVC 프레임워크 만들기: 유연한 컨트롤러 [스프링MVC 25] MVC 프레임워크 만들기: 단순하고 실용적인 컨트롤러 (tistory.c

joly156.tistory.com

 



[스프링MVC 22] MVC 프레임워크 만들기: 프론트 컨트롤러 (FrontController) (tistory.com)

 

[스프링MVC 22] MVC 프레임워크 만들기: 프론트 컨트롤러 (FrontController)

FrontController 패턴 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출 입구를 하나로! 공통 처리 가능 프론트 컨트롤러를 제외한

joly156.tistory.com

 

728x90
반응형