티스토리 뷰

Web/Spring

[Spring] ViewResolver의 동작 과정

Jaehee Jeon 2023. 5. 28. 23:57
반응형

스프링의 컨트롤러 메서드는 크게 데이터를 반환하는 RestController와 뷰를 반환하는 일반적인 Controller로 나뉜다.

 

RestController의 반환 값은 HttpMessageConverter에 의해 json과 같은 형식으로 변환되어 전달되고,

Controller의 반환 값은 뷰의 이름으로 간주되어 ViewResolver가 서버 내의 적절한 뷰 파일을 찾아 처리한다.

 

 

초기화

 

ViewResolver를 호출하고 사용하는 클래스는 DispatcherServlet이므로, DispatcherServlet에 대해 간단히 알아야 한다.

ApplicationContext가 초기화된 후에 DispatcherServlet 또한 초기화되는데, 이때 사용될 ViewResolver를 모두 찾아 리스트로 가져온다.

 

초기화는 initViewResolvers()에서 이루어진다.

ViewResolver들은 스프링 컨테이너에 등록되어있으며, 별다른 설정이 없다면 컨테이너에서 ViewResolver의 구현체들, 혹은 "viewResolver"라는 이름을 가진 빈들을 가져온다.

 

만약 어떤 ViewResolver도 존재하지 않는다면 jsp 뷰 렌더링에 사용되는 InternalResourceViewResolver를 등록한다.

 

아래와 같이 등록될 ViewResolver에 대한 정보를 설정할 수 있다.

InternalResourceViewResolver는 뷰 이름에 앞 뒤로 문자열을 붙여 뷰 파일을 찾을 수 있도록 prefix, suffix를 지정 가능한 생성자를 public으로 제공하고 있다.

 

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/views/" />
    <property name="suffix" value=".jsp" />
</bean>

 

 

ViewResolver 호출

 

컨트롤러 메서드에서 View에 대한 정보를 가져온다.

 

DispatcherServlet은 핸들러, 우리의 컨트롤러 메서드의 호출 결과로 ModelAndView를 얻는다.

만약 핸들러가 @ResponseBody가 붙어 통해 데이터를 반환한다면 ModelAndView는 null이 되며,

일반적인 @Controller의 경우 ModelAndView는 뷰에 관한 정보를 담은 객체가 된다.

 

DispatcherServlet의 doDispatch()에서 핸들러를 실행시켜 mv를 얻는다.
호출할 View에 대한 정보를 가진다.

 

 

ViewResolver의 resolveViewName()을 호출한다.

 

DispatcherServlet은 찾아온 ModelAndView를 해당 뷰에 적합한 ViewResolver를 통해 처리한다.

처리되는 큰 흐름은 아래와 같다.

  1. ModelAndView와 포함된 ViewName이 null이 아닌지 확인하고 가져온다.
  2. 보유한 ViewResolver 리스트를 순회하며 뷰의 이름과 로케일을 바탕으로 resolveViewName()을 호출한다.
  3. 얻은 View 객체를 resolve()한다.

 

 

ViewResolver 인터페이스

 

스프링에서는 JSP, Thymeleaf, FreeMarker, React, AngularJS, Vue.js 등 정말 다양한 뷰 엔진을 지원한다.

이는 View와 ViewResolver가 아래와 같이 인터페이스 기반으로 공통 처리를 하기 때문이다.

 

ViewResolver는 뷰의 이름과 로케일(국제화 관련) 정보를 통해 적절한 View 객체를 만든다.

 

public interface ViewResolver {

	@Nullable
	View resolveViewName(String viewName, Locale locale) throws Exception;

}

 

 

ViewResolver의 구현체

 

뷰의 종류에 따라 각각을 위한 ViewResolver의 구현체가 사용된다.

다음은 JSP를 위한 InternalResourceViewResolver와 Thymeleaf를 위한 ThymeleafViewResolver의 계층도이다.

 

JSP를 위한 InternalResourceViewResolver
Thymeleaf를 위한 ThymeleafViewResolver

 

동적 페이지를 렌더링하기 위한 뷰 엔진들은 구현 방식이 각각 다르나 AbstractCachingViewResolver라는 추상클래스를 사용하여 구현되는 모습을 볼 수 있다.

 

이름에서 알 수 있듯, 애플리케이션이 동작될 때 View는 캐싱되며 사용된다.

 

ViewResolver의 Javadoc. View 객체의 상태는 앱이 구동되는 도중 변하지 않으므로 캐싱이 가능함을 명시한다.

 

 

View의 캐싱

 

