Back-end/Spring MVC 개념

10. Front Controller(2)

devraphy 2022. 3. 7. 21:03

0. 개요

- 이전 포스팅에서 전체적인 Front Controller의 흐름에 대해서 알아보았다.

- 그러나 하나의 디자인 패턴이라 정의하기에는 구조적으로 불편한 부분이 많다.

- 이 부분을 어떻게 개선하는지, 최종적으로 Front Controller가 어떻게 작동하는지 알아보자.

 

* 본 포스팅은 Front Controller 시리즈의 내용입니다. 이전 포스팅을 읽는 것을 권장합니다.


1. ModelView 대신 view 이름(String)을 반환

- 이전 포스팅에서 각 Controller는 ModelView 객체를 생성하고 반환했다. 그러나 이는 SRP에 위배된다.

- Controller가 비즈니스 로직의 수행과 ModelView의 생성까지 책임지고 있기 때문이다.

- 그러므로 ModelView를 더 이상 사용하지 않고, JSP 이름(String)을 그대로 반환하도록 한다.

 

- 그렇다면 기존에 ModelView의 Model 역할은 어떻게 처리되는 것일까?

 

- 단순하게 Front Controller에서 paraMap과 model을 생성하여 Controller에게 넘기는 방식을 사용한다.

   → paraMap: 요청으로부터 넘어온 URL parameter를 저장하기 위한 자료구조 

   → model: Controller에서 view에게 전달할 데이터 객체를 저장하기 위한 자료구조  

 

- Controller는 Front Controller로부터 넘어온 model을 사용하여 view에게 전달할 데이터 객체를 저장한다.

- Controller가 model에 저장한 내용은 그대로 담겨있게 된다. 

 

- 아래의 코드를 보면, 이해가 더욱 쉬울 것이다.

public interface Controller {

    /**
     *
     * @param paraMap
     * @param model
     * @return viewName
     */
    String process(Map<String, String> paramMap, Map<String, Object> model);
           
   }
@WebServlet(name = "frontController", urlPatterns = "/front-controller/*" )
public class FrontController extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String requestURI = request.getRequestURI();

        Controller controller = controllerMap.get(requestURI);
        if(controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();

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

        view.render(model, request, response);
    }
}

 

- ModelView를 제거한 구조는 다음과 같다. 


2. 유연하지 않은 Controller

- Front Controller를 제외한 각 Controller는 하나의 인터페이스로부터 파생된 구현체다.

- 그러므로 각 Controller는 동일한 구조를 가지며, 동일한 비즈니스 로직 메서드의 이름을 갖는다.

 

- 그러나 개발을 하다 보면 모든 Controller가 동일한 구조를 가질 수는 없다.

  ex) Controller에서 다른 형태의 값이 반환되는 경우

  

- 그러므로 필요에 따라 구조가 다른 Controller를 사용할 수 있어야 하며,

- Front Controller가 이를 처리하는데 문제가 없어야 한다.

- 이와 같은 이유로 유연한 Controller가 필요한 것이다. 

 

- 현재까지의 Front Controller의 구조는 유연하지 않은 Controller를 사용한다.

- 어떻게 해야 서로 다른 구조를 가진 Controller를 유연하게 사용할 수 있을까? 

 

a) Adapter 패턴

- Adapter는 이름 그대로 변환기의 역할을 한다.

- 마치 110V의 전기 콘센트에서 220V의 제품을 사용하기 위해 필요한 어뎁터처럼 말이다. 

- Adapter 패턴을 사용하면 Front Controller에서 다양한 구조의 Controller를 처리할 수 있다.

 

b) Adapter 패턴의 구조

- Front Controller에 Adapter 패턴을 적용하면 다음과 같은 구조를 형성한다. 

- 우선 각 Adapter의 역할에 대해서 알아보자. 

 

c) HandlerMapping

- 기존의 Front Controller에서 URL을 이용하여 각 Controller를 매핑하였다.

- 이와 동일한 역할을 하는 것이 HandlerMapping이다.

 

- HandlerMapping은 Client의 요청 URL과 매핑되어 있는 Controller를 선택한다.

- 여기서 선택된 Controller를 Handler라고 부른다. 

 

d) HandlerAdapter

- HandlerAdapter는 선택된 Handler의 비즈니스 로직을 실행하는 역할이다.

- 선택된 Handler(= Controller)에 따라서, 그 구조에 맞게 처리할 수 있어야 한다.

- 그러므로 HandlerAdapter는 Handler의 구조에 맞는 처리방법(= 비즈니스 로직 메서드)을 제공한다. 

