그 외 공부/트러블 슈팅

SecurityFilterChain 적용 후 @WebMvcTest 테스트 실패하는 문제 (feat: @WebMvcTest 까보기)

SeongOnion 2023. 11. 4. 17:27
728x90

문제 상황

사내 프로젝트의 스프링 부트 및 자바 버전을 업데이트하면서 Deprecated된 클래스들 또한 새롭게 업데이트하고 있었다.

 

그 중, Spring Security의 WebSecurityConfigurerAdpater가 스프링 부트 2.7 버전부터 Deprecated 되었고 이를 대신해 SecurityFilterChain을 사용하도록 권고했다.

 

교체하는 것 자체는 어렵지 않았다.

 

기존 WebSecurityConfigurerAdpater를 상속하는 것 대신 SecurityFilterChain 빈을 등록해주면 되는 형태였다.

// 기존
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        ...
    }
}
// 수정
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
              .authorizeRequests()
              ...
              .build();
    }
}

 

하지만, 해당 수정을 반영 후 @WebMvcTest를 사용하는 테스트코드들에서 Status Code가 모두 401 Unauthorized가 내려오며 오류가 발생했다.

 

원인

동일한 이슈가 이미 스프링 부트 깃헙 이슈에서 다루어 진 적 있어서 원인은 의외로 금방 파악할 수 있었다.

https://github.com/spring-projects/spring-boot/issues/31162

 

WebMcvTest no longer auto-configures Spring Security when Using SecurityFilterChain Instead of WebSecurityConfigurerAdapter · I

Copied from spring-projects/spring-security#11275 Describe the bug After upgrading spring-boot-starter-parent from version 2.6.4 to 2.7.0 and then making the suggested changes described in Spring S...

github.com

명확한 원인은 해당 댓글에 잘 정리되어있다.

 

요약하자면, 기존에 WebSecurityConfigurerAdapter를 상속하는 클래스들은 @WebMvcTest의 컴포넌트 스캔 대상이 되지만, SecurityFilterChain 빈을 등록한 클래스는 단순 @Configuration을 통해 빈을 등록하므로 @WebMvcTest의 컴포넌트 스캔 대상이 되지 못한다는 것이었다.

아하! SecurityFilterChain은 단순히 빈을 등록한 것일 뿐이므로 MVC와 관련된 빈들만 스캔하는 @WebMvcTest에선 따로 스캔을 하지 않겠구나!

 

이걸 알고보니 SecurityFilterChain 빈이 왜 @WebMvcTest에서 스캔되지 않는가보다는,

 

Web과 단지 간접적으로만 관련이 있는 WebSecurityConfigurerAdapter 상속 클래스들이 어떻게 @WebMvcTest에서 스캔되는지가 더욱 궁금해졌다.

 

디깅

아래 공식문서에 따르면, @WebMvcTest 어노테이션에는 기본적으로 Spring Security 와 관련된 자동 설정을 해준다고 나와있다.

By default, tests annotated with @WebMvcTest will also auto-configure Spring Security and MockMvc (include support for HtmlUnit WebClient and Selenium WebDriver). For more fine-grained control of MockMVC the @AutoConfigureMockMvc annotation can be used.

 

