
⚠️ 에러 메세지
2025-11-06T11:29:29.683+09:00 ERROR 47426 --- [ch2] [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.ClassCastException: class org.springframework.web.servlet.resource.ResourceHttpRequestHandler cannot be cast to class org.springframework.web.method.HandlerMethod (org.springframework.web.servlet.resource.ResourceHttpRequestHandler and org.springframework.web.method.HandlerMethod are in unnamed module of loader 'app')] with root cause
java.lang.ClassCastException: class org.springframework.web.servlet.resource.ResourceHttpRequestHandler cannot be cast to class org.springframework.web.method.HandlerMethod (org.springframework.web.servlet.resource.ResourceHttpRequestHandler and org.springframework.web.method.HandlerMethod are in unnamed module of loader 'app')
at com.fastcampus.ch2.PerformanceInterceptor.preHandle(PerformanceInterceptor.java:24) ~[classes/:na]
at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:146) ~[spring-webmvc-6.2.10.jar:6.2.10]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1084) ~[spring-webmvc-6.2.10.jar:6.2.10]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.2.10.jar:6.2.10]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.2.10.jar:6.2.10]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.2.10.jar:6.2.10]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.44.jar:6.0]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.2.10.jar:6.2.10]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.44.jar:6.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.44.jar:10.1.44]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at com.fastcampus.ch2.PerformanceFilter.doFilter(PerformanceFilter.java:20) ~[classes/:na]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.2.10.jar:6.2.10]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.10.jar:6.2.10]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.2.10.jar:6.2.10]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.10.jar:6.2.10]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.2.10.jar:6.2.10]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.10.jar:6.2.10]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1769) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1189) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:658) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.44.jar:10.1.44]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
method.getMethod() = public org.springframework.http.ResponseEntity org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(jakarta.servlet.http.HttpServletRequest)
method.getBean() = org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController@705e081b
[/error] time = 3
📝 문제 상황
Interceptor(공통 전처리, 후처리의 단일 책임원칙 적용) 구현할 때 preHandle 메서드의 매개변수 handle이 HandlerMethod로 캐스팅 할 때 에러 발생함.
HandlerMethod method = (HandlerMethod) handler; // ClassCastException 발생
인터셉터 코드
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component // WebApplicationContext 객체 저장소에 빈 객체로 등록
public class PerformanceInterceptor implements HandlerInterceptor { // 단일 책임 원칙 (SRP) - 하나의 메서드는 하나의 책임만 갖는다
// Filter 구현에서 단일 책임원칙에 따라 메서드를 전처리와 후처리로 분리
//long startTime; // iv - 인스턴스 변수, 싱글톤(객체1개)이라서 여러 쓰레드가 하나의 인스턴스 변수 공유. 값 덮여쓰여질수있음.
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// uri 확인
// String uri = request.getRequestURI(); // 컨텍스트패스 포함
// String ctx = request.getContextPath(); // 보통 "" 또는 "/앱이름"
// String servlet = request.getServletPath(); // DispatcherServlet 기준 경로
// System.out.printf("[INTC] uri=%s, ctx=%s, servletPath=%s, handlerClass=%s%n",
// uri, ctx, servlet, handler.getClass().getName());
// 1. 전처리 작업
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
// handler - 요청하고 연결된 컨트롤러 메서드
HandlerMethod method = (HandlerMethod) handler; // ClassCastException 발생
System.out.println("method.getMethod() = " + method.getMethod()); // URL하고 연결된 메서드
System.out.println("method.getBean() = " + method.getBean()); // 메서드가 포함된 컨트롤러
// return true; // 다음 인터셉터나 컨트롤러를 호출. false면 호출 안함.
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
// 2. 후처리 작업
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 전처리에서 작업한 값을 가져오기.
long startTime = (long)request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
System.out.print("[" + ((HttpServletRequest)request).getRequestURI() + "]");
System.out.println(" time = " + (endTime - startTime));
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
}
✅ 원인 및 해결책
원인1: Dispatcher Servlet이 사용자로부터 받는 요청의 종류가 다양함
기존 생각
기존에 알고있던 처리 순서는 DS(Dispatcher Servlet)가 사용자 모든 요청을 받으면 Interceptor를 통해 전처리하고 컨트롤러 호출하여 처리한 다음 후처리하고 응답을 ModelAndView를 통해 반환하는 것으로 이해했다.

