SpringBoot MVC 파헤치기 (2)
Spring MVC란?
Spring을 활용하여 만든 애플리케이션은 Web 기반인 경우가 많습니다. 이때 Front Controller Pattern을 이용해서 만든 방식이 Spring MVC입니다. MVC는 Model, View, Controller 클래스로 분할하여 유연하고 확장성을 장점으로 갖추었습니다.
Model
- 데이터와 비즈니스 로직을 관리
- 애플리케이션이 포함해야 할 데이터가 무엇인지를 정의
- 일반적으로 POJO로 구성
View
- 레이아웃과 화면을 처리
- 애플리케이션의 데이터를 보여주는 방식을 정의
Controller
- VIew와 Model 사이의 인터페이스 역할
- 애플리케이션 사용자의 입력에 대한 응답으로 Model 및 View를 업데이트하는 로직을 포함
- Model/View에 대한 사용자 입력 및 요청을 수신하여 그에 따라 적절한 결과를 Model에 담아 View에 전달
- 즉, Model Object와 이 Model을 화면에 출력할 View Name을 반환
Spring MVC 프로세스
하나씩 알아가 보도록 하겠습니다.
Filter와 Interceptor에 관한 내용은 생략하도록 하겠습니다.
1. 요청(Request)
- Client로부터 요청을 받을 시 Filter를 거쳐 통과되는 요청은 DispatcherServlet에게 전달됩니다.
- DispatcherServlet 전달된 요청은 doDispatch를 통해서 handler를 찾는 과정을 수행합니다.
- hadler를 찾는 과정에서는 RequestMappingHandlerMapping 객체를 통해서 찾게 됩니다. 이때 URI를 통해서 찾습니다.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 중략
// URL에 맵핑된 Handler를 찾습니다.
mappedHandler = getHandler(processedRequest);
// Handler를 수행할 수 있는 HandlerAdapter를 찾습니다.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 중략
// HandlerAdapter를 통해서 Handler를 실행합니다.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// ModelAndView와 응답을 반환합니다.
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
2. HandlerMapping
mappedHandler = getHandler(processedRequest);
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
- getHandler()를 통해서 Handler를 찾습니다.
우선순위로 RequestMappingHanlderMapping이 사용되고, 없을 시 BeanNameUrlHanlderMapping이 사용됩니다.
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
3. HandlerAdapterList
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
- Handler를 수행할 수 있는 HandlerAdapter를 찾습니다. 없다면 예외를 날립니다.
아래 우선순위로 찾게 됩니다. 저희는 @RequestMapping으로 찾으므로, RequestMappingHandlerAdapter가 사용됩니다.
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션 X, 과거에 사용) 처리
4. Adapter, Handler 실행 ModelAndView 반환
Intercepter를 만족하여 통과하거나, 혹은 없을 경우 Adapter를 실행합니다.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
HandlerAdapter 인터페이스를 구현하고 있는 RequestMappingHandlerAdapter 이용합니다. 이것은 어노테이션 기반의 Controller인 @RequestMapping에서 사용됩니다.
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
invocableMethod.invokeAndHandle(webRequest, mavContainer);
- 위의 함수가 실행되면, Controller가 동작합니다. 그리고 getModelAndView()를 통해서 ModelAndView를 반환합니다.
5. ViewResolver실행, View 반환
InternerResourceViewResolverd와 BeanNameViewResolver 자동으로 등록되고 사용됩니다.
JSP를 사용하고자 하면 application.yml에 suffix와 prefix를 명시해주면 됩니다.
spring:
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
Thymeleaf를 dependency에 추가하였다면 ThymeleafViewResolver가 등록되고 사용됩니다.
prefix와 suffix의 기본값은 각각 'classpath:/templates/'와 '. html'입니다.
우선순위에 따라서 Resolver가 실행됩니다. 더 많은 Resolver가 존재하지만 많이 사용되는 Resolver만 적었습니다.
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {
private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered
/**
* Specify the order value for this ViewResolver bean.
* <p>The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered.
* @see org.springframework.core.Ordered#getOrder()
*/
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = obtainApplicationContext();
if (!context.containsBean(viewName)) {
// Allow for ViewResolver chaining...
return null;
}
if (!context.isTypeMatch(viewName, View.class)) {
if (logger.isDebugEnabled()) {
logger.debug("Found bean named '" + viewName + "' but it does not implement View");
}
// Since we're looking into the general ApplicationContext here,
// let's accept this as a non-match and allow for chaining as well...
return null;
}
return context.getBean(viewName, View.class);
}
}
- Bean의 이름으로 매칭하고 View를 반환합니다.
public class InternalResourceViewResolver extends UrlBasedViewResolver {
private static final boolean jstlPresent = ClassUtils.isPresent(
"javax.servlet.jsp.jstl.core.Config", InternalResourceViewResolver.class.getClassLoader());
@Nullable
private Boolean alwaysInclude;
public InternalResourceViewResolver() {
Class<?> viewClass = requiredViewClass();
if (InternalResourceView.class == viewClass && jstlPresent) {
viewClass = JstlView.class;
}
setViewClass(viewClass);
}
public InternalResourceViewResolver(String prefix, String suffix) {
this();
setPrefix(prefix);
setSuffix(suffix);
}
// 중략
@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
InternalResourceView view = (InternalResourceView) super.buildView(viewName);
if (this.alwaysInclude != null) {
view.setAlwaysInclude(this.alwaysInclude);
}
view.setPreventDispatchLoop(true);
return view;
}
}
- InternalResourceView 클래스는 내부에 forward()를 가지고 있습니다.
public class InternalResourceView extends AbstractUrlBasedView {
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 중략
String dispatcherPath = prepareForRendering(request, response);
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
// 중략
rd.forward(request, response);
}
//중략
}
- JSP의 경우 Dispatcher를 찾고 해당 Dispatcher에게 forward()를 진행합니다.
6. View Rendering
Dispatcher의 render 함수를 통해서 데이터가 rendering 됩니다.
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}
7. 응답 (Response)
마지막으로 Filter를 거치고 Client에게 응답이 됩니다.
지금까지 Spring MVC의 프로세스를 하나씩 확인해봤습니다.다음은 Controller 계층에서 @RequestBody, @ModelAttribute 같은 어노테이션을 사용하여 객체를 받을 수 있게하는 ArgumentResolver에 대해서 뜯어보겠습니다.
(직접 클래스들을 뜯어보면서 작성한 글입니다. 틀린 부분이 있다면 지적해주시면 감사합니다.)
'SpringBoot > spring mvc' 카테고리의 다른 글
Springboot MVC 파헤치기(7) @RequestBody, @ResponseBody (0) | 2022.03.15 |
---|---|
Springboot MVC 파헤치기(6) @ModelAttribute (0) | 2022.03.14 |
Springboot MVC 파헤치기(4) @PathVarialbe (0) | 2022.03.02 |
Springboot MVC 파헤치기(3) @RequstMapping (0) | 2022.03.02 |
SpringBoot MVC 파헤치기(1) (0) | 2022.02.28 |
댓글
이 글 공유하기
다른 글
-
Springboot MVC 파헤치기(6) @ModelAttribute
Springboot MVC 파헤치기(6) @ModelAttribute
2022.03.14 -
Springboot MVC 파헤치기(4) @PathVarialbe
Springboot MVC 파헤치기(4) @PathVarialbe
2022.03.02 -
Springboot MVC 파헤치기(3) @RequstMapping
Springboot MVC 파헤치기(3) @RequstMapping
2022.03.02 -
SpringBoot MVC 파헤치기(1)
SpringBoot MVC 파헤치기(1)
2022.02.28