[Spring] PreFlightHandler ClassCastException으로 알아보는 HandlerMapping 동작 구조

Loopy·2023년 4월 23일
1

삽질기록

목록 보기
17/28
post-thumbnail

☁️ 문제 상황

스프링 시큐리티 없이 구현중이므로, 필터가 아닌 스프링 MVC에서 제공해주는 WebMvcConfigurer에서 corsRegistry 를 통해 CORS를 설정해주었다.

@Configuration
class InterceptorConfig(
        private val authenticationInterceptor: AuthenticationInterceptor
): WebMvcConfigurer {
    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(authenticationInterceptor)
                 .addPathPatterns("/**")
                 .excludePathPatterns("/oauth/**")
     }

     override fun addCorsMappings(registry: CorsRegistry) {
         registry.addMapping("/**")
             .allowedOrigins("http://localhost:3000")
             .allowedMethods("GET", "POST", "PUT", "PATCH", "OPTIONS", "DELETE")
             .allowedHeaders("*")
             .allowCredentials(true)
             .maxAge(3600)
     }
}

그런데 처음 보는 캐스팅 에러가 터졌다.

Caused by: java.lang.ClassCastException: class org.springframework.web.servlet.handler.AbstractHandlerMappingPreFlightHandlercannotbecasttoclassorg.springframework.web.method.HandlerMethod(org.springframework.web.servlet.handler.AbstractHandlerMappingPreFlightHandler cannot be cast to class org.springframework.web.method.HandlerMethod (org.springframework.web.servlet.handler.AbstractHandlerMappingPreFlightHandler and org.springframework.web.method.HandlerMethod are in unnamed module of loader org.springframework.boot.loader.LaunchedURLClassLoader @5ce65a89)

org.springframework.web.servlet.handler.AbstractHandlerMapping.PreFlightHandle 에서 org.springframework.web.method.HandlerMethod 로 캐스팅이 불가능하다는 소리인데, 현재 JWT 인증 인터셉터에서는 @Authentication 이 붙어있는 메서드만 인증을 체크하기 위해 HandlerMethod 를 이용하여 어노테이션 정보를 확인하고 있었다.

해당 오류를 보고 두 가지 의문점이 들었다.

  1. LoginController를 실행했는데 PreflightHandler 라는 handler 가 우리 인터셉터에 들어왔을까?
  2. HandlerMethod로 캐스팅이 안되는 상황인데, 가능한 경우는 언제지?

일단 코드를 분석해보기 전에 대략 인터셉터 개념에 대해 생각해보았을 때 인터셉터 체인이라는 것이 있기 때문에, preflight 요청에 처리되는PreflightHandlerHandler Apdater 에서 실행 되기 전에 우리가 정의해놓은 AuthenticationInterceptor 를 거치게 되면서 문제가 발생할 것이라 예상했다.

문제 해결

문제 해결을 생각보다 간단하다! HandlerMethod 을 상속받은 클래스가 아닌 경우 인터셉터 로직을 실행시키지 않도록 바로 true 를 반환해서 통과시켜주면 된다.

override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
     if (handler !is HandlerMethod) return true
     handler.getMethodAnnotation(Authentication::class.java) ?: return true
}

그렇다면 PreflightHandler 란 무엇이며, 왜 HandlerMethod 로 캐스팅이 될 수 없는지 전체 동작 과정을 살펴봄으로써 이해해보도록 하자.

☁️ CORS

CORS 정책과 동작 과정에 대해 알아보기 포스팅에서 정리를 해두었지만, 우선
OPTIONS 메서드를 사용하여 요청을 보내도 되는지 확인하는 Preflight 요청이 발생한다.

스프링은 다음의 두 가지 클래스를 사용하여, CORS 정책을 설정하고 있다. 그리고 해당 두 가지 클래스에서는 공통적으로 CorsProcess.processRequest 를 호출한다.

  1. PreFlightHandler : CORS 예비 요청을 처리하는 핸들러이다.
  2. CorsInterceptor : 예비 요청 이후 본 요청을 처리하는 인터셉터이다.

그렇다면 전체 과정을 보기 전에, HandlerExecutionChain 란 무엇인지 먼저 이해를 하고 넘어가야 한다.

