Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] fix: 핸들러 메서드가 매핑되지 않고, 인터셉터가 적용되는 경로에 접근 시 예외 핸들링이 되지 않아 500 예외가 발생하는 버그 수정 (#829) #833

Merged
merged 2 commits into from
Apr 3, 2024

Conversation

seokjin8678
Copy link
Collaborator

@seokjin8678 seokjin8678 commented Apr 2, 2024

📌 관련 이슈

✨ PR 세부 내용

제목이 조금 길긴 한데, 제목 그대로 핸들러 메서드가 매핑되지 않고, 인터셉터가 적용되는 경로에 접근 시 예외 핸들링이 되지 않아 500 예외가 발생하는 버그를 수정했습니다.

해결 방법은 매우 간단한데, 그저 프로퍼티에 다음과 같은 설정을 추가하면 됩니다.

spring:
  mvc:
    throw-exception-if-no-handler-found: true
  web:
    resources:
      add-mappings: false

그저 이 설정만 추가하면 문제가 해결되니 왜 이 문제가 발생했는지 분석 결과, DispatcherServlet의 심연을 탐구하고 말았습니다..

우선 결론부터 말하자면 경로에 매핑된 핸들러가 null 또는 HandlerMethod 일때만 ExceptionHandler로 예외 처리가 됩니다.

그렇다는 말은 예외가 발생한 시점의 매핑된 핸들러가 null이 아닌 경우인데, 당연하게 HandlerMethod는 아닙니다.

해당 핸들러의 정체는 바로 ResourceHttpRequestHandler 인데, 이는 등록된 HandlerMapping 중 SimpleUrlHandlerMapping 때문입니다.

해당 SimpleUrlHandlerMapping를 타고 들어가면 상속하고 있는 AbstractUrlHandlerMapping를 볼 수 있는데, 해당 클래스에 선언된 필드인 pathPatternHandlerMap으로 경로에 대해 패턴을 매칭시킨 뒤, 패턴에 맞으면 ResourceHttpRequestHandler를 반환합니다.

바로 이 때문에 경로가 HandlerMethod로 매핑되지 않아도, ResourceHttpRequestHandler로 매핑되는 이유입니다.

더 정확하게 pathPatternHandlerMap의 값을 보면 다음과 같이 /**로 시작하는 패턴이 있기 때문에 무조건 ResourceHttpRequestHandler 핸들러가 매핑될 수 밖에 없습니다. 😂

Pasted image 20240402231237

이는 spring.web.resources.add-mappings: false 설정을 추가하여 기본으로 등록되는 PathPattern을 없앨 수 있습니다.

이렇게 되면 정적 경로에 대한 자원이 매핑되지 않으므로, static 폴더에 있는 파일을 제공할 수 없지만, 저희는 현재 정적 파일을 제공하고 있지 않기 때문에 문제가 되지 않습니다.

그러면 HandlerMethod에 등록된 경로가 아니면 핸들러가 null이 반환 되기에 404 응답이 내려옵니다.

근데 문제는 이 방법만 사용해서는 404 응답을 커스텀 하기가 불가능 합니다. (Whitelabel Error Page가 나옵니다)

따라서 spring.mvc.throw-exception-if-no-handler-found: true 설정을 추가해, 예외가 발생되게 하여 ControllerAdvice에서 예외를 잡게하여 저희가 원하는 응답으로 커스텀 할 수 있습니다.


(장문 주의)

여기까지만 보면, 그렇구나 할 수 있겠지만 매핑된 핸들러가 null인데, 왜 ControllerAdvice에서 예외가 잡히는지 궁금하여 심연을 더 파보았습니다.

예외 처리는 DispatcherServlet의 processHandlerException() 메서드에서 처리되는데, List<HandlerExceptionResolver> 필드를 반복문을 돌려 예외를 처리합니다.

@Nullable  
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,  
       @Nullable Object handler, Exception ex) throws Exception {  
  
    // Success and error responses may use different content types  
    request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);  
  
    // Check registered HandlerExceptionResolvers...  
    ModelAndView exMv = null;  
    if (this.handlerExceptionResolvers != null) {  
       for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {  
          exMv = resolver.resolveException(request, response, handler, ex);  
          if (exMv != null) {  
             break;  
          }  
       }  
    }  
    if (exMv != null) {
        ...
    }
    throw ex;
}

여기서 핸들러의 타입마다 반환되는 exMv 변수를 확인해보면 다음과 같습니다.

HandlerMethod
Pasted image 20240402214038

ResourceHttpRequestHandler
Pasted image 20240402214116

null
Pasted image 20240403024040

ResourceHttpRequestHandler의 경우 null이 할당되어 예외 처리할 수 없어, 예외를 던지게 되고, 더 이상 예외는 처리되지 못하여 톰캣까지 타고 흘러가게 됩니다.

이게 해당 이슈가 발생한 이유입니다...!

그리고 핸들러가 null 일 때 예외가 처리되는 이유는 HandlerExceptionResolverHandlerExceptionResolverComposite 타입일 때 처리되는데, 해당 클래스에서 위와 똑같이 예외를 핸들링하고 있습니다. (이름 그대로 Composite 디자인 패턴을 구현했습니다)

public class HandlerExceptionResolverComposite implements HandlerExceptionResolver, Ordered {
    @Nullable  
    private List<HandlerExceptionResolver> resolvers;

    @Override  
    @Nullable  
    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);  
            if (mav != null) {  
               return mav;  
            }  
         }  
      }  
      return null;  
    }
}

또 여기서 resolvers를 반복문 돌리며 예외를 처리하는데, resolvers는 다음과 같이 3개의 HandlerExceptionResolver를 가지고 있습니다.

Pasted image 20240402222926

여기서 ExceptionHandlerExceptionResolver 이 녀석이 바로 ControllerAdvice를 통해 등록한 ExceptionHandler를 가지고 예외를 핸들링 하는 녀석입니다.

(호출하는 ExceptionHandlerMethodResolvermappedMethods 필드에 있습니다)
Pasted image 20240403040054

그 뒤 설명하기엔 너무 길어질 것 같아.. 이 정도만 하겠습니다. 😂

자세한 건 ExceptionHandlerExceptionResolver 클래스의 doResolveHandlerMethodException() 메서드를 직접 살펴보는 것이 빠를 것 같네요.

(핸들러에 따라 예외가 처리되는 분기는 AbstractHandlerMethodExceptionResolvershouldApplyTo() 메서드를 통해 이뤄집니다)

public abstract class AbstractHandlerMethodExceptionResolver extends AbstractHandlerExceptionResolver {
    ...
    @Override  
    protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {  
        if (handler == null) {  
           return super.shouldApplyTo(request, null);  
        }  
        else if (handler instanceof HandlerMethod handlerMethod) {  
           handler = handlerMethod.getBean();  
           return super.shouldApplyTo(request, handler);  
        }  
        else if (hasGlobalExceptionHandlers() && hasHandlerMappings()) {  
           return super.shouldApplyTo(request, handler);  
        }  
        else {  
           return false;  
        }  
    }
    ...
}

그래서 정리하면 다음과 같습니다.

매핑된 handler가 null 또는 HandlerMethod 일 때, 발생한 예외가 ControllerAdvice의 Exception Handler에 등록되었으면 처리된다.

하지만 매핑된 handler가 ResourceHttpRequestHandler일 때, 발생된 예외가 Exception Handler에 등록 되었어도, 예외를 처리할 수 없어 500 에러가 발생한다.

추가로 다음과 같은 로그 프로퍼티도 추가했습니다.

logging:
  level:
      springframework:
        web:
          servlet:
            PageNotFound: off

이유는 noHandlerFound() 메서드가 호출될 때 WARN 로그가 발생하기 때문입니다. 😂
(대체 왜 INFO 또는 DEBUG로 하지 않았는지 모르겠네요)

protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (pageNotFoundLogger.isWarnEnabled()) {
	    pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request));
    }
    if (this.throwExceptionIfNoHandlerFound) {
	    throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
			    new ServletServerHttpRequest(request).getHeaders());
    }
    else {
	    response.sendError(HttpServletResponse.SC_NOT_FOUND);
    }
}

- spring.mvc.throw-exception-if-no-handler-found: true
- spring.web.resources.add-mappings: false
@seokjin8678 seokjin8678 added BE 백엔드에 관련된 작업 🛠 수정 수정에 관련된 작업 labels Apr 2, 2024
@seokjin8678 seokjin8678 self-assigned this Apr 2, 2024
@seokjin8678 seokjin8678 changed the title [BE] fix: 핸들러 메서드가 매핑되지 않고, 인터셉터가 적용되는 경로에 접근 시 예외 핸들링이 되지 않아 500 예외가 발생하는 버그 수정 [BE] fix: 핸들러 메서드가 매핑되지 않고, 인터셉터가 적용되는 경로에 접근 시 예외 핸들링이 되지 않아 500 예외가 발생하는 버그 수정 (#829) Apr 2, 2024
@github-actions github-actions bot requested review from BGuga, carsago and xxeol2 April 2, 2024 20:34
Copy link

github-actions bot commented Apr 2, 2024

Test Results

164 files  164 suites   32s ⏱️
588 tests 588 ✅ 0 💤 0 ❌
601 runs  601 ✅ 0 💤 0 ❌

Results for commit 51ee05a.

@seokjin8678 seokjin8678 merged commit 13aea6a into dev Apr 3, 2024
9 checks passed
@seokjin8678 seokjin8678 deleted the feat/#829 branch April 3, 2024 09:45
@seokjin8678
Copy link
Collaborator Author

슬랙에 자꾸 알람이 와서 해당 PR 우선 머지하겠습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BE 백엔드에 관련된 작업 🛠 수정 수정에 관련된 작업
Projects
None yet
1 participant