본문 바로가기
Back-end/java spring

[Spring MVC] 구조 이해

by kkkdh 2023. 5. 15.
728x90

이전 글에서 작성했던, 직접 스프링 MVC 프레임워크를 구현하는 과정과 비교하며 이번에는 실제 스프링 MVC 프레임워크의 구조를 이해하는 과정을 정리하는 강의이다.


스프링 MVC 전체 구조

직접 만들었던 MVC 프레임워크의 구조

(출처: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의)

Spring MVC 구조

(출처: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의)

각 요소들이 이름에 차이는 있지만, 똑같은 형태로 구성되어 있음을 확인할 수 있다.

 

차이점

  • FrontController -> DispatcherServlet
  • handlerMappingMap -> HandlerMapping (interface, 더 확장성이 있음)
  • MyHandlerAdapter -> HandlerAdpater
  • ModelView -> ModelAndView
  • viewResolver -> ViewResolver (interface, 더 확장성이 있음)
  • MyView -> View

 

Spring MVC 또한 front-controller pattern으로 구현되어 있으며, front controller의 역할을 바로 DispatcherServlet이 수행한다.

 

스프링 부트가 내장 tomcat 서버를 띄울 때, 동시에 DispatcherServlet을 servlet으로 등록하며, DispatcherServlet모든 경로('urlPatterns="/")에 대해서 매핑한다.

참고: 기존에 등록한 서블릿도 함께 동작한다. (하지만, 더 세세하게 명시된 구현이 항상 우선순위를 갖는다.)

 

요청의 흐름

  • servlet을 호출하면 HttpServlet이 제공하는 service method 호출
  • Spring MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service method를 overriding
  • FrameworkServlet.service method를 시작으로 하여 여러 메서드가 호출됨
  • DispatcherServlet.doDispatch method가 호출된다.

코드를 통해 확인하는 DispatcherServlet의 doDispatch method 동작 흐름

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());
    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);
}

(출처: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의)

 

기존에 설계해봤던 MVC와 아주 유사한 형태로 동작함을 코드를 통해 파악할 수 있었다. (물론 간단하게 살펴보기 위해 코드에 예외 처리나 인터셉터 기능은 제외되었다고 한다.)

 

Spring MVC 프레임워크 전체 실행 방식 정리

  1. 핸들러 조회: handlerMapping을 통해 요청 URL을 포함한 다양한 정보를 기반으로 알맞은 handler를 조회
  2. 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회
  3. 핸들러 어댑터 실행, 핸들러 어댑터를 이용한 핸들러 실행
  4. ModelAndView 반환: 핸들러 어댑터에 의해 ModelAndView가 반환된다.
  5. viewResolver 호출: 뷰 리졸버를 찾고 실행한다. (JSP의 경우 InternalResourceViewResolver가 자동 등록되고 사용된다고 한다.)
  6. View 반환: 뷰 리졸버에 의해 view의 논리 이름이 물리 이름으로 변경되고, rendering 역할을 담당할 View 객체를 생성해서 반환 (JSP의 경우 InternalResourceView(JstlView)를 반환하는데, 내부에 forward() logic이 있다고 함)
  7. View rendering

 

Spring MVC의 가장 큰 장점은 DispatcherServlet 코드의 변경 없이 원하는 기능을 구현하고 변경 확장이 가능하다는 점이라고 한다. 위에서 언급된 대부분의 기술들이 interface로 구현되어 원한다면, 확장이 가능하다고 한다.

 

하지만, 이미 오랜 세월에 걸쳐 대부분의 기술이 개발되어 있기 때문에, 만들어 사용하는 것보다 잘 찾아보면 다 있을 것이라고 한다.


핸들러 매핑과 핸들러 어댑터

지금의 개발에는 거의 어노테이션 기반으로 @Controller, @RequestMapping와 같은 어노테이션을 활용한 핸들러와 핸들러 어댑터를 사용하지만, 이전에 사용한 방식을 기반으로 spring mvc에서의 핸들러 매핑과 핸들러 어댑터를 확인해 보자.

public interface Controller {
    ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse 
    response) throws Exception;
}

과거에는 어노테이션 Controller가 아닌 위의 인터페이스의 구현체를 만드는 방식으로 핸들러(controller)를 구현했다고 한다.

 

다음과 같이 예시 controller 구현 가능

주석 처리된 부분에 작성된 것과 같이 spring bean의 이름을 controller가 처리할 url pattern으로 사용

  • 이러한 과정을 위해서는 Controller 인터페이스의 구현체인 핸들러를 찾을 수 있는 handlerMapping이 필요하다. (스프링 빈의 이름으로 handler를 찾을 수 있는)
  • 해당 handler를 호출 및 실행시킬 수 있는 handler adapter가 필요하다.

 

HandlerMapping 우선 탐색 순위

HandlerAdapter 우선 탐색 순위

