TIL

[TIL-45/240315] MVC 패턴

prao 2024. 3. 16. 00:10
반응형

MVC 패턴을 접한지는 어느덧 반년이 넘었다.  MVC 패턴을 처음 접하고 Model, View, Controller를 구현하면서 정말 재미있게 구현했던 기억이 난다. 그러다가 Model, View, Controller에 이어서 Service를 분리하여 구현하는 코드를 접하게 되었다. 하지만 그때의 나는 왜 꼭 Service가 필요하지? Controller에서 모든 코드를 구현하면 안되나? 라고 생각하며 Service의 필요성을 확실하게 느끼지 못하였고, 현재까지도 개발할 때 Service를 만들긴 하였으나, 왜 Service가 필요한지, Controller와 어떻게 분리해야 하는지에 대한 확실한 이해가 없이 개발을 해왔던 것 같다. 그렇기 때문에 이번 기회를 통하여 Controller와 Service의 분리를 포함하여 MVC에 대해 확실하게 공부하고자 한다. 시작하겠다.

 

Controller, Service는 왜 분리해야 할까?

내가 처음 느꼈듯이, 많은 사람들이 나와 비슷한 감정을 처음에 느꼈을거라 생각한다. Controller에 모두 구현해도 Service를 분리하여 구현한 것과 똑같이 동작하기 때문이다. 원리를 분석하여 이유를 알아보자.

 

API의 추가 및 변경이 발생할 때

RESTful API를 통해 서비스를 운영하고 있다고 가정하자. 사이트 반응속도가 너무 느려 서비스를 Light House로 분석해보니 하나의 페이지에서 너무 많은 API를 호출해 문제가 생겼다고 한다. 이러한 문제를 해결하기 위해 팀에서는 QraphQL을 도입하기로 결정하였다고 가정하자. 이때 만약 Controller에 모든 기능을 구현하였다면, GraphQL API를 구현하는 Controller에 또 다시 구현해야 하지만 Service에 기능을 구현하였다면 이럴 문제가 없다.

GqlController를 만들어 Service에 연결

그림처럼 GqlController를 생성하여 Service에 연결하면 된다.

이렇게 되면 또 하나의 장점이 존재한다. 바로 점진적으로 API를 변경할 수 있다는 점이다. 만약 API를 변경하기 위해 전부 컨트롤러를 변경한다면 모든 API가 변경된 이후에야 배포할 수 있다. 그러나 이런식으로 변경된 부분만 GqlController로 옮길 수 있으면 두 가지 API를 동시에 사용할 수 있어 점진적으로 서비스를 개선하여 한 번에 변경할 때 생기는 문제들을 미리 예방할 수 있다.

 

트랜잭션 처리

Spring에서 Service의 고유한 기능 중 하나는 트랜잭션 처리 기능이다.

트랜잭션이란 DB 업데이트시 일련의 업데이트 과정을 한 번에 처리하고 실패시 전체를 롤백, 성공시 전체를 커밋해주는 기능이다.

대표적인 예시인 송금 과정을 예시로 보자.

송금 과정

위 그림은 트랜잭션이 없는 상태의 송금 실패 시나리오다. DB에서 B의 계좌를 업데이트하는 것에 실패하였다. 트랜잭션이 없다면 A의 계좌를 롤백하지 않아 A의 계좌에만 5000원이 감소하게 된다. 그러나 트랜잭션을 사용한다면 5에서 실패하였을 때 A의 계좌도 다시 롤백하기 때문에 실패해도 안전하게 돈을 보존할 수 있다.

 

Controller와 Service의 역할

Spring Application Layered Pattern / Domain Driven Design Layered Pattern

위 그림은 웹 서비스를 만들 때 많이 사용하는 두 가지 계층 구조이다.

Spring Application Layered Pattern

  • Spring에서 흔히 사용하는 계층구조
  • Web Lyaer에서 사용자의 요청을 받음
  • Service Layer에서 실제 요청을 처리
  • Repository Layer를 통해 Data를 조회 및 변경

Domain Driven Desgin Layered Pattern

  • User Interface Layer에서 사용자의 요청을 받고 응답을 만듦
  • Application Layer에서는 기능을 제공
  • Domain Layer에서는 기능을 제공하기 위한 실제 변경 작업을 진행
  • Infrastructure Layer에서는 Database, 다른 Application, 웹 프레임워크 등 인프라에 접근하도록 인터페이스를 제공
  • Controller는 Web Layer와 User Interface Layer에 해당
  • Service는 Service Layer와 Application Layer 혹은 Domain Layer에 해당
단일 책임 원칙(SRP)
Controller에서는 사용자의 입력 처리와 응답에만 집중하고,
Service에서는 실제 기능을 어떤 식으로 제공하는지에 대해서만 집중하여야
나중에 다른 팀원이 코드를 수정할 때도 어떤 기능이 어디에 있는지 쉽게 알 수 있고, 변경하기 쉽다.

 

