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

9. Front Controller (1)

by devraphy 2022. 3. 4.

0. 개요

- 이전 포스팅에서 Servlet과 JSP를 이용한 MVC 1 구조에 대해 배웠다.

- MVC 1 구조에 대해 배우면서 각 Controller의 중복 코드에 대한 공통 처리가 필요하다는 것을 알게 되었다.

- 이 문제를 해결하기 위해서는 구조적인 접근이 필요하다.

- 이번 포스팅에서 MVC 1의 구조적 문제점을 해결하는 방법에 대해 알아보자.


1. Front Controller의 등장

- Front Controller 패턴은 Spring MVC가 사용하는 패턴과 동일하다.

- Spring의 MVC의 구조는 Front Controller 패턴을 기반으로 확장된 형태이기 때문이다. 

- 그러므로 Front Controller의 흐름을 이해한다면, Spring MVC의 핵심 흐름을 이해하는 것이다.

- Front Controller 구조와 그 흐름에 대해서 알아보자.  

 

* 참고 - Front Controller 패턴을 MVC 2라고 부르기도 한다.

a) Front Controller를 사용하는 이유 

- MVC 1의 각 Controller에는 중복적인 코드가 존재한다.

- 중복적인 코드가 작성될 수밖에 없는 이유는 Controller마다 각각의 진입점을 갖기 때문이다.

- 다수의 진입점을 갖는 이유는 다음과 같다.

   → Controller마다 mapping 되어있는 URL이 각기 다르다.

   → 요청(request)이 각 Controller에게 직접 전달된다.

   → 즉, Controller마다 Servlet 처리를 수행해야 한다. 

- MVC 1은 위의 그림과 같은 구조를 이루고 있다.

- 그러므로 다수의 진입점이 존재한다는 문제가 발생한다.

- 이를 해결하기 위해서는 간단하게 진입점을 하나로 만들면 된다.

- Front Controller는 이 진입점을 하나로 만들며, 중복적으로 작성된 코드를 일괄 처리한다. 

b) Front Controller의 동작원리

- 진입점이 하나라는 의미는 모든 요청을 하나의 Controller가 받는다는 뜻이다.

- 그러므로 Front Controller는 아래의 그림처럼 시스템의 가장 앞쪽에 위치한다.

- Front Controller는 Controller마다 중복적으로 수행했던 Servlet 처리 또한 담당한다.

c) Front Controller의 특징

- Front Controller는 Servlet을 사용하는 Controller다.

- Front Contorller에서 Servlet 처리를 하기 때문에, 다른 Controller는 Servlet을 사용하지 않는다. 

- 진입점이 하나이므로 요청에 맞는 Controller를 Front Controller가 직접 호출한다.

 

- 그렇다면 다음과 같은 질문을 해보자.

  → Front Controller는 어떻게 요청에 맞는 Controller를 호출할까?

  → 각 Controller가 처리한 데이터는 어떻게 client에게 전달될까?

 

- 이에 대해서 차근차근 알아보도록 하자. 


2. Front Controller 구현

- 위에서 확인한 Front Controller의 그림은 Front Controller의 큰 흐름을 표현한 것이다.

- MVC 1 패턴에서 Front Controller 패턴으로 전환하는 과정을 통해 Front Controller의 기본 구조를 이해해보자.

a) 모든 요청을 받는다.

- Front Controller의 가장 첫 번째 역할은 모든 요청을 받는 것이다.

- Front Controller는 단순히 Servlet을 사용하는 Controller이다.

- 그러므로 모든 요청을 받기 위해서 Front Controller의 mapping URL을 다음과 같이 설정한다. 

ex) "/최상단 접근 경로/ *"

@WebServlet(name = "frontController", urlPatterns = "/front/*")
public class FrontController extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       // 공통기능 처리 구현
    }
}

b) 모든 Controller를 interface로 정의한다.

- Controller는 모두 interface로 선언되는데, 이는 객체지향의 DIP와 다형성을 유지하기 위함이다.

 

- Front Controller는 요청 객체를 처리할 수 있는 비즈니스 로직을 가진 Controller를 호출한다.

- 그 다음, Front Controller는 Controller의 비즈니스 로직을 수행하는 메서드를 호출한다.

- 이때 비즈니스 로직을 수행하는 메서드의 이름이 Controller마다 다르다면, Front Controller에서 일괄적으로 호출할 수 없다.

- 즉, Front Controller에서 Controller마다 다른 메서드 이름을 사용하여 호출하게 되는 것이다. (= 중복 코드 발생)

- 이와 같은 문제를 예방하기 위하여 모든 Controller는 interface로 선언하여,

- 각 Controller의 비즈니스 로직을 수행하는 메서드는 모두 동일한 이름을 갖게 된다.(= 다형성)  

 

- 그렇다면 Contoller 구현체는 누가, 어떻게 찾는 것일까? (= DIP)

- 이는 Controller Map이 수행한다.

- Controller Map은 Spring Container처럼 Controller 구현체를 찾아주는 역할을 한다.