위 순서에 따라서 HandlerMapping을 통해 알맞은 handler를 찾고, handler와 호환이 되는 handler adapter를 찾는 과정을 거치게 되는 것이다.

 

Controller interface (org.springframework.web.servlet.mvc.Controller)에 맞는 handler mapping은 BeanNameUrlHandlerMapping이고, handler를 지원하는 adapter는 SimpleControllerHandlerAdapter이다.

HttpRequestHandler interface를 이용해 구현한 handler 예시

추가적으로 HttpRequestHandler 또한 강의에서 예시로 보여주셨는데, 

  • HttpRequestHandler interface의 구현체 생성
  • 마찬가지로 spring bean name을 기반으로 핸들러를 찾게 된다.
  • 따라서 BeanNameUrlHandlerMapping, HttpRequestHandlerAdapter를 각각 handler mapping, handler adapter로 사용하게 된다.

가장 우선순위가 높은(탐색의 우선순위) handler mapping, handler adpater는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter로, @RequestMapping annotation의 앞글자를 따서 만든 handler mapping, handler adapter라고 한다. (거의 99.99% 현업에서는 이 반식을 차용 중이라고 한다.)


View Resolver

view resolver는 이전에 직접 구현한 MVC에서 view의 논리 이름을 물리 이름으로 변경하고, model을 view에 전달하여 view를 생성하는 작업을 담당했다.

 

우선 View를 사용할 수 있도록 예제 코드를 다음과 같이 변경한다.

ModelAndView를 반환하도록 변경

view name을 이용해 새로운 ModelAndView를 생성 후 반환하도록 변경한다.

 

  • 위와 같이 구현하는 경우 이제 "localhost:8080/springmvc/old-controller" url로 요청을 보내는 경우 new-form에 해당하는 JSP를 이용해 web page를 rendering 해서 보여줘야
  • 하지만, 다음과 같은 추가 설정을 해줘야 spring boot에 의해 ViewResolver가 생성된다.
    • application.properties에 다음 property를 추가
    • spring.mvc.view.prefix=/WEB-INF/views
    • spring.mvc.view.suffix=.jsp
  • Spring MVC가 위의 설정 상황을 확인한 뒤, view resolver를 생성해 논리 이름을 물리 이름으로 변경하는 작업을 대신 수행해 준다.
  • 위 방법 이외에도 별도로 ModelAndView에 전체 경로를 전달해도 된다고는 한다. (권장 X)

 

View Resolver 동작 방식

  1. handler adapter 호출하여 "new-form"이라는 논리 뷰 이름을 획득
  2. View Resolver 호출
    1. BeanNameViewResolver는 new-form이라는 이름의 spring bean을 찾는데, 찾을 수 없음
    2. InternalResourceViewResolver가 호출되어 알맞은 View를 반환
  3. InternalResourceView를 반환 (View도 인터페이스로 구현되어 다양한 구현체를 만들어 사용할 수 있다.)
  4. InternalResourceView는 JSP 사용 시 호출되는 View 구현체로 forward()를 호출해서 처리할 수 있는 경우에 사용됨
  5. view.render() -> InternalResourceView.forward() 호출
  6. forward method에 의해 JSP로 넘어간 뒤 page rendering 되는 방식이다.

Spring-Boot가 자주 등록하는 view resolver

(출처: 스프링 MVC 1편 - 웹 백엔드 개발 핵심 기술)

참고
InternalResourceView는 만약 JSTL 라이브러리가 있는 경우 InternalResourceView를 상속받은 JstlView를 반환. JSTL 태그 사용 시 약간의 부가 기능을 추가로 제공한다고 함.

다른 view는 실제 view를 rendering 하지만, JSP는 forward()를 통해 이동한 뒤 rendering 된다. 다른 view template들은 바로 rendering 됨(view 역할에만 집중하기 때문)

Thymeleaf view template을 사용하는 경우 ThymeleafViewResolver를 등록해야, 최근에는 spring boot가 라이브러리만 추가하면 이러한 작업을 전부 대신해준다고 한다.

Spring MVC - 시작하기

Spring이 제공하는 controller는 annotation 기반으로 동작해서, 매우 유연하고 실용적이다.

 

@RequestMapping

  • 스프링이 만들어낸 어노테이션을 활용한 컨트롤러는 @RequestMapping 어노테이션을 사용하는 컨트롤러이다.
  • 과거에는 스프링 프레임워크의 MVC 파트가 약점이어서 스트럿츠 + 스프링 등 여러 기술 툴의 조합으로 사용했다고 한다.
  • @RequestMapping 어노테이션의 등장으로 Spring MVC가 MVC 부분을 독점하게 되었다.

@RequestMapping 어노테이션을 사용하면, RequestMappingHandlerMapping(핸들러 매퍼), RequestMappingHandlerAdapter(핸들러 어뎁터)를 사용하여 동작하는 방식을 따른다. (이전에 정리한 바와 같이 둘은 각각의 역할에서 최고의 우선순위를 갖는다.)

 