Spring MVC 구조

Spring MVC 구조

Spring MVC 동작 순서

  1. 핸들러 조회 : 핸들러 매핑을 통해 URL에 매핑된 핸들러(컨트롤러) 조회
  2. 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터 조회
  3. 핸들러 어댑터 실행: 핸들러 어댑터 실행
  4. 핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행
  5. ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해 반환.
  6. viewResolver 호출: 뷰 리졸버를 찾아 실행한다.
    ⇒ JSP: InternalResourceViewResolver가 자등 등록되어 사용된다.
  7. View 반환: 뷰 리졸버는 뷰의 논리 이름을 물이 이름으로 바꾸고 렌더링 역할을 담당하는 뷰 객체 반환.
    ⇒ JSP: InternalResourceView(JstlView)를 반환하는데, 내부에는 forward() 가 있다.
  8. 뷰 렌더링: 뷰를 통해서 뷰를 렌더링한다.

 

DispatcherServlet의 구조

디스패처 서블릿은 부모 클래스에서 HttpServlet을 상속받아 사용하며, 서블릿으로 동작한다.

DispatcherServlet → FrameworkServlet → HttpServletBean → HttpServlet

스프링 부트 구동시 DispatcherServlet을 서블릿으로 자동 등록하며 모든 경로(urlPattern="/")에 대해 매핑한다.

즉, Spring MVC 역시 프론트 컨트롤러 패턴으로 구현되어 있고 DispatcherServlet이 프론트 컨트롤러의 역할을 한다.

(경로의 우선순위는 자세할수록 높기 때문에 기존에 작성한 서블릿도 같이 동작함)

다이어그램

요청 흐름

  1. 서블릿이 호출되면 HttpServlet이 제공하는 service() 메서드 호출
  2. 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해둠
  3. FrameworkServlet.service()를 시작으로 여러 메소드가 실행되며 DispatcherServlet.doDispatch()가 호출됨

 

DispatcherServlet.doDispatch() 핵심 로직 분석

  • 메소드 이름대로 해당 메소드에서 적절한 컨트롤러를 찾아 매핑해주고 뷰까지 찾아 View를 반환해 렌더링까지 해주는 핵심 메소드
protected void doDispatch(HttpServletRequest request, 
                              HttpServletResponse response) throws Exception {

    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    ModelAndView mv = null;

    // 1. 핸들러 조회
    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null) {
        noHandlerFound(processedRequest, response);
        return;
    }
    //2.핸들러 어댑터 조회-핸들러를 처리할 수 있는 어댑터
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    /**
     * 3. 핸들러 어댑터 실행 
     * -> 4. 핸들러 어댑터를 통해 핸들러 실행 
     * -> 5. ModelAndView 반환 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
     */
		mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

private void processDispatchResult(HttpServletRequest request,
                                   HttpServletResponse response, 
                                   HandlerExecutionChain mappedHandler, 
                                   ModelAndView mv, Exception exception) throws Exception {
    // 뷰 렌더링 호출
    render(mv, request, response);
}

protected void render(ModelAndView mv, HttpServletRequest request,
                      HttpServletResponse response) throws Exception {
    View view;
    String viewName = mv.getViewName(); //6. 뷰 리졸버를 통해서 뷰 찾기,7.View 반환
    view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
    // 8. 뷰 렌더링
    view.render(mv.getModelInternal(), request, response);
}
  • mappedHandler = getHandler(processedRequest);
    ⇒ 요청에 맞는 적절한 핸들러를 찾아 반환해준다.
  • noHandlerFound(processedRequest, response);
    ⇒ 적절한 핸들러를 찾지 못한경우 404 에러코드를 반환해준다.
  • HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    ⇒ 찾은 핸들러를 처리할 수 있는 핸들러 어댑터를 찾아준다.|
    ⇒ 만약 찾지 못할경우 ServletException 발생
  • mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    ⇒ 찾은 핸들러 어댑터를 이용해 로직을 수행하는 handle 메서드를 호출한다.
    ⇒ 결과로 ModelAndVIew를 반환받고, 이를 이용해 렌더링까지 수행된다.
  • processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    ⇒ 실제 코드는 복잡하게 되있는데 결국 render() 메서드를 호출해준다.
    ⇒ render() 에서는 ModelAndView에서 View를 찾아 뷰 리졸버를 이용해 뷰의 물리적 이름을 완성해서 forward 해준다.

controller와 service의 차이, 그리고 Spring MVC에서 핵심적인 기능을 하는 DispatcherServlet에 대해서 알아보았다.

주말에는 김영한님의 Spring MVC 강의를 들으며 MVC 구조에 대한 더 깊은 이해를 해보려 한다.

반응형