- 즉, Handler가 보유한 비즈니스 로직 처리 메서드를 호출하는 역할이다. 

 


3. Adapter 패턴의 동작원리 

- Adapter를 사용하는 Front Controller가 어떻게 동작하는지 하나씩 알아보자.

 

- 아래 코드에서는 서로 다른 구조를 갖는 Controller A와 Controller B가 사용되었다.

- Controller A는 ModelView를 반환하는 구조를 가졌다. 

- Controller B는 View 이름을 그대로 반환하는 구조를 가졌다. 

 

- 아래의 코드를 읽기 전에 다음을 명심하도록 하자.

- Controller A는 ModelView 객체를 반환하고, Controller B는 view 이름(String)을 직접 반환한다.

- 서로 다른 구조를 일괄 처리할 수 있도록, HandlerAdapter는 공통적으로 ModelView를 사용하도록 설계하였다.  

 

- 아래의 전체 코드는 Adapter 패턴을 이용하여 Controller를 처리하는 과정을 보여준다.

- Client의 요청으로 Controller A의 구현체를 처리하는 과정이라고 생각하고 가볍게 훑어보자.  

@WebServlet(name = "frontController", urlPatterns = "/front-controller/*")
public class FrontController extends HttpServlet {

    // controller를 받아오는 Map인데, 현재는 모든 종류의 controller를 허용해야한다.
    // 그러므로 Map의 value를 Object 타입으로 만든다. (Object가 최상위 클래스이기 때문)
    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

    public FrontController() {

        initHandlerMappingMap(); // url에 따라 controller 객체를 생성하는 메소드
        initHandlerAdapter(); // 받아온 controller의 버전에 맞게 기능을 수행하는 객체를 생성하는 메소드
    }

    private void initHandlerMappingMap() {
        // 컨트롤러 A
        handlerMappingMap.put("/front-controller/a/members/new-form", new MemberFormControllerA());
        handlerMappingMap.put("/front-controller/a/members/save", new MemberSaveControllerA());
        handlerMappingMap.put("/front-controller/a/members", new MemberListControllerA());

        // 컨트롤러 B
        handlerMappingMap.put("/front-controller/b/members/new-form", new MemberFormControllerB());
        handlerMappingMap.put("/front-controller/b/members/save", new MemberSaveControllerB());
        handlerMappingMap.put("/front-controller/b/members", new MemberListControllerB());
    }

    private void initHandlerAdapter() {
        handlerAdapters.add(new HandlerAdapterControllerA());
        handlerAdapters.add(new HandlerAdapterControllerB());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 1.url에 따른 controller 객체를 받아온다. (handler == controller 객체)
        Object handler = getHandler(request);

        if(handler == null) { // 예외처리 - 매핑에 없는 url이 요청됐을 때
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 2. handler(controller 객체)의 버전에 맞는 기능을 수행하는 handlerAdapter를 반환한다.
        HandlerAdapter adapter = getHandlerAdapter(handler);

        // 3. adapter의 기능을 호출 / handle(): handler의 버전과 동일한 컨트롤러의 기능을 수행)
        // => handle()에서 paraMap을 생성하여 url 파라미터를 가져오고, 이 정보를 해당 컨트롤러에게 넘김
        ModelView mv = adapter.handle(request, response, handler);


        // 4. Controller A - ModelView로부터 저장된 JSP 이름을 가져온다.
        String viewName = mv.getViewName();

        // 5. Controller A - 반환받은 JSP 이름을 기반으로 JSP 파일의 절대경로를 생성하는 과정(아래쪽에 메소드 있음)
        // 생성된 JSP 파일의 절대경로를 View 객체에 저장함
        MyView view = viewResolver(viewName);

        // 6. Controller A - view 객체에 저장된 절대경로를 이용하여 JSP를 forwarding 한다.
        view.render(mv.getModel(), request, response);
    }

    private Object getHandler(HttpServletRequest request) {
        // 1. request 객체의 요청 url를 가져온다.
        String requestURI = request.getRequestURI();

        // 2. 요청 url과 매칭하는 Controller 객체를 handlerMappingMap에서 가져온다.
        return handlerMappingMap.get(requestURI);
    }

    private HandlerAdapter getHandlerAdapter(Object handler) { // handler = controller 객체
        for (HandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) { // supports(): handler와 동일한 버전의 adapter인지 판별한다.
                return adapter; // 찾고있는 버전의 adapter가 맞다면 이를 반환
            }
        }
        // 오류처리 - handler와 일치하는 버전의 adapter가 없는 경우
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
    }

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

 

a) 동작원리

1. Request

