멀티 모듈 환경에서 WebTestClient 이용 시 주의 사항
객체 내 java docs 설명에 대한 번역을 chatGPT의 도움을 받았습니다.
예를 들면 아래와 같은 주석을 번역기 돌렸다는 말입니다.
이 문제를 해결하면서 배운 내용도 정리한 글이라 생각보다 장황할 수 있습니다. 문제의 원인/해결을 들어가기 전에 간단하게 요약하겠습니다.
원인
한 프로젝트 내 모듈 A / 모듈 B가 존재합니다.
모듈 A spring-web 의존
모듈 B srping-webflux 의존
한 프로젝트 내 spring-web/spring-webflux 모두 의존하게 되는 상황
해결책
Spring docs - Detecting Web Application Type 를 참고하시면 됩니다.
WebTestClient
리액티브 환경에서 컨트롤러 레이어의 테스트를 원활하게 수행하기 위해서는 WebTestClient 가 필요합니다.
WebTestClient 객체의 역할은 다음과 같습니다.
요약하면 여튼 필요합니다.
제 생각을 조금 더해보자면
WebFlux는 동기/블락킹 기반의 tomcat WAS 대신 비동기 기반/논블락킹 기반의 netty를 WAS로 사용하기 때문입니다. 이로 인해 필요한 구성요소가 달라집니다. 예를 들어 WebFlux 에서 테스트를 수행하기 위해서는 Mono, Flux 타입과 같은 Publisher 가 필요합니다. 일반적인(tomcat was)를 띄우는 spring-web 프로젝트에서는 해당 객체들이 주입되지 않습니다.
@SpringBootTest
@AutoConfigureMockMvc
class PlaceControllerTest {
@Autowired MockMvc mockMvc;
@Autowired ObjectMapper objectMapper;
@Test
void save_places_success() throws Exception {
PlaceForm placeForm = new PlaceForm();
mockMvc.perform(post("/places")
.contentType(MediaType.APPLICATION_JSON)
// 이 부분을 처리할 수 없음
.content(objectMapper.writeValueAsString(Mono.just(placeForm))))
.andExpect(jsonPath("$.message").value(ENROLL))
.andExpect(status().isCreated());
}
정확히는 아래와 같은 에러가 발생합니다.
Request processing failed: org.springframework.http.converter.HttpMessageConversionException:
Type definition error: [simple type, class reactor.core.publisher.Mono]
jakarta.servlet.ServletException:
Request processing failed: org.springframework.http.converter.HttpMessageConversionException
컨버팅 과정에서 Mono 타입을 처리할 수 없어보입니다. 아마도 이 부분을 해결한다고 해도 또 다른 문제가 기다리고 있지 않을까 싶습니다.
결론적으로 WebFlux 환경에서 컨트롤러 레이어를 테스트하려면 WebTestClient 를 주입하는것이 좋아보입니다.
@AutoConfigureWebTestClient, @WebFluxTest
두 애노테이션은 대표적으로 WebTestClient 주입을 제공하는 애노테이션입니다.
위 설명에 따르면 @AutoConfigureWebTestClient 은 @SpringBootTest와 함께 사용할 수 있습니다. @SpringBootTest는 잘 아시다시피 ApplicationContext를 띄웁니다. 따라서 우리가 만들 @Bean, @Component 등을 모두 등록합니다.
반면 @WebFluxTest는 컨트롤러 레이어 전용 테스트 애노테이션이라고 생각하면 됩니다. 쉽게 @WebMvcTest와 비슷합니다. 쉽게 @Controller, @ControllerAdvice 등을 선언한 빈들을 이용할 수 있습니다. 반면 @Component, @Service, @Repository 빈들은 이용할 수 없습니다. 아래는 이에 대한 java docs 설명입니다.
상황에 따라 어떤 애노테이션을 사용할지 판단하면 됩니다. 목적이 컨트롤러 레이어만 테스트하거나 혹여나 API 문서를 만들기 위해 작성하는 테스트 코드에서는 @WebFluxTest가 가벼우니 좋아보인다고 생각합니다.
반면 end-to-end 테스트가 필요할 때는 @AutoConfigureWebTestClient 와 @SpringBootTest 를 함께 사용하는게 좋아보입니다. 제게 필요한 것은 end-to-end 테스트였기에 두 애노테이션을 사용했습니다.
멀티 모듈 환경에서 WebClientTest
멀티 모듈이라고 무조건 발생하는건 아닙니다. 멀티 모듈을 구성하는 과정에서 모듈에 의존성을 더하면서 문제가 생긴 것 같습니다.
@SpringBootTest
@AutoConfigureWebTestClient
class PlaceControllerTest {
@Autowired WebTestClient webTestClient;
@DisplayName("장소 등록 요청 - 성공")
@Test
void save_places_success() throws Exception {
PlaceForm placeForm = new PlaceForm();
webTestClient.post()
.uri("/places")
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(placeForm), PlaceForm.class)
.exchange()
.expectStatus().isCreated()
.expectBody().jsonPath("$.message", ENROLL);
}
정확히 아래와 같은 에러가 발생했습니다.
쉽게 말하면 WebTestClient 후보가 될 수 있는 Bean이 2개 이상이라는 말인 것 같습니다. 위 같은 에러는 아래와 같은 상황에서 발생할 수 있습니다.
@Bean
WebTestClient xuniClient() {
return WebClient.create();
}
@Bean
WebTestClient yuniClient() {
return WebClient.create();
}
동일한 인터페이스에 대해 2개의 빈이 등록되기 때문에 No qualifying 되었다고 합니다. 일반적으로 @Qualify 애노테이션을 통해 주입 단계에서 이를 해결할 수 있습니다. 아래는 생성자 주입을 통해 이 문제를 해결하는 방법입니다.
public PlaceController(PlaceService placeService, @Qualifier("xuniClient") WebClient webClient) {
this.placeService = placeService;
this.webClient = webClient;
}
해결 방법은 이러한 접근을 해야되는 것을 아는데요. 중요한 건 WebTestClient라는 객체를 저는 제가 직접 빈으로 등록한 적이 없다고 생각하고 있었습니다. 그래서 이를 어찌할꼬 고민하다가 어부지리로 해결되었습니다.
저는 여기서 해결책을 얻었습니다.
위에서 설명하기를 WebFlux, Spring MVC가 둘 다 존재한다면 반드시 spring.main.web-application-type 을 설정해야 한다고 합니다. spring.main.web-application-type 값에는 아래와 같은 값이 들어갈 수 있습니다.
/**
* The application should not run as a web application and should not start an
* embedded web server.
*/
NONE,
/**
* The application should run as a servlet-based web application and should start an
* embedded servlet web server.
*/
SERVLET,
/**
* The application should run as a reactive web application and should start an
* embedded reactive web server.
*/
REACTIVE;
위 속성을 서정하면 테스트 레벨에서 애플리케이션을 띄울 때 어떤 WAS가 띄워질지 결정됩니다. REACTIVE 속성을 이용하면 embedded reactive web server 가 실행된다고 합니다.
더 자세한 내용은 WebApplicationType docs에서 확인하시면 됩니다. 결론적으로 SpringBootTest properties 값을 설정해주면 됩니다.
@SpringBootTest(properties = "spring.main.web-application-type=reactive")
전체 코드는 아래와 같습니다.
@SpringBootTest(properties = "spring.main.web-application-type=reactive")
@AutoConfigureWebTestClient
class PlaceControllerTest {
@Autowired WebTestClient webTestClient;
@Test
void save_places_success() throws Exception {
PlaceForm placeForm = new PlaceForm();
webTestClient.post()
.uri("/places")
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(placeForm), PlaceForm.class)
.exchange()
.expectStatus().isCreated()
.expectBody().jsonPath("$.message", ENROLL);
}
}