HandlerAdapter가 ModelAndView(모델 + 뷰 이름 또는 View 객체)를 돌려주면, DispatcherServlet.render가 이를 실제 HTTP 응답으로 바꾼다. 두 개의 SPI가 협력한다.
ViewResolver: 논리 뷰 이름("user/list")을View객체로 해석한다. 컨트롤러가 물리적 경로/기술을 모르게 한다.View: 모델 데이터를 받아 실제 콘텐츠(HTML/JSON/PDF...)를 응답 스트림에 쓴다. 상태 없는(stateless) 스레드-세이프 컴포넌트여야 한다.
@ResponseBody/ResponseEntity처럼 핸들러가 응답 본문을 직접 쓴 경우(03장의requestHandled=true)는 이 단계를 건너뛴다. 이 챕터는 "뷰 기반(서버 렌더링)" 응답 경로다.
ViewResolver resolveViewName(name, locale): View
▲
├ AbstractCachingViewResolver 해석 결과 View 캐시
│ ▲
│ UrlBasedViewResolver prefix+name+suffix → 뷰 URL
│ ▲
│ InternalResourceViewResolver (기본) JSP 용, View=InternalResourceView
│ AbstractTemplateViewResolver FreeMarker 등
├ BeanNameViewResolver 뷰 이름 = 빈 이름으로 View 빈 조회
├ ContentNegotiatingViewResolver Accept 헤더로 여러 리졸버 중 선택
└ ViewResolverComposite 여러 리졸버 묶음
View render(model, request, response)
▲
AbstractView 모델 병합·헤더 세팅 공통 → renderMergedOutputModel(추상)
├ InternalResourceView RequestDispatcher.forward → JSP
├ RedirectView sendRedirect / flash 속성 전달
├ AbstractJacksonView JSON/XML 직렬화
└ AbstractTemplateView FreeMarker/Groovy 템플릿
소스: ViewResolver.java, View.java, view/AbstractView.java, view/InternalResourceView.java, view/RedirectView.java, view/ContentNegotiatingViewResolver.java
render(mv, request, response)
│ locale = localeResolver.resolveLocale(request) → response.setLocale
│
│ viewName 이 있으면:
│ view = resolveViewName(viewName, model, locale, request)
│ └ viewResolvers 를 순서대로 돌며 첫 non-null View 반환
│ view == null → ServletException (해석 실패)
│ 아니면:
│ view = mv.getView() 이미 View 객체를 들고 있음
│
│ view 가 SmartView 면 → 중첩 뷰 해석(프래그먼트 등)
│ mv.getStatus() 있으면 → response.setStatus
▼ view.render(mv.getModelInternal(), request, response)
뷰 이름 해석은 02/03장과 같은 "리스트 순회 + 첫 매치" 패턴이다. 여러 ViewResolver가 등록돼 있으면 Ordered 순서대로 resolveViewName을 호출해 처음으로 null이 아닌 View를 받은 곳에서 멈춘다.
public void render(Map<String,?> model, HttpServletRequest request, HttpServletResponse response) {
Map<String,Object> mergedModel = createMergedOutputModel(model, request, response);
// staticAttributes + pathVariables + model 을 한 맵으로 병합
prepareResponse(request, response); // 캐시 헤더 등
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); // 서브클래스
}renderMergedOutputModel이 기술별 실제 렌더링이다.
InternalResourceView(JSP) — 모델을 요청 속성으로 노출(exposeModelAsRequestAttributes)한 뒤RequestDispatcher.forward(...)로 JSP에 위임. 이미 include 중이거나 응답이 커밋됐으면include로 폴백.RedirectView—response.sendRedirect. 모델의 단순 속성을 쿼리스트링으로 붙이고,RedirectAttributes의 flash 속성은FlashMapManager를 통해 다음 요청으로 넘긴다(SmartView라 디스패처가 리다이렉트임을 안다).AbstractJacksonView— 모델(또는 지정 키)을 Jackson으로 JSON/XML 직렬화해 응답 본문에 쓴다.
String "user/list"
→ ViewNameMethodReturnValueHandler (03장) 가 mavContainer 뷰 이름으로 설정
→ ModelAndView(viewName="user/list", model)
→ render → InternalResourceViewResolver.resolveViewName("user/list")
prefix="/WEB-INF/views/" + "user/list" + suffix=".jsp"
→ InternalResourceView("/WEB-INF/views/user/list.jsp") (캐시됨)
→ view.render → 모델을 request attribute 로 → forward to JSP
InternalResourceViewResolver는 AbstractCachingViewResolver를 상속하므로 같은 이름은 한 번만 해석하고 View를 캐시한다(기본 활성).
핸들러가 모델만 채우고 뷰 이름을 안 정하면, DispatcherServlet.applyDefaultViewName이 RequestToViewNameTranslator(기본 DefaultRequestToViewNameTranslator)로 요청 경로에서 뷰 이름을 추론한다(/user/list → user/list).
같은 컨트롤러로 HTML과 JSON을 모두 서빙하고 싶을 때. 이 리졸버는 직접 View를 만들지 않고, 다른 ViewResolver들에게 후보 View를 모은 뒤 요청의 Accept 헤더(또는 확장자/파라미터)로 가장 알맞은 것을 고른다. ContentNegotiationManager로 미디어 타입을 판정하고, 각 후보 View.getContentType()과 매칭한다.
- 논리 이름 ↔ 물리 뷰 분리 : 컨트롤러는
"user/list"만 안다. 경로 프리픽스/서픽스/뷰 기술은ViewResolver설정에 격리돼, 뷰 기술 교체가 컨트롤러에 영향을 주지 않는다. - 캐싱 :
AbstractCachingViewResolver가 이름→View를 캐시. 뷰 인스턴스가 stateless·thread-safe여야 하는 이유다. SmartView:RedirectView처럼 디스패처가 특별 취급해야 하는 뷰의 마커(리다이렉트 여부, 중첩 뷰 해석).render에서isRedirectView/resolveNestedViews를 통해 분기한다.FragmentsRendering(7.x) — HTMX 류를 위한 부분(프래그먼트) 렌더링. 한 응답에 여러 뷰 조각을 조합한다.- 확장 : 새 뷰 기술은
AbstractView(+ 필요하면AbstractUrlBasedView)를 상속하고 대응ViewResolver를 등록하면 된다.WebMvcConfigurer.configureViewResolvers(07장)가 등록 DSL을 제공한다.
ViewResolver는 논리 뷰 이름을 View 객체로 해석하고(여러 개면 순서대로 시도 + 캐시), View는 모델을 받아 실제 응답 콘텐츠를 쓴다. DispatcherServlet.render가 둘을 잇는다 — 로케일 적용 → 뷰 해석 → view.render. JSP(InternalResourceView, forward), 리다이렉트(RedirectView, flash), JSON(AbstractJacksonView), 콘텐츠 협상(ContentNegotiatingViewResolver)이 모두 이 한 인터페이스 쌍 위에 구현된다. REST(@ResponseBody) 응답은 이 단계를 타지 않고 03장 메시지 컨버터가 끝낸다.