실제 구현이 어떻게 되어있는지는 코드로 직접 확인해보자.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(WebMvcTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(WebMvcTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureWebMvc
@AutoConfigureMockMvc
@ImportAutoConfiguration
public @interface WebMvcTest {
    ...
}

 

@WebMvcTestWebMvcTypeExcludeFillter@TypeExcludeFilters에 등록해주고 있다.

 

그렇담 WebMvcTypeExcludeFilter를 까보자.

public final class WebMvcTypeExcludeFilter extends StandardAnnotationCustomizableTypeExcludeFilter<WebMvcTest> {
   ...

   private static final String[] OPTIONAL_INCLUDES = { "com.fasterxml.jackson.databind.Module",
         "org.springframework.security.config.annotation.web.WebSecurityConfigurer",
         "org.springframework.security.web.SecurityFilterChain", "org.thymeleaf.dialect.IDialect" };

   private static final Set<Class<?>> DEFAULT_INCLUDES;

   static {
      Set<Class<?>> includes = new LinkedHashSet<>();
      includes.add(ControllerAdvice.class);
      includes.add(JsonComponent.class);
      includes.add(WebMvcConfigurer.class);
      includes.add(WebMvcRegistrations.class);
      includes.add(javax.servlet.Filter.class);
      includes.add(FilterRegistrationBean.class);
      includes.add(DelegatingFilterProxyRegistrationBean.class);
      includes.add(HandlerMethodArgumentResolver.class);
      includes.add(HttpMessageConverter.class);
      includes.add(ErrorAttributes.class);
      includes.add(Converter.class);
      includes.add(GenericConverter.class);
      includes.add(HandlerInterceptor.class);
      for (String optionalInclude : OPTIONAL_INCLUDES) {
         try {
            includes.add(ClassUtils.forName(optionalInclude, null));
         }
         catch (Exception ex) {
            // Ignore
         }
      }
      DEFAULT_INCLUDES = Collections.unmodifiableSet(includes);
   }
   ...
}

 

MVC와 관련된 Controller, Filter, Interceptor 등등의 클래스들을 includes에 포함시키고 있으며, 그 외 객체 직렬-역직렬화를 위한 jackson 및 Security와 관련된 WebSecurityConfigurerSecurityFilterChain 클래스들 또한 optional로 포함하고 있었다.

 

WebSecurityConfigurer는 Securiy 관련된 사용자의 커스텀 설정을 위한 인터페이스이고, 우리가 지금껏 썼던(이제는 Deprecated인)  WebSecurityConfigurerAdpater는 해당 인터페이스를 구현하는 추상클래스이다.

public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
    ...
}

 

따라서 WebSecurityConfigurerAdpater를 상속하는 우리의 SecurityConfig 클래스 또한 컴포넌트 스캔 대상에서 포함되게 된 것이었다.

오잉? 근데 새로 도입된 SecurityFilterChain 인터페이스 역시 @WebMvcTest에서 includes에 포함시키고 있는데?

 

차이점은 우리의 새로운 SecurityConfigWebSecurityConfigurerAdpater에서 처럼 추상 클래스를 상속하는 것이 아닌 단순히 SecurityFilterChain이라는 인터페이스의 구현체라는 것이다.

 

즉, 해당 인터페이스 구현체는 Spring Security 모듈이 아닌 우리의 프로젝트 내부에서 빈으로 등록된 것이기 때문에 직접 등록해주지 않으면 당연히 컴포넌트 스캔 범위에 포함되지 못한다.

 

해결

해결 방법은 매우 간단하다.

@WebMvcTest
@Import(SecurityConfig.class) // 추가
public class BaseControllerTest {
    ...
}

 

@WebMvcTest를 사용하는 곳에 SecurityFilterChain 빈을 등록하는 Config 클래스를 Import하여 컴포넌트 스캔 대상에 포함시켜주면 된다.

 

이는 스프링 공식문서에 명시된 방법이기도 하다.

For a @WebMvcTest for an application with the above @Configuration class, you might expect to have the SecurityFilterChain bean in the application context so that you can test if your controller endpoints are secured properly. However, MyConfiguration is not picked up by @WebMvcTest’s component scanning filter because it doesn’t match any of the types specified by the filter. You can include the configuration explicitly by annotating the test class with @Import(MyConfiguration.class). This will load all the beans in MyConfiguration including the BasicDataSource bean which isn’t required when testing the web tier. Splitting the configuration class into two will enable importing just the security configuration.

 

참고: https://docs.spring.io/spring-boot/docs/2.7.x/reference/htmlsingle/#howto.testing.slice-tests

 

Spring Boot Reference Documentation

This section goes into more detail about how you should use Spring Boot. It covers topics such as build systems, auto-configuration, and how to run your applications. We also cover some Spring Boot best practices. Although there is nothing particularly spe

docs.spring.io