스프링 MVC 구조
스프링의 MVC 구조는 다음과 같다.
스프링 MVC에선, FrontController 가 DispatcherServlet으로 구현되어 있으며 스프링 MVC의 핵심이라고 할 수 있다.
DispatcherServlet 서블릿
DispatcherServlet은 HttpServlet을 상속하는 부모를 상속하고 있다. 또한, 스프링 부트는 DispatcherServlet을 서블릿으로 자동 등록하고 모든 경로 "urlPatterns="/" 에 대해서 매핑한다.
요청 흐름
- 서블릿이 호출되면 HttpServlet의 service()가 호출
- SpringMVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이딩 해두었으며, 이 메서드에서 여러 메서드가 호출되면서, DispatcherServlet의 핵심 메서드인 doDispatch()가 호출된다.
API 소스코드를 확인해 봤더니, 실제 흐름을 간략히 올려보겠다.
service() ~ doDispatch() 이전까지
// 1. FrameworkServlet의 service() 호출.
// 여기는 FrameworkServlet의 service()이다. 요청이 들어오면 아래의 메서드가 자동 호출된다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 요청 메서드가 HttpMethod의 한 종류인 경우를 의미한다.
if (HTTP_SERVLET_METHODS.contains(request.getMethod())) {
super.service(request, response);
}
else {
processRequest(request, response);
}
}
// 2. 만약 요청 메서드가 get이라면 super.service()에 의해 아래의 doGet()이 호출된다.
// 이외에도 여러 HTTP Method에 대해 적잘한 메서드가 대응되어 있었다.
// 자세한 것은 HttpServlet의 service()를 메서드와 FrameworkServlet에서 확인하자.
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
// 3. processRequest() 호출.
// 여기에서 doService()를 호출한다.
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doService(request, response);
}
// 4. doService()의 내부엔 다음과 같이 doDispatch()를 호출하고 있다.
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
doDispatch(request, response);
}
}
doDispatch() 코드
실제 구현 코드는 이것보다 훨씬 복잡하지만, 핵심 코드는 다음과 같다.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1. 현재 요청을 처리할 수 있는 핸들러 반환.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 2. 현재 요청을 처리할 수 있는 핸들러 어댑터 반환.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 3. 실제 핸들러를 호출한다.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 4. 이 메서드에선 render할 view가 반환되었다면, render()를 호출하여 다른 resource로 이동한다.
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
- HandlerMapping(핸들러 매핑) :
- 핸들러 매핑에서 요청을 처리할 핸들러를 찾아야 한다.
- 0 : RequestMappingHandlerMapping : 애너테이션 기반 컨트롤러인 @RequestMapping에 사용
- 1 : BeanNameURLHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾음
- Handler Adapter(핸들러 어댑터) :
- 핸들러 매핑을 통해서 찾은 Handler 를 실제로 실행할 수 있는 핸들러 어댑터가 필요하다.
- 0 : ReqeustMappingHandlerAdapter : 애너테이션 기반의 컨트롤러인 @RequestMapping에 사용. RequestMappingHandlerMapping과 한 쌍인거 같다.
- 1 : HttpRequestHandlerAdapter : HttpRequestHandler 처리
- 2 : SimpleControllerHandlerAdapter ; Controller 인터페이스 구현체 처리
https://www.springcloud.io/post/2022-08/spring-mvc-handlermapping/#gsc.tab=0
참고 : 여전히 헷갈리고 알고 싶은 것.
스프링 부트 3.0 이후, RequestMappingHandlerMapping은 @Controller가 붙어 있는 것만 빈들만 매핑 대상으로 인식한다. 하지만 이건 내가 따로 인터페이스를 구현하지 않고, @Controller만 적용한건데, 어떻게 반환된 Object 타입의 handler를 HandlerAdapter가 적절하게 프로그래밍 해서 실제 컨트롤러를 가져올 수 있는거지?에 대한 궁금증이 계속 남아있었다. (지금 생각해보면 메타정보와 reflection API를 활용했기 때문)
beanName으로 핸들러를 매핑하는 방식에선 바로 컨트롤러를 객체를 반환하고 HandlerAdapter는 TypeCasting을 통해서 해결할 수 있겠단 생각이 들었지만, 내가 고민하고 있었던 것의 대답으론 충분하지 않았다. 공부를 계속 하면서 깨달은 건 HandlerMethod 라는 객체에서부터 이 고민의 대답이 시작된다고 생각한다.
HandlerMappingInfo는 RequestMappingHandlerMapping이 구현하는 인터페이스이며, 애플리케이션 실행 시점에 매핑 대상(@Controller)의 모든 매핑 후보 메서드에 대한 메타정보(빈 메타정보, 메서드 메타 정보, 파라미터 메타 정보, 애터네이션 메타 정보)를 담아둔다. 클라이언트로부터 요청이 들어오면 Request URL과 HTTP 메서드를 기반으로 이 메타 정보와 핸들러를 매핑하는데, 애너테이션 기반 컨트롤러라면 hadler에 HandlerMethod와 handlerInterceptor 목록을 wrapping 하는 HandlerExecutionChain을 DispatcherServlet에 반환하고 HandlerAdapter는 이 HandlerMethod를 사용해 실제 컨트롤러를 찾아서 호출한다.
핸들러를 매핑할 땐 대부분 Request URL을 사용하여 핸들러를 찾아내고, 애너테이션 기반 핸들러인 경우엔 HandlerMethod를, Controller, HttpRequestHandler를 구현한 경우엔 실제 핸들러 객체를 반환한다. 그리고 Handler Adapter의 과정에서 HandlerMethod를 사용해서 실제 컨트롤러를 가져오겠지만, 이외의 두가지 경우에선 TypeCasting으로도 충분히 실제 컨트롤러 로직을 호출할 수 있어야 한다.
결국 내 가설이 맞다면, 실제 컨트롤러를 호출하기 전 단계인 Interceptor의 preHandle()에서, @Controller가 사용된 핸들러가 아닌 경우 paramter로 받는 handler는 절대 HandlerMethod로 형변환이 될 수 없어야 한다. 즉, @Controller인 경우에만 preHandle()에서 handler를 HandlerMethod로 형변환이 가능해야 한다는 것이다.
View Resolver 동작 방식
스프링에서 기본으로 제공하는 View Resolver는 훨씬 많이 있지만 중요한 2개만 알아보자
- BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다.
- InternarlResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
@Component("/springmvc/old-controller")
public class OldController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("OldController.handleRequest");
return new ModelAndView("new-form");
}
}
이와 같은 코드가 있을 때 동작 순서는 다음과 같다.
- 1. 핸들러 어댑터에게 "new-form"이라는 논리 뷰 이름을 가진 ModelAndView를 반환한다.
- 2. new-form이라는 뷰 이름으로 viewResolver를 순서대로 호출하며 적절한 viewResolver를 찾는데, BeanNameViewResolver에서, "new-form"이라는 빈으로 등록된 뷰는 없기 떄문에 InternalResourceViewResolver가 호출.
- 3. InternalResourceViewResolver는 InternalResourceView를 반환하고, JSP 처럼 다른 resource로 이동할 때 forward()를 호출해서 처리할 수 있는 경우에 사용.
- view.render()가 호출되며, InternalResourceView는 forward()를 사용해서 JSP 실행
참고 : JSP 실행, Thymeleaf 뷰 템플릿
다른 뷰는 실제 뷰를 랜더링하지만, JSP의 경우엔 forward()를 통해서 해당 JSP로 이동해야 랜더링이 된다. Thymeleaf 와 같은 다른 뷰 템플릿은 forward() 없이 바로 랜더링 되며, 이를 사용하기 위해선 ThymeleafViewResolver를 등록해야 한다. 최근에는 라이브러리만 추가하면 스프링 부트가 이런 작업도 자동으로 해준다.
스프링 MVC
스프링이 제공하는 애너테이션 기반 컨트롤러는 매우 유연하고 실용적이다. 과거의 자바 언어는 애너테이션이 없기도 했지만, 스프링도 처음부터 유연한 컨트롤러를 제공하진 않았다.
@RequestMapping
스프링에서 애너테이션 기반의 아주 유용한 컨트롤러를 제공한다고 했는데, 바로 이 애너테이션을 사용한 컨트롤러이다.
실습을 해봤더니, 애너테이션이 아닌 Controller, HttpRequestHandler 인터페이스를 구현한 컨트롤러는 @RequestMapping을 적용 자체는 가능하나, 매핑하는 것은 불가능했다. (스프링 부트 3.0 이후 부턴 @Controller만 매핑 대상으로 인식하기 때문에 @RequestMapping이 붙어 있더라도 인식되지 않는다.)
왜 인터페이스를 구현한 컨트롤러는 유연하지 않은 것처럼 얘기할까? 에 대해서 생각을 좀 해봤더니, 답은 간단했다.
이러한 컨트롤러들은 조상 인터페이스 단 한 개의 추상 메서드를 구현하고, 핸들러 어댑터는 이 메서드를 호출하는 메커니즘으로 비즈니스 로직을 처리하기 떄문이다. 따라서 하나의 컨트롤러에서 여러 url을 매핑하는 것이 불가능하다.
하지만, 애너테이션 기반 컨트롤러는 하나의 컨트롤러에서 다양한 HTTP 메서드 매핑 애너테이션을 사용해서 다수의 url을 매핑하는 메서드를 만들수 있었다.
@RequestMapping
- RequestMappingHandlerMapping
- RequestMappingHandlerAdatper
이 둘은 핸들러 매핑과 핸들러 어댑터를 찾을 때 가장 우선순위가 높고, 이 둘은 모두 @RequestMapping을 처리한다.
즉, @Controller로 빈을 정의하고, @RequestMapping 으로 매핑 메서드를 만들면 이 두개가 동작한다. 이 방식은 실무에서 99.% 쓰인다고 한다.
@Controller
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
private ModelAndView handleRequest() {
return new ModelAndView("new-form");
}
}
- @Controller
- 스프링이 자동으로 해당 객체를 스프링 빈으로 등록한다.
- 스프링 MVC에서 이를 애너테이션 기반 컨트롤러로 인식한다. 이 덕분에 RequestMappingHandlerMapping가 이 매핑 대상을 알 수 있다.
- @RequestMapping
- 요청 정보를 매핑한다. 해당 URL이 호출되면 이 메서드가 호출된다. 애너테이션 기반으로 동작하기에 메서드명은 아무거나 상관없다.
- Model And View
- 모델과 뷰 정보를 담아서 반환한다.
다시 한번 말하지만, 스프링 부트 3.0부터 클래스 레벨에 @RequestMapping이 있어도 스프링 컨트롤러로 인식하지 않는다. 오직 @Controller가 있어야만 스프링 컨트롤러 인식하기 때문에 @Controller가 없는 경우엔 RequestMappingHandlerMapping, RequestMappingHandlerAdatper는 동작하지 않는다.
아래는 ModelAndView를 사용하는 예시 코드이다.
@RequestMapping("/springmvc/v1/members")
private ModelAndView process() {
List<Member> members = memberRepository.findAll();
ModelAndView mv = new ModelAndView("members");
mv.addObject("members", members);
return mv;
}
뷰에서 결과값을 참조하기 위해선, Model이 필요하다고 했다.
ModelAndView에서 Model 데이터를 추가할 때 addObject()를 사용하면 된다.
애너테이션 기반 컨트롤러는 여러 메서드로 URL을 매핑할 수 있다.
따라서 전체적인 구조는 아래의 코드를 보면 왜 이게 유연한지 더 잘 다가올 것이다.
@Controller
@RequestMapping("/springmvc/v2/members") // 1. 클래스 레벨 매핑
public class SpringMemberControllerV2 {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@GetMapping("/new-form") // 2. 메서드 레벨 매핑
private ModelAndView newForm() {
...
}
@PostMapping("/save")
public ModelAndView saveMember(HttpServletRequest request) {
...
}
@GetMapping
public ModelAndView members() {
...
}
}
각 메서드를 일일히 url을 매핑하기 보다 클래스 레벨의 @RequestMapping에 공통 URI를, 메서드 레벨에서 각각 따로 URI를 정해주면, 유지 보수하기도 쉽고 간결해진다.
'스프링 > 스프링 MVC' 카테고리의 다른 글
검증 validation (0) | 2023.05.19 |
---|---|
스프링 MVC 기본 기능 (0) | 2023.05.16 |
MVC 프레임워크 (0) | 2023.05.14 |
Servlet의 한계, 템플릿 엔진, MVC 패턴 (0) | 2023.05.13 |
서블릿 (0) | 2023.05.12 |