MVC 패턴을 지원하기 위해 만들어진 Spring 웹 모듈.
( MVC 패턴이란, Model View Controller 의 요소를 이용하여, 비지니스 로직과 화면을 분리한 소프트웨어 디자인 패턴인다 )
Spring MVC 는 사용자의 다양한 Http Request 처리를 지원하며, MVC 패턴 구현을 위해 'DispatcherServlet' 'ModelAndView' 'ViewResolver' 'View' 'Handler' 등의 컴포넌트를 지원한다.
Spring MVC 에 관한 자세한 내용을 다음 공식 문서를 확인해보자.
현 문서는 MVC 구성 요소 중 'View' 에 대해 정리한다.
view 는 단어 뜻 그대로 '보이는 것' 을 담당한다. 즉 데이터가 보여지는 방식에 책임이 있다.
view 는 최종적으론 HttpServletResponse
에 데이터를 적재하고, 헤더를 기록하는 등의 작업을 하게된다. 이는 이후 코드로 확인하자.
View 가 동작하는 것을 확인하기 전에 HandlerAdapter
와 ModelAndView
, 그리고 ViewResolver
가 대략적으로 동작하는 방식을 살펴보자.
이는, data 가 view 에 도달하는 방식과 밀접한 관련이 있다.
그림을 보자. SpringMVC 의 간략화된 구조이다.
(내가 그린건 아니다. velog 의 블로거에게 축복을.)
data 는 1의 요청을 타고와서 3으로 흘러들어갈 수도 있고, 요청에 의해 Controller에서 생성될 수도 있다.
어떻게 data가 생성되던 Controller
는 data와 view 를 감싼 ModelAndView
라는 객체를 DispatcherServlet
에 반환하게 된다 (정확히 말하면 HandlerAdapter
에서 반환됨).
ModelAndView
에서 view
라는 이름의 멤버는 Object
타입으로 되어있다는 것이다. 즉, 진짜 View를 때려넣을 수도 있고, View_name 만 넘겨서 view resolver에게 view를 찾는 책임을 전가 할 수도 있다.// org.springframework.web.servlet.ModelAndView
public class ModelAndView {
/** View instance or view name String. */
@Nullable
🎉 private Object view;
...
}
++ 살짝 먼저 보여주자면, ModelAndView
에 View 가 있는지, ViewName이 있는지에 따라 ViewResolver
를 타는지 여부가 나뉜다
// org.springframework.web.servlet.DispatcherServlet
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
...
View view;
String viewName = mv.getViewName();
if (viewName != null) {
🎉 view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
...
}
else {
🎉 view = mv.getView();
...
}
...
}
ModelAndView
를 반환받은 DispatcherServlet
은 몇 가지 과정 후에 render
메서드를 동작한다 (바로 위에서 보여준 함수).
( 정확히는 doDispatch
→ processDispatchResult
→ render
스택으로 호출된다 )
render
메서드에선 필요할 때 resolveViewName
메서드를 호출하는데, 이 메서드에서 ViewResolver
를 호출하고, View
를 반환한다.
// org.springframework.web.wervlet.DispatcherServletprotected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
🎉 for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
DispatcherServlet
init
시, 존재하는 모든 view resolver 를 불러와 저장한다.
resolveViewName
의 for 문을 보면, 모든 view resolver의 resolveViewName
메서드를 호출해서 View 를 반환하는지 안하는지 체크한다.
( 스프링에서 이런 코드 볼 때 마다 재밌다. 수면 아래서 열심히 헤엄치는 백조가 생각난다. )
그래서 아무튼 view resolver에서 제대로된 View
가 반환되면 이를 render
함수에서 사용할 수 있도록 반환한다.
자, 이제 View
의 render
함수를 호출할 차례다.
Dispatcher 서블릿의 render
함수에서 View
를 받아왔으면, 이제 정말 render 해야한다.
이 단계는 DispatcherServlet
이 아닌 View 객체에 책임을 위임하여 처리하게 된다 (Delegation).
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
...
View view;
...
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
🎉 view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
...
throw ex;
}
}
View
는 인터페이스다. 따라서 구현체마다 render
함수의 구현이 다르다.
( 나중에 직접 View
를 정의해야할 일이 있으므로, 위 사실은 중요한 열쇠가 된다. )
Spring 엔 여러 ViewResolver
가 등록되어있고, 각 resolver 마다 반환하는 View
구현체가 다르니 잘 찾아보길 바란다.
난 예시로 HtmlResourceView
코드를 살펴보았다. 생각보다 간단한 코드이다. (쓰면서 알았는데, 이거 뷰는 에러 화면 띄울 때 사용하나보다...)
//org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
...
private static class HtmlResourceView implements View {
private Resource resource;
HtmlResourceView(Resource resource) {
this.resource = resource;
}
@Override
public String getContentType() {
return MediaType.TEXT_HTML_VALUE;
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
🎉 response.setContentType(getContentType());
🎉 FileCopyUtils.copy(this.resource.getInputStream(), response.getOutputStream());
}
}
}
DefaultErrorViewResolver
가 반환하는 View 로서, 서브 클래스로 선언되어있다. render 메서드의 길이가 매우 짧다.
🎉 reposne.setContentType(getContentType())
View
가 결국엔 응답에 데이터를 싣는 역할을 한다는것이 드러난 순간이다...!
( 시간나면 DefulatErrorViewResolver
도 꼭 살펴보자 )
ServletResponse
에 Data
를 싣는 방법이다.간단하게 말하면,
1. Binary Data - getOutputStream으로 가져온 ServletOuputStream을 받아 담는다 (MIME type..?)
2. Character Data - getWriter로 가져온 printWriter에 담는다
잘 이해가 안가는 부분이지만, 이렇게 구분된다는 사실만 일단 기억하자. Http 응답 구조에 대해 더 공부하고 정리하면 다시 돌아보도록 하자.
이렇게 View 의 동작에 대해 알아보았다.