Memories in SeoK

기억하고 싶은 것들, 기억해야 하는 것들

개발/자바 Java

[Java] Spring Boot Test - IllegalArgumentException: Failed to find servlet [] in the servlet context

Seo K 2024. 6. 9. 01:00

Spring Boot 3.2.5에서 (Spring Security 6.2.4) random portMock mvc test 진행 시 IllegalArgumentException 발생

java.lang.IllegalArgumentException : Failed to find servlet [] in the servlet context

 

main 쪽에 구성한 Web Security Configuration의 (@EnableWebSecurity) SecurityFilterChain을 제대로 못 불러오는(?) 어떤 버그가 있는 것 같고, 마침 며칠 전에 해결이 된 것 같아 간략히 기록해 둔다 (영어를 읽기 힘든 개발자라.. 슬픔...ㅠㅠ)

cf. Spring Security GitHub Issue (접은 글은 AI 도움으로 번역한 내용)

 

DispatcherServletDelegatingRequestMatcher causes errors when there is more than one ServletContext · Issue #14418 · spring-pro

Describe the bug In a Spring Boot application with multiple servlets registered to the context (DispatcherServlet and at least one other), an IllegalArgumentException with message Failed to find se...

github.com

더보기

2024/01/09 LewisMcReu: DispatcherServlet과 최소 하나 이상의 다른 서블릿이 등록된 Spring Boot 애플리케이션에서, @SpringBootTest (webEnvironment = RANDOM_PORT)로 테스트를 실행할 때 "IllegalArgumentException" 예외가 발생합니다. 예외 메시지는 ```Failed to find servlet [] in the servlet context```입니다.
Spring Boot 3.0.2에서 Spring Boot 3.2.1 (Spring Security 6.2.1)로 업그레이드한 후 발생하기 시작했습니다.
이 예외는 DispatcherServletDelegatingRequestMatcher에서 발생하며, 이는 MockMvcRequestBuilders 클래스로 생성된 표준 MockHttpServletRequest 인스턴스와의 호환성 문제 때문입니다. 이러한 인스턴스는 servletName이 빈 문자열인 일반적인 HttpRequestMapping을 가지고 있어, Matcher가 ServletRegistration을 찾지 못하고 예외를 발생시킵니다.

 

2024/02/16 seschi98: 문제가 처음 발생한 정확한 spring-boot / spring-security 버전을 찾는데 노력했습니다.
시나리오:
* Spring Boot 3.1.x (테스트용, 3.2.x에서도 마찬가지로 문제 발생)
* 메인 서버와 다른 포트에서 관리 서버 실행 (제 경우 :8081)
* SecurityFilterChain 구성
* 추가 서블릿 구성
Spring Boot 3.1.1 (Security 6.1.1) 모든 게 예상대로 작동 (시작 시 오류 없고, 정상 응답 수신)
Spring Boot 3.1.2 (Security 6.1.2) 서버가 시작되지 않음
Spring Boot 3.1.3 (Security 6.1.3) 같은 문제, 오류 메시지만 개선됨
( Boot 3.1.4 (Security 6.1.4), Boot 3.1.5 (Security 6.1.5), Boot 3.1.6 (Security 6.1.5) 동일 )
Spring Boot 3.1.7 (Security 6.1.6) 서버 시작되지만 HTTP 500 오류 수신
application.yaml에서 ```management.server.port: 8081``` 라인을 제거하면, 정상적으로 작동 (물론 이 경우 포트는 8080)

 

2024/03/27 amb-sebastian-podgorski-pt: ```config.requestMatchers( "/hello" ).permitAll();``` 사용하면 AntPathRequestMatcher 생성이 AbstractRequestMatcherRegistry에 위임됩니다. 그다음에는 resolve 메서드가 호출되는데, 이 메서드는 컨텍스트에 구성된 DispatcherServlet이 하나만 있는지 확인합니다. 그리고 추가적인 DispatcherServlet을 선언하면 ant와 mvc 매퍼가 DispatcherServletDelegatingRequestMatcher 내에 래핑됩니다. 이 Matcher는 dispatcherServletRegistration이라는 이름의 Bean을 찾는데, 이 Bean은 지연 초기화(LAZY MODE)로 설정되어 있습니다.
다시 말해, 이 Bean은 첫 번째 다른 포트에(예: localhost:8081/actuator/health) 대한 요청 후에 초기화되므로, 애플리케이션 초기화 중에 구성된 컨텍스트에서 찾을 수 없습니다. 그 결과 이 문제가 발생하게 됩니다. 가장 간단한 해결책은 직접 AntPathRequestMatcher를 선언하는 것입니다.

예: config.requestMatchers( AntPathRequestMatcher.antMatcher( "/hello" ) ).permitAll();

 

2024/06/04 jzheaux: 약간의 배경 설명을 드리면, Spring Security에서 requestMatchers(String)을 사용할 때는 해당 엔드포인트가 MVC인지 아닌지를 알 수 없습니다. 따라서 MVC와 non-MVC 엔드포인트가 모두 있는 애플리케이션(이 경우 사용자 정의 서블릿)에서는 Spring Security에 더 많은 정보가 필요합니다.
Spring Security에서 MockMvc 테스트 내에서 실행 중인 것을 활용할 수 있을 것 같지만, 이에 대한 일반적인 방법은 명확하지 않습니다. Spring Web 팀에 문의하여 의견을 들어보겠습니다.
... [ 아래 해결 방법으로 이어짐 ]

 

문제 제기와 원인 분석 과정은 접은 글을 참조하고, 전체 내용을 종합해서 해결 방법을 간단하게 적으면 다음과 같다

  • 랜덤 포트를 쓰지 말고 8080으로 고정하거나
  • MockHttpServletRequest를 직접 정의하고 송신할 때마다 지정한다

Servlet을 직접 정의하는 것과 관련해서는 다음 두 가지가 제시되었다

(1) Spring Security 버전을 최신 SNAPSHOT으로 업데이트한 경우 다음과 같이 수정할 수 있다

(Maven 저장소에 올라온 것 같진 않던데 GitHub 소스를 직접 받아서 사용할 경우를 말하는 건지.. 확실히는 잘 모르겠다)

this.mvc.perform(get("/").with((request) -> {
    request.setHttpServletMapping(new MockHttpServletMapping("/", "/", "dispatcherServlet", MappingMatch.PATH));
    return request;
})).andExpect(status().isOk());


(2) 또는 다음과 같이 좀 더 일반화된 방법으로 수정할 수도 있다

private static RequestPostProcessor mvcMapping() {
	return (request) -> {
		String matchValue = request.getRequestURI();
		String servlet = "dispatcherServlet";
		String pattern = request.getServletContext().getServletRegistration(servlet).getMappings().iterator().next();
		HttpServletMapping mapping = new MockHttpServletMapping(matchValue, pattern, servlet, MappingMatch.PATH);
		request.setHttpServletMapping(mapping);
		return request;
	};
}

// ...
    this.mvc.perform( get("/")
            .with( mvcMapping() )
            .andExpect( status().isOk() )
        );
// ...