알고보니 에러 메세지에서 ClassCastException에 엄청난 힌트가 있었다.
class org.springframework.web.servlet.resource.ResourceHttpRequestHandler 클래스가 실제로 Interceptor 의 handler에 넘겨진 객체의 타입이었다.
즉, 리소스(Resource) 에는 동적 리소스, 정적 리소스가 있다. 종류에 따라 Dispatcher Servlet이 처리하는 순서가 달라진다.
- 동적 리소스 : 사용자 요청마다 내용이 바뀌는 것으로 실행기가 필요하고
- 정적 리소스 : 내용이 바뀌는 않는 것으로 파일 같은 예가 있다.
ResourceHttpHandler의 경우 HTTP 요청이 정적 리소스(e.g. *.css )인 경우에 Url요청에 연결된 @Controller 애너테이션이 붙은 클래스의 메서드가 호출되지 않는 것이다.
정적 리소스 요청이 발생할 수 있으면 preHandle() 전처리 메서드 구성할 때 HandlerMethod타입과 ResourceHttpHandler를 분기해서 캐스팅을 해야한다.
해결책 1: (채택) handle이 가리키는 타입을 분기해서 캐스팅 or instanceof
사용자 요청을 구성하는 여러 종류를 알아야하고 그것을 모두 분기해야함. 혹은 ! (not)으로 HandlerMethod타입만 캐스팅하도록 instacneof 를 사용
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component // WebApplicationContext 객체 저장소에 빈 객체로 등록
public class PerformanceInterceptor implements HandlerInterceptor { // 단일 책임 원칙 (SRP) - 하나의 메서드는 하나의 책임만 갖는다
// Filter 구현에서 단일 책임원칙에 따라 메서드를 전처리와 후처리로 분리
//long startTime; // iv - 인스턴스 변수, 싱글톤(객체1개)이라서 여러 쓰레드가 하나의 인스턴스 변수 공유. 값 덮여쓰여질수있음.
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 컨트롤러 호출하는 handler 만 전처리, 후처리 작업 하도록한다.
if(!(handler instanceof HandlerMethod)) {
return true;
}
// 1. 전처리 작업
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
// handler - 요청하고 연결된 컨트롤러 메서드
HandlerMethod method = (HandlerMethod) handler; // ClassCastException 발생
System.out.println("method.getMethod() = " + method.getMethod()); // URL하고 연결된 메서드
System.out.println("method.getBean() = " + method.getBean()); // 메서드가 포함된 컨트롤러
// return true; // 다음 인터셉터나 컨트롤러를 호출. false면 호출 안함.
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
// 2. 후처리 작업
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 전처리에서 작업한 값을 가져오기.
long startTime = (long)request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
System.out.print("[" + ((HttpServletRequest)request).getRequestURI() + "]");
System.out.println(" time = " + (endTime - startTime));
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
}
[해결책1]의 문제 : 소요시간 마지막에 /error요청이 preHandle에 걸림
/error 도 결국 컨트롤러 메서드 이므로 HandlerMethod이기 때문에 소요시간 측정이 된 것.
[/] time = 196
[/.well-known/appspecific/com.chrome.devtools.json] time = 6
[/css/main.css] time = 5
method.getMethod() = public org.springframework.http.ResponseEntity org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(jakarta.servlet.http.HttpServletRequest)
method.getBean() = org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController@77c929d2 [
/error] time = 23
[해결책1]의 문제 해결 : /error 경로 제외 등록
/error의 경우 정상 요청이 아니라 에러 응답 요청이기 때문에 이런 요청까지 인터셉터가 공통 전처리 및 후처리를 해버리면 성능 낭비가 된다.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new PerformanceInterceptor())
.addPathPatterns("/**") // 인터셉터를 적용할 대상
.excludePathPatterns("/css/**", "/js/**", "/.well-known/**", "/error"); // 인터셉터 적용 제외 대상
}
}
로그가 예쁘게 찍혔다.

해결책2 : interceptor에서 경로 제외
정적 리소스에 대한 요청은 제외한다.
단점 : 이미 아래와 같이 /css/** 경로는 제외하였음에도 발생한 에러임.
정적 경로가 css 말고도 더 있을 수도 있기 때문에 ClassCastException을 예방하기에 완벽하지는 않다.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new PerformanceInterceptor())
.addPathPatterns("/**") // 인터셉터를 적용할 대상
.excludePathPatterns("/css/**", "/js/**"); // 인터셉터 적용 제외 대상
}
}