@WebMvcTest를 이용한 Controller 테스트 시 Spring Security에 대해서

2024. 4. 21. 20:13Backend/Spring Security

@WebMvcTest는 컨트롤러 레이어의 테스트를 위한 어노테이션입니다.

Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e. @Controller@ControllerAdvice@JsonComponentConverter/GenericConverterFilterWebMvcConfigurer and HandlerMethodArgumentResolver beans but not @Component, @Service or @Repository beans)

 

따라서 WebMvcTest는 Spring 전체 컨텍스트를 로드하지 않고 MVC test에 필요한 위의 어노테이션들만 실행되게 됩니다. (@Component, @Service, @Repository bean은 로드되지 않음)

 

하지만 Spring Security를 사용하는 환경이거나, @RestControllerAdvice에서 다른 의존성(@Component 이 붙어있는 클래스가 필요)이 있는 상황에서 WebMvcTest를 사용하게 되면 

 

외부 의존성이 필요한 Component가 로드되지 않기 때문에 UnsatisfiedDependencyException이 발생합니다.


GlobalExceptionHandler(@RestControllerAdvice)의 의존성을 해결한 이후에도

Filter를 로드하는데 필요한 의존성이 @Component 어노테이션으로 정의돼있어 로드되지 않는 문제가 발생했습니다.

 

@WebMvcTest로 인해 로드되지 않은 의존성으로 인해 다양한 MockBean에 대한 정의가 필요한 점이 너무 불편했습니다.

 

이것을 해결하기 위해 Filter와 관련된 의존성을 제외하도록 구성했습니다.

 

Spring Security에서 이용하는 Filter들은 OncePerRequestFilter를 상속하여 등록하는데, 이것을 excludeFilters 속성을 통해  @ComponentScan 동안 관련된 구성요소를 스프링 애플리케이션 컨텍스트의 후보에서 제외했습니다.

 

코드 내 @ComponentScan.FilterFilterType.ASSIGNABLE_TYPE을 사용하여 특정 클래스 타입(OncePerRequestFilter.class)을 기반으로 빈 스캐닝에서 제외하도록 지정하는 역할을 합니다.

 

 

 

 

하지만, @WebMvcTest를 사용할 때, 일반적으로 Spring Security 설정이 함께 로드되지만, 직접 정의한 SecurityConfig가 로드되지 않기 때문에 SecurityAutoConfiguration과 함께 제공되는 기본적인 보안 설정은 포함되게 됩니다.

 

따라서 이렇게만 Filter와 관련된 모든 의존성을 제거하더라도

 

기본적인 SecurityFilterChain이 로드되고, 따라서 테스트코드를 실행하더라도

401오류가 발생하게 됩니다.

 

따라서 WebMvcTest의 특성으로 SecurityAutoConfiguration으로 포함되는 default SecurityFilterChain이 포함되지 않게 해야하고, 

 

위처럼 SecurityAutoConfiguration을 exclude한 뒤, Spring Security가 없는 상황에서 존재하지 않는 URI에 대해 요청하면 Not Found가 발생할 것입니다.

 

이를 테스트 코드로 구현하면

위와 같고, 

 

기존 401에러가 발생하던 것이

 

 

예상대로 NotFound가 발생하면서 통과하게 됩니다.


Controller 테스트 시 Spring Security 의존성을 제외해도 괜찮을까?

@WebMvcTest 어노테이션은 스프링 MVC 컨트롤러의 단위 테스트를 위해 설계되었습니다.

 

이는 서비스 레이어의 호출 없이 Controller의 메서드가 요청 경로와 쿼리 파라미터, 그리고 요청 본문(body)을 올바르게 매핑하고 파싱하는지 확인합니다.

 

예를 들어, POST 요청의 JSON 본문을 객체로 변환하는 과정 등이 정확히 수행되는지 테스트하는 목적을 가집니다

 

이 어노테이션의 주요 목적은 웹 계층에 집중하여 불필요한 컨텍스트 로드를 최소화하는 것이므로, Controller 레이어만을 대상으로 하여 관련 의존성만 로드하게 되는데, Spring Security의 인증/인가 부분은 Controller 단위 테스트의 목적과는 맞지 않다고 판단하여 Spring Security와 관련된 컨텍스트를 로드하는 것이 적절할 것으로 판단했습니다.

 

결국 이는 Controller 레이어를 얇게 테스트하여 테스트를 단순화하고 실행 시간을 단축시키는 데에도 도움이 될 것이라고 생각합니다.


HandlerMethodArgumentResolver나, Controller가 Spring Security Context를 이용하는 경우에는?

하지만 모두의 랜덤 디펜스에서는 Spring Security ContextHolder를 이용한 HandlerMethodArgumentResolver를 통해 현재 로그인한 사용자의 정보 인스턴스를 받아오고 있습니다.

이 때는 Spring Security 의존성을 제거해버리면 Handlermethodargumentresolver를 이용할 수 없기 때문에 Spring Security의존성을 제거한 테스트는 불가능하게 될 것 같았지만

 

SecurityContextHolderThreadLocal을 사용하여 각 스레드에서 실행되는 요청의 인증 정보를 독립적으로 관리할 수 있도록 합니다. 이는 동시에 여러 요청을 처리할 때 각 요청이 서로의 인증 정보에 영향을 주지 않도록 보장합니다.

 

SecurityContextHolder는 Bean이 아니라 static 메소드를 통해 사용되기 때문에 Security 관련 설정을 제외하더라도 SecurityContextHolder.getContext()를 통해 이용할 수 있게 됩니다.

 

결국 SecurityAutoConfiguration의 비활성화는 기본 제공되는 HTTP 보안 설정, 메소드 보안 등을 초기화하지는 않지만, SecurityContextHolder를 포함한 Spring Security의 핵심 기능은 영향받지 않기에 문제가 발생하지 않습니다.