- Client로부터 Front Controller에 요청이 들어온다. 

 

2. Handler 조회 

- 요청 객체의 request URL을 이용하여 HandlerMapping으로부터 Controller(= handler)를 조회한다.

Object handler = getHandler(request);
private Object getHandler(HttpServletRequest request) {
        // 1. request 객체의 요청 url를 가져온다.
        String requestURI = request.getRequestURI();

        // 2. 요청 url과 매칭하는 Controller 객체를 handlerMappingMap에서 가져온다.
        return handlerMappingMap.get(requestURI);
}

 

 

3. HandlerAdapter 조회

- 조회된 Controller(= handler)를 처리할 수 있는 HandlerAdapter를 HandlerAdapter 목록으로부터 조회한다.

HandlerAdapter adapter = getHandlerAdapter(handler);
private HandlerAdapter getHandlerAdapter(Object handler) { // handler = controller 객체
        for (HandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) { // supports(): handler와 동일한 버전의 adapter인지 판별한다.
                return adapter; // 찾고있는 버전의 adapter가 맞다면 이를 반환
            }
        }
        // 오류처리 - handler와 일치하는 버전의 adapter가 없는 경우
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
    }

- getHandlerAdapter() 메서드를 살펴보자.

- handlerAdapters라는 자료구조에는 Controller A와 Controller B를 처리할 수 있는 HandlerAdapter가 모두 들어있다.

- 반복문을 돌려 handlerAdapters로부터 HandlerAdapter를 하나씩 꺼내어 검증과정을 거친다. 

- 위의 코드에서는 이 검증을 위해 supports() 메서드를 사용한다. 

 

public class HandlerAdapterControllerA implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        // controller A의 객체가 넘어오면 true를 반환한다. 
        return (handler instanceof ControllerA);
}

public class HandlerAdapterControllerB implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        // controller B의 객체가 넘어오면 true를 반환한다. 
        return (handler instanceof ControllerB);
    }

- HandlerAdapter는 조회된 handler를 본인이 처리할 수 있는지를 판단하는 supports() 메서드를 갖고 있다.

- 위의 supports() 메서드를 살펴보면, 해당 handler가 특정 Controller 인터페이스의 구현체인지 검사한다.

- 이 검사를 통해, HandlerAdapter가 해당 handler를 처리할 수 있는지 판단한다. 

 

 

4. 비즈니스 로직 실행

- 해당 handler를 처리할 수 있는 HandlerAdapter를 찾았다면, 해당 HandlerAdapter를 통해 비즈니스 로직을 실행한다.

ModelView mv = adapter.handle(request, response, handler);
public interface HandlerAdapter {

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

- 비즈니스 로직을 실행하기 위해서 adapter의 handler이라는 메서드를 사용한다.

- 이는 모든 HandlerAdapter 구현체가 보유하고 있는 메서드로, HandlerAdapter 인터페이스에 의해 정해진 이름이다.

- 모든 HandlerAdapter의 비즈니스 로직을 처리하는 메서드가 동일한 이름을 가질 수 있도록 한 것이다.

 

 

5. View 처리

- 이후의 과정은 ModelView를 사용했던 방식과 동일한 처리과정을 거친다.

- Controller A는 ModelView를 반환하므로, ModelView로부터 view 이름을 꺼내와서 render()를 실행한다.

// 1. adapter에서 비즈니스 로직을 수행하면 ModelView 객체를 반환한다.
ModelView mv = adapter.handle(request, response, handler);

// 2. 반환된 ModelView의 객체로부터 view 이름을 꺼내온다. 
String viewName = mv.getViewName(); 

// 3. viewResolver 메서드를 통해 view의 절대경로를 생성하고 반환 받는다.
MyView view = viewResolver(viewName);

// 4. 반환된 view 객체에게 model과 request, response 객체를 전달한다. 
view.render(mv.getModel(), request, response);

3. 결론

- 여기까지 Adapter 패턴을 이용한 유연한 Controller에 대해 알아보았다.

- Adapter 패턴까지 온전히 이해했다면, Spring MVC의 구조를 이해한 것과 마찬가지다. 

- Spring MVC의 구조 또한 Adapter 패턴을 사용한 Front Controller를 기반하기 때문이다.

 

- 다음 포스팅에서는 Spring MVC의 구조에 대해서 본격적으로 알아보려고 한다.

- Spring MVC는 Front Controller에서 확장된 형태이기 때문에 각각의 특징을 이해해야 한다.

- 더불어 Spring MVC가 사용하는 명칭이 조금씩 다르므로, 이에 대해서도 알아보도록 하자.