- Front Controller는 요청 객체가 들어온 URL를 참고하여, 해당 URL과 매핑된 Controller를 Map에서 조회한다.

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

    // 요청 url에 따른 controller의 매핑정보를 저장하는 자료구조
    private Map<String, ControllerV3> controllerMap = new HashMap<>();

    public FrontController() { // Constructor(생성자)

        // 요청된 Url을 처리할 Controller를 매핑
        controllerMap.put("controller A의 매핑URL", new ControllerA());
        controllerMap.put("controller B의 매핑URL", new ControllerB());
        controllerMap.put("controller C의 매핑URL", new ControllerC());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // request가 들어오면 servlet에서는 service 메소드가 가장 먼저 실행된다.

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

        // 2. 요청 url과 매칭하는 Controller 객체를 controllerMap에서 가져온다.
        ControllerInterface controller = controllerMap.get(requestURI);
        if(controller == null) { // 예외처리 - 매핑에 없는 url이 요청됐을 때
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

    }
}

c) Servlet 대신 Model View

- MVC 1에서 Controller와 JSP 간의 데이터를 전달하는 방식으로 Model을 사용했다.

- MVC 1에서 Model은 HttpServletRequest의 임시저장소를 이용하여 구현되었다.

   → HttpServletRequest.setAttribute(), HttpServletReqeust.getAttribute()

   → Forward()를 사용하여 JSP에게 HttpServletRequest 객체를 넘긴다.

 

- 그러나 Front Controller 패턴에서 Controller(구현체)는 더 이상 Servlet을 사용하지 않는다.

- 그렇다면 Servlet 없이 각 Controller는 어떻게 데이터와 view를 전달할까? 

- 또한 request를 통해 전달되는 URL parameter를 Servlet 없이 어떻게 Controller에게 전달할까? 

 

- 그 해결책은 ModelView에 있다.

- 우선 ModelView의 Model 역할에 대해 알아보자. 

 

- ModelView 클래스는 다음과 같이 구성된다.

@Getter @Setter
public class ModelView {  // url 파라미터와 View 이름을 저장할 객체

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

    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}

- Front Controller는 request 객체에서 URL parameter를 추출하여 ModelView 객체에 담는다.

- 이때 ModelView 클래스의 model은 HashMap으로 구현된다. 

 

- model은 HashMap이므로 key와 value로 데이터를 구성되는 자료구조다.

- key에는 URL parameter의 이름이, value에는 URL parameter의 값이 들어간다.

 

- Front Controller는 reqeust 객체로부터 URL parameter를 추출하여 ModelView 객체에 담는다.

- 그리고 Front Controller는 ModelView 객체를 Controller에게 전달한다.

- 이 과정은 다음과 같이 구현된다.

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

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // request가 들어오면 servlet에서는 service 메소드가 가장 먼저 실행된다.

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

        // 2. request 객체에 들어온 모든 url 파라미터를 paramMap에 저장하는 과정 (아래쪽에 메소드 있음)
        Map<String, String> paramMap = createParamMap(request);

        // 3. controller한테 url 파라미터를 넘겨서 비즈니스 로직을 수행한다(Controller.process)
        // process() 메서드는 JSP 이름이 담긴 ModelView 객체를 반환한다.
        ModelView mv = controller.process(paramMap);

    }

    private Map<String, String> createParamMap(HttpServletRequest request) {
    
        // request 객체에 들어온 url 파라미터를 paramMap에 저장한다.
        Map<String, String> paramMap = new HashMap<>();
        
        request.getParameterNames().asIterator() // 모든 url 파라미터를 가져오기 때문에 iterator를 돌린다.
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        // => url파라미터의 이름을 Key로, value는 url 파라미터의 값으로 저장한다.
        
        return paramMap;
    }
}

 

- 여기까지 정리하자면, Front Controller는 ModelView 객체를 사용하여 Controller에게 데이터를 전달한다. 

 

- 그렇다면 ModelView는 view를 어떻게 처리하는지 알아보자. 

 

d) viewPath 제거

- MVC 1은 Controller가 데이터를 넘겨줘야 하는 JSP의 경로를 viewPath라는 변수에 직접 설정했다.

- Front Controller 구조에서는 Controller가 경로 대신 JSP 이름을 직접 반환한다.

 

- 각 Controller는 ModelView 객체에 JSP 이름을 담아서 반환한다.

- 그러므로 각 Controller에서는 ModelView 객체를 생성한다. (사실 이 부분은 SRP 위반으로, 추후에 개선된다.)

- ModelView 클래스의 생성자는 viewName을 매개변수로 받는다.

- 이 매개변수(viewName)에 JSP 이름이 할당된다. 다음 예시 코드를 살펴보자.

public class ExampleController implements ControllerInterface {

    @Override
    public ModelView process(Map<String, String> paraMap) {
        // Controller에서는 View(JSP)의 이름만 반환한다.
        return new ModelView("new-form");
    }
}

- 위의 메서드에 @Override가 명시된 것을 확인할 수 있다.

- 이는 Controller 인터페이스로부터 오버라이드 되는, 각 Controller의 비즈니스 로직을 수행하는 메서드(= process)다. 

e) ViewResolver

- 각 Controller는 ModelView 객체에 JSP 이름을 담아 Front Controller에게 반환한다. 

