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에 대해서 뜯어보겠습니다. 

(직접 클래스들을 뜯어보면서 작성한 글입니다. 틀린 부분이 있다면 지적해주시면 감사합니다.)