실무에서 99.99%는 @Controller, @RequestMapping을 활용한 어노테이션 기반 컨트롤러를 사용한다고 한다..

 

다음과 같이 위에서도 숱하게 구현한 controller들을 쉽게 어노테이션 기반으로 구현할 수 있다.

new-form controller
member-save controller
member-list controller

이미 알고 있는 바와 같이 @Controller 어노테이션 내부에는 @Component 어노테이션이 포함되어 있어, component scan의 대상이 되고, 이로 인하여 스프링 빈으로 등록된다.

 

참고: 스프링 부트 3.0 이상 버전부터는 @RequestMapping을 클래스 레벨로 선언해도 컨트롤러로 인식하지 않는다고 한다. (아마, 핸들러 어뎁터 등록, 핸들러 매퍼 등록 등의 작업을 @Controller 로만 가능하게 만들었다는 것을 의미하는 것 같다.)

강의에서 설명된 바에 따르면 @Component + @RequestMappinng = @Controller 이렇게 서로 대체가 가능하다고 함(물론, 클래스 레벨에 선언할 때, 당연히 @Controller 하나를 사용하는 것이 훨씬 보기도 좋고 편리하다.)

 

이러한 상황의 이유는 @RequestMapping annotation만으로는 Component scan의 대상이 될 수 없기 때문일 것이고, 스프링 부트 3.0 이상부터는 @Controller를 선언하여 사용하는 방식이 유일한 컨트롤러로 등록하는 방법이 되었기에 @Controller 어노테이션을 사용하는 것이 여러모로 편리할 것으로 보인다.

 

정리

  • @Controller
    • 스프링이 자동으로 스프링 빈으로 등록 (내부에 @Component 포함하기 때문)
    • 스프링 MVC에서 어노테이션 기반 컨트롤러(핸들러)로 인식하게 한다.
  • @RequestMapping
    • 요청 정보를 매핑한다. 해당 URL이 호출되면, 어노테이션을 붙인 메서드가 호출되도록 한다.
    • 어노테이션 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지어도 된다.
  • RequestMappingHandlerMapping은 스프링 빈 중에서도 @RequestMapping or @Controller 어노테이션이 클래스 레벨에 붙어있는 경우에 매핑 정보로 인식하고 등록한다.

RequestMappingHandlerMapping의 isHandler method를 확인하면 위와 같다.


Spring MVC - 컨트롤러 통합

굳이 여러 개의 클래스에 나누어 컨트롤러를 구현하지 않아도 @RequestMapping을 메서드 단위로 적용하여 하나의 컨트롤러 클래스로 통합해서 사용할 수 있다.

이렇게 하나의 컨트롤러 클래스로 통합해서 설계가 가능하다.

새롭게 알게 된 사실로는 class level에서 선언한 @RequestMapping의 url patternmethod level에 선언된 @RequestMapping의 url patternconcatenation 되어 method가 매칭되어 수행될 url pattern이 결정된다는 점이 있었다.

  • class level @RequestMapping("/springmvc/v2/members")
  • method level @RequestMapping("/new-form") → /springmvc/v2/members/new-form
  • method level @RequestMapping("/save") → /springmvc/v2/members/save
  • method level @RequestMapping → /springmvc/v2/members
  • 이런 식으로 매칭된다.

Spring MVC - 실용적인 방식

앞서 MVC framework를 직접 만들어보는 과정에서도 v3는 ModelView 객체를 개발자가 직접 생성해서 반환하는 불편한 방식의 컨트롤러였다.

 

이를 v4 controller를 만들며 논리적 view name만을 반환하는 방식으로 개선했는데, 스프링 MVC에서는 이미 개발자의 편의를 위한 다양한 기능들을 제공하고 있다.

 

실무에서 주로 사용하는 방식은 다음과 같다.

첫 번째 개선 방식

기존의 v4 controller에서 개선했던 바와 같이 String type을 반환하도록 하여, view component의 논리적 이름만을 반환하는 컨트롤러의 설계 또한 Spring MVC에서 제공한다.

 

또한 @RequestParam annotation을 사용해 query parameter를 직접 매핑할 수도 있고, Model 객체를 통해 view component에 전달할 model을 method 내에서 사용할 수도 있다. (request.getParam("~~") 코드와 동일한 기능을 수행한다)

 

@RequestMapping annotation에 method 속성을 지정하여 url pattern + Http method에 따라 수행 가능한 작업에 제한을 두는 것 또한 가능함을 확인할 수 있었다.

더 깔끔하게도 가능하다.

사실 위와 같이 @GetMapping, @PostMapping 등의 HTTP method까지 지정할 수 있는 어노테이션 또한 제공이 되어 위의 방식을 실무에서는 가장 많이 사용한다고 함. (다른 HTTP method에 대한 annotation 또한 물론 제공한다)

728x90

댓글