Skip to content

Latest commit

 

History

History
109 lines (82 loc) · 7.17 KB

File metadata and controls

109 lines (82 loc) · 7.17 KB

04. View / ViewResolver — 모델을 응답으로 렌더링하기

무엇을 / 왜

HandlerAdapterModelAndView(모델 + 뷰 이름 또는 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

동작 흐름: DispatcherServlet.render

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를 받은 곳에서 멈춘다.

AbstractView.render — 공통 렌더링 골격

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로 폴백.
  • RedirectViewresponse.sendRedirect. 모델의 단순 속성을 쿼리스트링으로 붙이고, RedirectAttributes의 flash 속성은 FlashMapManager를 통해 다음 요청으로 넘긴다(SmartView라 디스패처가 리다이렉트임을 안다).
  • AbstractJacksonView — 모델(또는 지정 키)을 Jackson으로 JSON/XML 직렬화해 응답 본문에 쓴다.

핵심 메서드 / 시나리오

시나리오 A: 컨트롤러가 "user/list" 반환 (JSP)

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

InternalResourceViewResolverAbstractCachingViewResolver를 상속하므로 같은 이름은 한 번만 해석하고 View를 캐시한다(기본 활성).

시나리오 B: 뷰 이름 미지정

핸들러가 모델만 채우고 뷰 이름을 안 정하면, DispatcherServlet.applyDefaultViewNameRequestToViewNameTranslator(기본 DefaultRequestToViewNameTranslator)로 요청 경로에서 뷰 이름을 추론한다(/user/listuser/list).

시나리오 C: ContentNegotiatingViewResolver

같은 컨트롤러로 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장 메시지 컨버터가 끝낸다.