HandlerExceptionResolver : 에러에대한 Mapping과 실행 중 예외를 해결할 수 있는 능력을 가진 인터페이스
doDispatch도중 Exception이 존재할경우 해당 Resolver가 실행이 됨 ( Composite패턴으로 구성되어있음 )
ViewResolver : MVC모델중 View타입을 쉽게 반환해주도록 도와주는 인터페이스
ApplicationContextAware에의해 Context에 접근이 가능하며 IntilizingBean을 구현해 이 또한 Bean에 등록이 된다
간단하게 처리 핵심 부분부터 먼저 확인해보면
doDispatch -> HandlerExceptionResolverComposite(Composite패턴).resolveException -> 등록된 다양한 HandlerExceptionResolver의 구현체
이와같은 순서로 진행됩니다
ContentNegotiatingViewResolver : MediaType을 확인후 해당확장자에 맞게 ViewName을 Mapping한 뒤 다른 Resolver에게 처리를 위임한다
UrlBasedViewResolver : redirect, foward를 구분하고 이도저도아니면 그냥 View를 생성한뒤 Return한다
AbstractCachingViewResolver : 생성된 View에 대해선 Caching을 해둔다
이외에도 ResourceBundleViewResolver(Resource자원접근), InternalResourceViewResolver(UrlBasedViewResolver를 상속받은 내부리소스 접근) 등이 있다
@Override
public void afterPropertiesSet() {
initExceptionHandlerAdviceCache();
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
...
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
}
Bean이 초기화 될 때 ControllerAdviceBean을 찾고 존재하면 해당 ExceptionMapping에 대한 내용을 불러온 뒤 ArgumentResolver와 ReturnValueHandler까지 주입한걸 볼 수 있다
실제 디버깅을 해보면 Bean이 초기화되면서 정상적으로 ControllerAdvice와 그 안의 메서드들이 포함된걸 확인할 수 있다
실제 처리될때는 ExceptionHandlerExceptionResolver에 Map형태로 저장된다
Controller내에 포함된 @ExceptionHandler같은 경우는
위와같이 exceptionHandlerCache이 저장이 되어 처리된다
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
}
1편 DispatchServlet의 내용을 토대로 따라가다보면 processHandlerException이 있는데 이 중 resolveException을 통해 Exception을 Handling한다
우리가 작업했던 ExceptionHandler들은 HandlerExceptionResolverComposite를 통해 Composite패턴으로 묶여 여러개가 실행된다
HandlerExceptionResolverComposite.class
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
if (this.resolvers != null) {
for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
...
}
}
}
AbstractHandlerExceptionResolver.class
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
if (shouldApplyTo(request, handler)) {
...
ModelAndView result = doResolveException(request, response, handler, ex);
}
}
AbstractHandlerMethodExceptionResolver.class
protected final ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
HandlerMethod handlerMethod = (handler instanceof HandlerMethod ? (HandlerMethod) handler : null);
return doResolveHandlerMethodException(request, response, handlerMethod, ex);
}
ExceptionHandlerExceptionResolver.class
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
...
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
...
}
위의 흐름대로 Composite에서 실행 -> 계속 하위클래스로 구현을 위임시킴의 구조로 진행되는걸 알 수 있다
결국 마지막의 처리는 ExceptionHandlerExceptionResolver가 진행하고 이부분은 전편 HandleAdapter에서의 처리방식과 일치한다
대표적으로 사용되는 Media에 따른 처리방식에 대해서만 알아보자
public View resolveViewName(String viewName, Locale locale) throws Exception {
...
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
...
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
...
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
return bestView;
...
}
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
for (MediaType requestedMediaType : requestedMediaTypes) {
List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
for (String extension : extensions) {
String viewNameWithExtension = viewName + '.' + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
}
DispatchServlet으로부터 resolveViewName이 호출되면 ContentNegotiatingViewResolver가 전달받아 MediaType과 CandidateView를 불러온다음 최적의 View를 찾아 리턴한다
이때 CandidateView를 불러오는 과정에선 viewNameWithExtension을통해 확장자를 지정하고 이에대해 한번더 resolveViewName으로 검사를해 통과하면 add한다
Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
if (!isCache()) {
return createView(viewName, locale);
}
else {
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
}
}
DEFAULT_CACHE_LIMIT값이 0보다 클경우 Cache를 허용하며 위의방식처럼 Cache를 진행한다
@Override
protected View createView(String viewName, Locale locale) throws Exception {
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
...
return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
}
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
InternalResourceView view = new InternalResourceView(forwardUrl);
return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
}
return super.createView(viewName, locale);
}
실제 foward, redirect 접두사를 앞에붙이면 해당 방식에 따라 처리되는 부분이 있는데 위의 방식대로 처리된다
1편부터 3편까지 Dispatch, RequestMapping, Interceptor, RequestAdapter, ExceptionHandler, ViewResolver의 초기화와 처리방식 그리고 전체적인 흐름에 대해서 알아봤습니다
이 부분은 실제 Request가 왔을 때 SpringBoot에서 어떻게 Controller에게 처리를 위임시키고 어떻게 Response를 보내주는지에 대한 처리 과정이였고요
다음편부턴 Bean의 초기화와 관리, 의존성 주입을 해주는 '스프링 컨테이너(IoC)'에 대해서 알아볼 예정입니다