웹앱 서버의 기본은 요청에 따라 다른 View를 렌더링 하여 보여주는 것이다.

 

하지만 요청이 매우 많고, 각 요청마다 뷰를 새로 그려내야 한다면 서버의 부담이 매우 클 것이다.

 

이를 대응하기 위해 요청으로 그려낼 뷰가 같다면, 캐싱하여 서버의 부하를 낮추는 방식을 사용한다.

 

기본 1024개의 뷰를 캐싱한다

 

AbstractCachingViewResolver를 살펴보면 기본 1024개의 뷰를 캐싱하는 것을 알 수 있다.

이는 기본 설정일 뿐이며 필요에 따라 캐시의 사이즈를 자유롭게 변경할 수 있다.

 

 

캐싱된 뷰는 LinkedHashMap에 저장되며, 캐시가 다 차면 오래된 뷰를 제거하는 것을 볼 수 있다.

 

 

정말로 캐싱을 하는가?

 

캐싱이 이루어지는지 눈으로 확인해보자.

Thymeleaf를 사용한 템플릿과 컨트롤러를 아래와 같이 구성하였다.

 

@GetMapping("/example/{value}")
public String displayPathVariable(@PathVariable String value, Model model) {
    model.addAttribute("value", value);
    return "example"; // View name
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>Example Page</title>
</head>
<body>
<h1>PathVariable: <span th:text="${value}"></span></h1>
</body>
</html>

 

간단히 PathVariable을 하나 받아 아래와 같이 그대로 출력하는 웹앱이다.

 

 

첫 번째 요청에 대한 동작 과정이다.

viewCreationCache에 캐싱된 정보가 null이므로 createView를 진행하게 된다.

 

 

새로운 View 객체를 만들고 이를 반환, 사용자가 볼 수 있게 된다.

 

 

 

이후 다음 요청을 보내는 과정에서는 cache에 이미 저장된 값이 있는 것을 볼 수 있다.

 

viewAccessCache의 크기가 1이다. 뷰의 이름과 Locale을 Key로 가진다.

 

Cache의 Value값인 View 객체에는 렌더링에 사용할 View에 대한 정보가 담겨있다.

 

 

View의 이름과 Locale 정보로 캐싱된 View를 찾는 것을 알 수 있다.

 

 

 

View

 

새로운 뷰 파일과 로케일이라면 View를 만들고, 이미 만들었었다면 캐싱하여 가져온다.

 

하지만 같은 뷰 파일과 로케일을 사용하나 요청 혹은 응답의 세부 내용이 달라진다면 어떻게 캐싱한 것을 사용하는가?

 

이 부분은 View 객체의 render()를 호출할 때 일어난다.

 

 

 

다시 DispatcherServlet으로 돌아가자.

보유한 ViewResolver를 순회한 뒤 뷰 이름과 Locale을 통해 View 객체를 가져왔다.

DispatcherServlet은 곧 자신의 render() 내에서 View 객체의 render()를 호출하여 렌더링 책임을 위임한다.

 

 

View는 역시 인터페이스로, 내부 동작을 보려면 구현체를 살펴보아야 한다.

 

public interface View {
	void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception;
}

 

 

 

예시로 사용 중인 ThymeleafView를 살펴보면 render()와 긴 renderFragment()를 통해 실제 Response가 작성되는 것을 볼 수 있다.

 

 

직접 response에 setXXX()를 통해 값을 입력하는 모습이다.

 

org.thymeleaf의 TemplateEngine의 process()를 호출한다.

 

템플릿에 대한 내용은 org.thymeleaf의 TemplateEngine의 process()를 통해 처리된다.

(템플릿 엔진이 실제로 어떻게 동작하는지는 본 글의 목적과 맞지 않으므로 생략한다.)

여기서 View 객체 자체는 동일한 것을 사용, Model에 담긴 값만이 다르므로 이를 조합하여 response객체를 완성하게 된다.

 

템플릿 엔진에 따라서도 다르겠지만 Thymeleaf의 경우 템플릿의 컴파일 결과를 캐싱하는 프로세스가 추가된다.

Spring 내부에서 View를, Thymeleaf 템플릿 엔진에서 컴파일 결과를 한번 더, 총 두 번의 캐싱 프로세스가 일어나는 것이다.

 


 

이로써 다양한 요청에 대해 적절한 View Resolver를 선택, View 객체를 생성하고 캐싱하며 결과물인 response 객체를 완성하는 과정을 살펴보았다.

Spring은 이 과정을 인터페이스화하여 다양한 템플릿 엔진에서 이 프로세스를 자유롭게 구현하고 있음을 알 수 있다.

반응형