스프링 WebFlux를 사용할 때는 Observable이나 Single 같은 Rxjava 타입을 사용할 수 있다.
이뿐만 아니라 리액터의 Mono 타입과 동일한 Rxjava의 Completable 타입, Observable이나 Flux 타입의 대안으로 Flowable 타입을 반환할 수도 있다.
/design/taco GET 요청이 들어왔을 경우 recents() 메서드에서 처리되고, /design POST 요청이 들어왔을 경우 postTaco() 메서드에서 처리한다.
리액티브 컨트롤러 테스트
WebFlux를 사용하는 리액티브 컨트롤러의 테스트를 쉽게 작성할 수 있게 도와주는 테스트 유틸리티로 WebTestClient가 있다.
GET 요청 테스트
1@Test 2publicvoidshouldReturnRecentTacos(){ 3Taco[]tacos={ 4testTaco(1L),testTaco(2L), 5testTaco(3L),testTaco(4L), 6testTaco(5L),testTaco(6L)};// 테스트 데이터 생성
7Flux<Taco>tacoFlux=Flux.just(tacos); 8 9TacoRepositorytacoRepo=Mockito.mock(TacoRepository.class);10when(tacoRepo.findAll()).thenReturn(tacoFlux);// 모의 TacoRepository
1112WebTestClienttestClient=WebTestClient.bindToController(13newDesignTacoController(tacoRepo))14.build();// WebTestClient 생성
1516testClient.get().uri("/design/recent")17.exchange()// 가장 최근의 객체 요청
18.expectStatus().isOk()// 기대한 응답인지 검사
19.expectBody()20.jsonPath("$").isArray()21.jsonPath("$").isNotEmpty()22.jsonPath("$[0].id").isEqualTo(tacos[0].getId().toString())23.jsonPath("$[0].name").isEqualTo("Taco 1")24.jsonPath("$[12]").doesNotExist();25}
expectStatus() 메서드를 호출하여 응답이 200(OK) 상태 코드를 가지는지 확인한다.
jsonPath() 메서드를 호출하여 응답 몸체의 JSON이 기대값을 가지는지 검사한다.
POST 요청 테스트
1@Test 2publicvoidshouldSaveATaco(){ 3TacoRepositorytacoRepo=Mockito.mock(TacoRepository.class); 4Mono<Taco>unsavedTacoMono=Mono.just(testTaco(null)); 5TacosavedTaco=testTaco(null); 6savedTaco.setId(UUID.randomUUID()); 7Mono<Taco>savedTacoMono=Mono.just(savedTaco); 8 9when(tacoRepo.save(any())).thenReturn(savedTacoMono);// 모의 TestRepository
1011WebTestClienttestClient=WebTestClient.bindToController(// WebTestClient 생성
12newDesignTacoController(tacoRepo)).build();1314testClient.post()// post 처리
15.uri("/design")16.contentType(MediaType.APPLICATION_JSON)17.body(unsavedTacoMono,Taco.class)18.exchange()19.expectStatus().isCreated()// 응답을 검사한다
20.expectBody(Taco.class)21.isEqualTo(savedTaco);22}
post() 메서드를 통해 POST 요청을 제출하고 application/json 타입의 몸체와 페이로드를 같이 전달한다.
exchange() 실행 후 응답이 CREATED(201) 상태 코드를 가지는지, 응답으로 객체와 동일한 페이로드를 가지는지 검사한다.
실행 중인 서버로 테스트
1@RunWith(SpringRunner.class) 2@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT) 3publicclassDesignTacoControllerWebTest{ 4 5@Autowired 6privateWebTestClienttestClient; 7 8@Test 9publicvoidshouldReturnRecentTacos(){10testClient.get().uri("/design/recent")11.accept(MediaType.APPLICATION_JSON).exchange()// 가장 최근의 객체 요청
12.expectStatus().isOk()// 기대한 응답인지 검사
13.expectBody()14.jsonPath("$[?(@.id == 'TACO1')].name").isEqualTo("Carnivore")15.jsonPath("$[?(@.id == 'TACO2')].name").isEqualTo("Bovine Bounty")16.jsonPath("$[?(@.id == 'TACO3')].name").isEqualTo("Veg-out")17}18}
WebTestClient의 통합 테스트를 위해서는 @RunWith와 @SpringBootTest 어노테이션을 지정해야 한다.
WebEnvironment.RANDOM_PORT : 무작위로 선택된 포트로 실행 서버가 리스닝하도록 요청
자동 연결되는 WebTestClient를 사용하기 때문에 인스턴스를 생성할 필요가 없다는 특징이 있다.
REST API 사용
RestTemplate이 제공하는 모든 메서드는 도메인 타입이나 컬렉션 타입을 처리하기 때문에 스프링 5에서는 RestTemplate 대신 리액티브 타입을 사용할 수 있는 대안으로 WebClient가 존재한다.
일반적인 WebClient 패턴
WebClient 인스턴스를 생성한다.
요청을 전송할 HTTP 메서드를 지정한다.
요청에 필요한 URI와 헤더를 지정한다.
요청을 제출한다.
응답을 사용한다.
GET 사용
1Mono<Ingredient>ingredient=WebClient.create()2.get()3.uri("http://localhost:8080/ingredients/{id}",ingredientId)4.retrive()5.bodyToMono(Ingredient.class);// 단일 값
67ingredient.subscribe(i->{});
create() 메서드로 새로운 WebClient 인스턴스를 생성하고, get()과 uri()를 통해 GET 요청을 정의한다.
bodyToMono() 메서드 호출을 통해 response body의 페이로드를 Mono로 추출한다.
DELETE 요청은 응답 페이로드를 가지지 않기 때문에 Mono를 반환하고 subscribe()로 구독해야 한다.
에러 처리
에러가 생길 수 있는 Mono나 Flux를 구독할 때는 subscribe() 메서드 내에 에러 컨슈머를 등록할 필요가 있다.
1ingredientMono.subscribe(2ingredient->{3// 데이터 처리
4},5error->{6// 에러 처리
7});
404 같은 상태 코드가 반환되면 두번째 인자로 전달된 에러 컨슈머가 실행되어 기본적으로 WebClientResponseException을 발생시킨다.
→ WebClientResponseException : 구체적인 예외를 나타내지 않기 때문에 커스텀 에러 핸들러가 필요!
HTTP 상태 코드가 400 수준의 상태코드일 경우 UnknownIngredientException을 포함하는 Mono가 반환되고 ingredient 실행이 실패하게 된다.
요청 교환
retrieve() 메서드는 ResponseSpec 타입을 반환하는데, 응답의 헤더나 쿠키 값을 사용할 경우에는 ResponseSpec 타입으로 처리할 수 없다는 단점이 존재한다.
이 단점을 해결하기 위해 ClientResponse 타입의 Mono를 반환하는 exchange() 메서드가 존재하는데, (1) 리액티브 오퍼레이션을 적용하거나, (2) 응답의 모든 부분을 사용할 수 있다.
스프링 MVC와 다르게 @EnableWebSecurity 대신 @EnableWebFluxSecurity가 지정되어 있다. 또한 인자로 HttpSecurity 대신 SecurityHttpSecurity를 사용해서 authorizeExchange()를 호출하였다.
중요한 점은 프레임워크 메서드를 오버라이딩 하는 대신 build() 메서드를 호출하여 모든 보안 규칙을 반환해야 한다.
userDetailsService의 리액티브 버전인 ReactiveUserDetailsService 빈을 선언하였다. userRepo.findByUsername() 에서 반환된 Mono가 발행하는 User 객체의 toUserDetails() 메서드를 호출하여 User 객체를 UserDetails 객체로 변환한다.