☁️ HandlerExecutionChain

public class HandlerExecutionChain {

	private final Object handler;

	private final List<HandlerInterceptor> interceptorList = new ArrayList<>();

}

왜 굳이 HandlerMethod 을 한번 더 감싸서 보내는 것일까? 바로 컨트롤러로 요청을 넘겨주기 전에 처리해야 하는 인터셉터 등을 포함하기 위해서이다.

HandlerExecutionChain 은 실제로 1. 호출된 핸들러에 대한 참조2. 핸들러 실행 전후에 수행될 핸들러 인터셉터도 참조하고 있다. 즉, 핸들러는 항상 HandlerExecutionChain 인스턴스에 포함되어 실행되는 것이다.

HandlerMapping의 동작 과정

가장 앞단에 존재하는 DispatcherServlet 부터 차근차근 봐보자.

  1. DispatcherServlet 에서 핸들러를 조회하는 작업이 일어나고, 이 때 HandlerMapping 리스트에서 HandlerExecutionChain을 얻으려고 한다.

  1. AbstractHandlerMapping.getHandler 에서는 먼저 핸들러를 얻고, 해당 핸들러를 담은 HandlerExecutionChain 을 생성하는 작업이 일어난다.

가장 우선으로 어노테이션 기반의 RequestMappingHandlerMapping 과 같은 핸들러 매핑 구현체들에서 핸들러를 찾아서 가져온다. 이후 ApplicationContext 에서 핸들러 이름으로 빈을 조회하여 핸들러 객체와 HandlerExecutionChain 을 생성한다.

이때 CORS 요청이라면, CorsHandlerExecutionChain 을 새롭게 생성한 후 덮어씌워서 반환한다.

  1. 예비 요청이라면, PreflightHandler 객체와 인터셉터를 담은 HandlerExecutionChain새롭게 생성해서 반환한다. 하지만 본 요청이라면, 인자로 받은 기존 컨트롤러 요청에 대한 HandlerExecutionChain 의 인터셉터 체인에 CorsInterceptor 만 생성에서 가장 맨 앞에 등록한다.

  1. HandlerExecutionChain을 바탕으로 HandlerAdapter에서 실제 핸들러를 실행하게 된다.

☁️ PreFlightHandler

PreFlightHandler에서는 DefaultCorsProcessor 에서 우리가 설정했던 cors 정보들을 가져와서, 비교하는 작업이 일어난다.

그리고 예비 요청에 대한 응답(Preflight Response)으로 현재 자신이 어떤 것들을 허용하고, 어떤 것들을 금지하고 있는지에 대한 정보를 보내주기 위해 응답 헤더에 담는 작업이 수행된다.

🔖 CorsRegistry
URL 패턴 기반으로, CorsConfiguration 매핑의 설정 정보를 전역적으로 등록하는 방법이다. 내부에 패턴 정보와 CorsConfiguration 을 가지고 있다.

☁️ CorsInterceptor

앞서서 살펴보았을 때, CorsInterceptor 를 인터셉터 체인에서 가장 먼저 수행될 수 있도록 첫번째에 추가해주었다.

마찬가지로 processRequest 작업이 수행되지만, 아래 로직을 보면 알 수 있듯이 내부에서 예비 요청이 아닌 경우는 true 값을 반환해준다. 따라서 CORS 요청이 허용되고 다음 인터셉터로 넘어갈 수 있게 되는 것이다.

☁️ HandlerMethod

HandlerMethod@RequestMapping@GetMapping, @PostMapping 등의 하위 어노테이션이 붙은 메소드의 정보를 추상화한 객체이다. 메소드를 실행하기 위해 필요한 모든 정보들을 가지고 있는 객체로써, 핸들러 객체 자체, 메시지 소스, 클래스 타입, 메서드, 파라미터, 상태 코드 등 모든 정보가 존재한다.

AbstractHandlerMethodMapping.getHandlerInternal

HandlerMapping 구현체에서도 urlhttp request를 가지고 HandlerMethod을 생성해서 반환하고 있는 것을 볼 수 있다.

사진 출처
https://ee-22-joo.tistory.com/20

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글