ExceptionHandler 에서 @CookieValue 를 사용할 수 없는 이유

0_0_yoon·2022년 9월 17일
2

문제상황


RefreshToken 이 유효하지 않은 경우 예외를 던지고 쿠키가 삭제되도록 구현했다.
Controller 에서 사용했던 것 처럼 @CookieValue 를 사용해 쿠키를 인자로 받으려고 했지만 해당 테스트가 계속 실패했다.(해당 ExceptionHandler가 동작하지 않았다)

	@ExceptionHandler(RefreshTokenNotFoundException.class)
    public ResponseEntity<ExceptionResponse> handleRefreshTokenNotFoundException(final UnauthorizedException e,
    																		     final HttpServletResponse response,
                                                                                 @CookieValue("refreshToken") final Cookie cookie) {
        cookie.setMaxAge(0);
        response.addCookie(cookie);
        return handleUnauthorizedException(e);
    }

원인

Controller 와 다르게 ExceptionHandler 에서는 @CookieValue를 지원하는 ServletCookieValueMethodArgumentResolver 를 가지고 있지 않다. 그래서 Cookie 로 바인딩하려고 할 때 처리할 ArgumentResolver 를 찾을 수 없어서 테스트가 실패했던 것이다.(아래 사진 참고)


아래 코드와 같이 ExceptionHandler 는 Controller 처럼 다양한 argumentResolver 들을 지원해 주지 않는다.

// ExceptionHandlerExceptionResolver
protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
		List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

		// Annotation-based argument resolution
		resolvers.add(new SessionAttributeMethodArgumentResolver());
		resolvers.add(new RequestAttributeMethodArgumentResolver());

		// Type-based argument resolution
		resolvers.add(new ServletRequestMethodArgumentResolver());
		resolvers.add(new ServletResponseMethodArgumentResolver());
		resolvers.add(new RedirectAttributesMethodArgumentResolver());
		resolvers.add(new ModelMethodProcessor());

		// Custom arguments
		if (getCustomArgumentResolvers() != null) {
			resolvers.addAll(getCustomArgumentResolvers());
		}

		// Catch-all
		resolvers.add(new PrincipalMethodArgumentResolver());

		return resolvers;
	}
// RequestMappingHandlerAdapter
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
	List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);

	// Annotation-based argument resolution
	resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
	resolvers.add(new RequestParamMapMethodArgumentResolver());
	resolvers.add(new PathVariableMethodArgumentResolver());
	resolvers.add(new PathVariableMapMethodArgumentResolver());
	resolvers.add(new MatrixVariableMethodArgumentResolver());
	resolvers.add(new MatrixVariableMapMethodArgumentResolver());
	resolvers.add(new ServletModelAttributeMethodProcessor(false));
	resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
	resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
	resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
	resolvers.add(new RequestHeaderMapMethodArgumentResolver());
	resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
	resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
	resolvers.add(new SessionAttributeMethodArgumentResolver());
	resolvers.add(new RequestAttributeMethodArgumentResolver());

	// Type-based argument resolution
	resolvers.add(new ServletRequestMethodArgumentResolver());
	resolvers.add(new ServletResponseMethodArgumentResolver());
	resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
	resolvers.add(new RedirectAttributesMethodArgumentResolver());
	resolvers.add(new ModelMethodProcessor());
	resolvers.add(new MapMethodProcessor());
	resolvers.add(new ErrorsMethodArgumentResolver());
	resolvers.add(new SessionStatusMethodArgumentResolver());
	resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
	if (KotlinDetector.isKotlinPresent()) {
		resolvers.add(new ContinuationHandlerMethodArgumentResolver());
	}
    
	// Custom arguments
	if (getCustomArgumentResolvers() != null) {
		resolvers.addAll(getCustomArgumentResolvers());
	}

	// Catch-all
	resolvers.add(new PrincipalMethodArgumentResolver());
	resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
	resolvers.add(new ServletModelAttributeMethodProcessor(true));

	return resolvers;
}

해결

@CookieValue 대신 스프링에서 지원하는 WebUtils 를 사용해서 쿠키값을 가져왔다.

	@ExceptionHandler(RefreshTokenNotFoundException.class)
    public ResponseEntity<ExceptionResponse> handleRefreshTokenNotFoundException(final UnauthorizedException e,
    																			 final HttpServletRequest request,
                                    											 final HttpServletResponse response) {
		final Cookie cookie = WebUtils.getCookie(request, "refreshToken");
        cookie.setMaxAge(0);
        response.addCookie(cookie);
        return handleUnauthorizedException(e);
    }

추가

ExceptionHandler 와 같이 쓰이는 ControllerAdvice 는 어디서 동작할까?
스프링에서 예외가 발생하면(Dispatcher Servlet 이 Handler 를 찾고 실행시키는 과정에서 예외가 발생하는 경우) HandlerExceptionResolver 가 예외를 처리한다.

아래 사진과 같이 HandlerExceptionResolver 중에서 HandlerExceptionResolverComposite 안에 있는 ExceptionHandlerAdviceCache 에 있다.

정리하면 HandlerExceptionResolvers(ArrayList) 는 DefaultErrorAttributes 와 HandlerExceptionResolverComposite(ArrayList)을 가지고 있다. 이 중 HandlerExceptionResolverComposite 안에 있는 Resolver(ExceptionHandlerExceptionResolver) 중 ExceptionHandlerAdviceCache(LinkedHashMap)에 들어있다.

profile
꾸준하게 쌓아가자

0개의 댓글