본문 바로가기
Back-end/Spring MVC 개념

10. Front Controller(2)

by devraphy 2022. 3. 7.

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가 사용하는 명칭이 조금씩 다르므로, 이에 대해서도 알아보도록 하자. 

'Back-end > Spring MVC 개념' 카테고리의 다른 글

12. Annotation 기반의 Spring MVC  (0) 2022.03.09
11. Spring MVC의 특징  (0) 2022.03.08
9. Front Controller (1)  (0) 2022.03.04
8. Forward와 Redirect  (0) 2022.03.03
7. MVC 1의 동작원리와 한계점  (0) 2022.03.02

댓글