스프링부트 해부학 : RequestFlow(3) - HandlerExceptionResolver, ViewResolver

정윤성·2022년 6월 4일
0

스프링부트 해부학

목록 보기
3/20

역할

HandlerExceptionResolver : 에러에대한 Mapping과 실행 중 예외를 해결할 수 있는 능력을 가진 인터페이스
doDispatch도중 Exception이 존재할경우 해당 Resolver가 실행이 됨 ( Composite패턴으로 구성되어있음 )

ViewResolver : MVC모델중 View타입을 쉽게 반환해주도록 도와주는 인터페이스


Class Diagram

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를 상속받은 내부리소스 접근) 등이 있다


HandlerExceptionResolver

ExceptionHandelrExceptionResolver.java

Bean Initializer

@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이 저장이 되어 처리된다

ResolveException

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에서의 처리방식과 일치한다

ViewResolver

대표적으로 사용되는 Media에 따른 처리방식에 대해서만 알아보자

ResolveViewName

ContentNegotiatingViewResolver.java

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한다

AbstractCachingViewResolver.java

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를 진행한다

UrlbaseViewResolver.java

@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. ExceptionHandler와 @ContollerAdvice의 처리방식은 dispatch에서 Exception이 발생했을때 Handler을 진행하며 이 후 Adapter의 방식대로 처리를 해 ModelAndView를 받아온다
  2. 다양한 ViewResolver 전략들이 있엇는데 ContentNegotiatingViewResolver를 통해 여러 ViewResolver들에게 처리를 위임시켜 Candidate들을 모아 최적의 View를 선정시킨다 ( 이때 다양한 ViewResolver전략들이 있다 )

마무리

1편부터 3편까지 Dispatch, RequestMapping, Interceptor, RequestAdapter, ExceptionHandler, ViewResolver의 초기화와 처리방식 그리고 전체적인 흐름에 대해서 알아봤습니다

이 부분은 실제 Request가 왔을 때 SpringBoot에서 어떻게 Controller에게 처리를 위임시키고 어떻게 Response를 보내주는지에 대한 처리 과정이였고요

다음편부턴 Bean의 초기화와 관리, 의존성 주입을 해주는 '스프링 컨테이너(IoC)'에 대해서 알아볼 예정입니다

profile
게으른 개발자

0개의 댓글