문제 정의
장바구니 미션을 진행하면서 인증을 위해 Interceptor를 구성했는데요, Interceptor를 만들고 기존에 작성했던 컨트롤러 테스트들을 돌려보자, 제대로 동작하지 않는 것을 확인했습니다.
왜 이런 일이 발생하는 걸까 고민이 많이 되었습니다. 예외가 발생하는 지점은 ProductControllerTest였는데, 코드 상에서 예외가 발생할만한 부분은 존재하지 않았기 때문입니다. 코드는 아래와 같은데요,
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private ProductService productService;
...
}
코드를 바꾼 적도 없고.. 단순히 Interceptor만 등록했을 뿐인데 왜 아래와 같이 ApplicationContext를 load하지 못했다는 예외가 발생하는 걸까요?
고민
예외 메세지를 조금 내려보니, 다음과 같은 문구를 찾을 수 있었습니다.
MemberService 빈을 찾을 수 없다는 내용입니다. 저는 분명 ProductControllerTest를 진행하고 있고, MemberService는 한 번도 사용하지 않는데 이런 예외가 발생하자 당황스러웠습니다.
MemberService 빈이 어디서 사용되는지 고민해 본 결과, 예상되는 지점이 하나 존재했습니다.
public class AuthInterceptor implements HandlerInterceptor {
private final MemberService memberService;
...
}
저는 위와 같이 내부적으로 memberService를 사용하는 방식으로 인터셉터를 구성했었습니다. 그리고 저는 여기서 한 가지 가정을 해볼 수 있었습니다. 혹시 WebMvcTest가 인터셉터까지 끌고 와 테스트를 하려고 하니 MemberService 빈을 찾을 수 없다는 게 아닐까?
문제 해결
그래서 WebMvcTest 어노테이션의 구현부를 들어가 보았는데요, 다음과 같은 어노테이션을 찾을 수 있었습니다.
@TypeExcludeFilters(WebMvcTypeExcludeFilter.class) // 이 친구
@AutoConfigureCache
@AutoConfigureWebMvc
@AutoConfigureMockMvc
@ImportAutoConfiguration
public @interface WebMvcTest {
바로 TypeExcludeFilters라는 친구입니다.
찾아보니 컴포넌트 스캔을 할 때 제외할 빈들을 지정해주는 역할을 하는 어노테이션이었는데요, WebMvcTypeExcludeFilter 클래스를 통해 제외할 빈들을 지정해주고 있는 것 같았습니다.
그래서 WebMvcTypeExcludeFilter 구현부에 들어가봤습니다.
public final class WebMvcTypeExcludeFilter extends StandardAnnotationCustomizableTypeExcludeFilter<WebMvcTest> {
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);
...
}
위와 같이 static initializer block에서 무엇을 컴포넌트 스캔 대상으로 지정할지 명시하고 있었습니다.
그리고 중간에 WebMvcConfigurer가 보이시나요? 저는 인터셉터를 빈으로 등록하기 위해서 WebMvcConfigurer를 상속받는 설정파일 빈을 아래처럼 사용하고 있었는데요,
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
private final MemberService memberService;
public WebMvcConfiguration(MemberService memberService) {
this.memberService = memberService;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggerInterceptor())
.addPathPatterns("/**");
registry.addInterceptor(new AuthInterceptor(memberService))
.addPathPatterns("/carts");
}
...
}
이 설정 파일을 WebMvcTest에서 자동으로 스캔하니, 인터셉터들 또한 자동으로 컨테이너에 등록되는 것이었습니다. 그리고 인터셉터가 사용하는 MemberService에 대한 의존성 주입을 명시하지 않았으니 NoSuchBeanDefinitionException이 발생했던 것입니다.
그래서 WebMvcTest에서 컴포넌트 스캔 대상을 제외하는 방법을 찾아보았습니다. WebMvcTest에는 excludeFilters라는 속성 값이 있었는데, 이를 활용하면 될 것 같았습니다.
@WebMvcTest(value = ProductController.class,
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {
WebMvcConfiguration.class
}
))
위처럼 WebMvcConfiguration 설정 파일을 스캔 대상에서 제외해줬고, 테스트가 아래처럼 정상적으로 작동하는 것을 확인할 수 있었습니다.
마무리
Spring MVC의 구성 요소인 인터셉터를 사용하면서 @WebMvcTest를 사용하던 기존 테스트코드가 깨지는 문제가 생겼고,
무엇이 문제인지, 그리고 어떻게 해결할 수 있는지까지 알아보았습니다.
앞으로 이런 문제가 많이 발생할 수 있을텐데, Spring 프레임워크에 대한 기본적인 이해가 필요하다는 것을 많이 느끼는 계기였습니다.
참고 자료
https://www.jvt.me/posts/2022/02/07/webmvctest-exclude-filter/
'우테코 5기' 카테고리의 다른 글
[레벨 2 미션] 웹 장바구니 미션 학습 기록 (1) (1) | 2023.05.14 |
---|---|
[레벨 2 미션] 웹 자동차 경주 미션 학습 기록 (0) | 2023.05.07 |
장바구니 미션) 도메인에서 영속성 개념 분리해보기 (6) | 2023.04.29 |
우아한테크코스 레벨1 회고 (6) | 2023.04.06 |
[레벨 1 미션] 체스 학습 기록(2) (2) | 2023.04.06 |