- 그렇다면 JSP는 누가 찾아줄까? JSP 이름만으로 어떻게 JSP(= view)를 찾을 수 있을까? 

 

- ViewResolver라는 메서드를 사용하여 JSP 파일을 찾는다.

- ViewResolver 메서드는 JSP 이름을 매개변수로 받아, 이를 이용하여 JSP 파일의 절대 경로를 반환한다.

- 다음 코드를 살펴보자.

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

    // 요청 url에 따른 controller의 매핑정보를 저장하는 자료구조
    private Map<String, ControllerV3> controllerMap = new HashMap<>();

    public FrontController() { // Constructor(생성자)

        // 요청된 Url을 처리할 Controller를 매핑
        controllerMap.put("controller A의 매핑URL", new ControllerA());
        controllerMap.put("controller B의 매핑URL", new ControllerB());
        controllerMap.put("controller C의 매핑URL", new ControllerC());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // request가 들어오면 servlet에서는 service 메소드가 가장 먼저 실행된다.

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

        // 2. 요청 url과 매칭하는 Controller 객체를 controllerMap에서 가져온다.
        ControllerInterface controller = controllerMap.get(requestURI);
        if(controller == null) { // 예외처리 - 매핑에 없는 url이 요청됐을 때
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 3. request 객체에 들어온 모든 url 파라미터를 paramMap에 저장하는 과정 (아래쪽에 메소드 있음)
        Map<String, String> paramMap = createParamMap(request);

        // 4. controller한테 url 파라미터를 넘겨서 비즈니스 로직을 수행한 후
        // 반환되는 JSP 이름이 담긴 ModelView 객체를 가져온다.
        ModelView mv = controller.process(paramMap);

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

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

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

    }

    private Map<String, String> createParamMap(HttpServletRequest request) {
    
        // request 객체에 들어온 url 파라미터를 paramMap에 저장한다.
        Map<String, String> paramMap = new HashMap<>();
        
        request.getParameterNames().asIterator() // 모든 url 파라미터를 가져오기 때문에 iterator를 돌린다.
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        // => url파라미터의 이름을 Key로, value는 url 파라미터의 값으로 저장한다.
        
        return paramMap;
    }

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

 

- 우선 viewResolver()를 살펴보자. 

- Front Controller는 Controller에서 반환된 ModelView 객체에서 JSP 이름을 꺼내온다.

Strng viewName = mv.getViewName();

 

- 이렇게 받아온 viewName을 viewResolver() 메서드에게 전달하여 해당 JSP 파일의 절대 경로를 생성한다.

- 생성된 JSP 파일의 절대 경로는 MyView 객체에 담긴다.

 MyView view = viewResolver(viewName);

 

- MyView 객체의 render() 메서드를 사용하여 client에게 전달한다. 

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

 

- MyView 클래스는 view를 랜더링 하는 역할을 수행한다.

- 여기서 랜더링이란, 단순하게 client에게 view를 전달한다는 것을 말한다.

- 아래의 MyView 클래스의 코드를 보면 이해가 쉬울 것이다.

public class MyView {

    private String viewPath;

    public MyView(String viewPath) {
        this.viewPath = viewPath;
    }

    public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        modelToRequestAttribute(model, request);
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);

    }

    private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
    
        // model의 모든 데이터를 꺼내서 request의 임시저장소에 저장한다.
        model.forEach((key, value) -> request.setAttribute(key, value));
    }
}

- 위의 코드에서 확인할 수 있듯이, render() 메서드는 Servlet을 사용하여 forward() 하는 것을 확인할 수 있다.

- 이 과정은 Front Controller에서 수행되는 것으로, Front Controller가 일괄적으로 Servlet을 처리한다는 부분은 위배되지 않는다.


3. 전체적인 Front Controller의 흐름 

- 지금까지의 설명한 내용은 아래와 같은 흐름을 만든다.

- 여기까지 기본적인 Front Controller의 구조를 알아보았다.

- 하지만, 이것이 끝이 아니다. 아직 개선해야 할 부분이 남아있기 때문이다.

 

a) 개선사항 

1. 역할의 분리가 완전하지 않다.

 → SOLID 원칙 중 SRP가 지켜지지 않고 있다.

 → 각 Controller는 비즈니스 로직을 처리하고 ModelView 객체를 직접 생성하여 반환하기 때문이다.

 

2. 구조적으로 아직 복잡하다.

 → 구조적으로 복잡하다 보니 코드의 간결성이 많이 떨어진다. 

 → 코드가 간결하지 않다 보니 이해하기 힘들고, 어떤 역할과 기능을 수행하는지 한눈에 알기 힘들다.

 

- 다음 포스팅에서는 위의 2가지 개선사항을 어떻게 구현하는지에 대해서 설명하도록 하겠다.

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

11. Spring MVC의 특징  (0) 2022.03.08
10. Front Controller(2)  (0) 2022.03.07
8. Forward와 Redirect  (0) 2022.03.03
7. MVC 1의 동작원리와 한계점  (0) 2022.03.02
6. MVC 1의 등장  (0) 2022.03.01

댓글