Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

참새의 이야기

[MVC1] MVC 프레임워크의 구성 본문

Spring

[MVC1] MVC 프레임워크의 구성

참새짹짹! 2023. 8. 2. 21:34

MVC 프레임워크의 구성

Front Controller

개발을 하다 보면, 여러 클라이언트의 요청에 공통적인 처리가 필요한 경우가 많다.

공통적인 처리를 각각의 controller에서 따로 한다면 코드의 중복이 생겨버린다.

깜빡하는 순간 서비스의 장애로 이어질 수도 있다.

만약 공통적인 처리를 담당하는 컨트롤러가 있다면 개발의 복잡성을 덜어낼 수 있을 것이다.

이를 FrontController라고 부른다.

FrontController는 모든 요청을 쓸어 담아 공통적으로 이루어져야 하는 로직을 수행하고 각각의 컨트롤러에게 다음 로직을 수행하도록 넘긴다.

FrontController가 요청이 들어갈 수 있는 단 하나의 입구이고 적절한 컨트롤러를 찾아서 호출해 주기 때문에 다른 컨트롤러들은 servlet을 사용하지 않아도 된다.

지금껏 스프링을 공부하면서 뭔지도 모르는 채 마주쳤던 DispatcherServlet도 FrontController패턴으로 구성되어 있다.

Servlet으로만 구성된 패턴에서 스프링 MVC로 발전해 나가는 과정을 단계적으로 공부하기 위해 여러 버전의 FrontController를 만들어보았다.

V1: FrontController의 도입

@Slf4j
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {

    private Map<String, ControllerV1> controllerMap = new HashMap<>();

    public FrontControllerServletV1() {
        controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
        controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
        controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                // URI를 이용해 적절한 controller를 찾기.
        String requestURI = request.getRequestURI();
        log.info("requestURI = {}", requestURI);
        ControllerV1 controller = controllerMap.get(requestURI);
        log.info("controller = {}", controller);

                // controller에 담긴 controller 객체가 null이 아닌지 확인 후 실행.
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        controller.process(request, response);
    }
}

FrontController 서블릿의 서비스에서 적절한 컨트롤러를 찾아 실행시킨다.

/front-controller/v1/로 들어오는 모든 요청은 이 서블릿을 통해 다른 컨트롤러로 들어간다.

V2: View 분리

이 FrontController의 문제점은 모든 컨트롤러가 view로 넘어갈 때 중복되는 코드를 가지고 있다는 것이다.

이를 개선하기 위해 view를 분리하고 view에서 forward 하도록 한다.

아래는 변경된 FrontController의 service 메서드와 View의 코드다.

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String requestURI = request.getRequestURI();

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

    MyView view = controller.process(request, response);
    view.render(request, response);
  }
public class MyView {
     private String viewPath;
     public MyView(String viewPath) {
       this.viewPath = viewPath;
     }
     public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
         RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
         dispatcher.forward(request, response);
     }
}

FrontController는 컨트롤러로부터 반환받은 view를 MyView에게 넘겨 render와 forward가 이루어지도록 한다.

V3: Model 추가

앞서 각각의 컨트롤러에서 servlet을 제거했다.

하지만, 각 컨트롤러에서 여전히 HttpServletRequest와 HttpServletResponse를 사용한다.

이를 Map으로 대신 넘기도록 하여 제거하고, view의 render 메서드를 호출할 때는 Model 객체를 parameter로 넘기도록 하면 서블릿 기술을 컨트롤러에서 완전히 제거할 수 있다.

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String requestURI = request.getRequestURI();

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

    // request에서 parameter만 Map으로 추출하고 비즈니스 로직을 수행.
    Map<String, String> paramMap = createParamMap(request);
    ModelView mv = controller.process(paramMap);

        // 불러야 하는 view를 찾기 위한 로직.
    String viewName = mv.getViewName();
    MyView view = viewResolver(viewName);

    view.render(mv.getModel(), request, response);
}
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();
}

새로 추가한 ModelView 객체를 통해 view의 이름과 view에서 필요한 데이터들을 운반한다.

V4: 실용적인 컨트롤러

매번 컨트롤러에서 ModelView 객체를 생성하고 반환하는 것은 개발자의 편의를 위해서는 바람직하지 않다.

예를 들어 아래와 같은 경우, ModelView 객체에 담지 않아도 반환된 “members”를 이름으로 하는 view를 찾아 render 해준다.

@GetMapping
public String members(Model model) {
    List<Member> members = memberRepository.findAll();
    model.addAttribute("members", members);

    return "members";
}

이런 편의를 제공하기 위해 FrontController를 아래와 같이 개선할 수 있다.

@Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();

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

        // request에서 parameter만 Map으로 추출하고 비즈니스 로직을 수행.
        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();

        // controller로부터 modelview가 아닌, viewname을 받음.
        String viewName = controller.process(paramMap, model);
        MyView view = viewResolver(viewName);

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

V5: 유연한 컨트롤러

V3와 V4 모두 좋은 컨트롤러이다.

두 버전을 모두 활용하기 위해서 개선한 것이 다섯 번째 FrontController이다.

동작 방식은 이렇다.

  1. FrontController가 컨트롤러와 url의 매핑에서 사용가능한 handler를 가져온다.
  2. 해당 handler를 사용하기 위한 어댑터를 찾고 이를 통해 handler에게 처리를 요청한다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Object handler = getHandler(request);
    if (handler == null) {
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        return;
    }
    MyHandlerAdapter adapter = getHandlerAdapter(handler);
    ModelView mv = adapter.handle(request, response, handler);
    MyView view = viewResolver(mv.getViewName());
    view.render(mv.getModel(), request, response);
}

어댑터는 아래와 같은 interface로 구성된다.

public interface MyHandlerAdapter {
    boolean supports(Object handler);

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

정리

위와 같은 개선을 통해 스프링의 MVC 구조를 알아봤다.

FrontController의 역할을 하는 DispatcherServlet의 내부를 이해하는 등, 단계적으로 발전시켜 MVC 구조의 작동 방법을 원리와 함께 이해할 수 있었다.

reference

이 글은 김영한님의 '스프링 MVC 1편'을 듣고 작성했습니다.

'Spring' 카테고리의 다른 글

[MVC1] 스프링 MVC Response  (0) 2023.08.04
[MVC1] 스프링 MVC Request  (4) 2023.08.04
[MVC1] Slf4j를 사용한 logging  (0) 2023.08.03
[MVC1] 스프링 MVC 활용  (0) 2023.08.02
[MVC1] 웹 애플리케이션에 대한 이해  (0) 2023.07.14