Successfully reported this slideshow.

스프링5 웹플럭스와 테스트 전략

63

Share

1 of 128
1 of 128

스프링5 웹플럭스와 테스트 전략

63

Share

Download to read offline

이일민 / Epril
---
스프링5에 도입된 웹플럭스와 리액티브 함수형 프로그래밍 기술에 대한 여러가지 테스트 기술을 살펴보고 이를 효과적으로 개발에 적용하는 방법을 설명합니다.
- 리액티브 함수형 프로그래밍과 테스트
- 웹플럭스를 이용하는 웹 테스트
- 스프링5 애플리케이션의 테스트 전략

이일민 / Epril
---
스프링5에 도입된 웹플럭스와 리액티브 함수형 프로그래밍 기술에 대한 여러가지 테스트 기술을 살펴보고 이를 효과적으로 개발에 적용하는 방법을 설명합니다.
- 리액티브 함수형 프로그래밍과 테스트
- 웹플럭스를 이용하는 웹 테스트
- 스프링5 애플리케이션의 테스트 전략

More Related Content

Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all

스프링5 웹플럭스와 테스트 전략

  1. 1. 호주에 거주하고 있지만 여러 나라를 다니며 일하기 좋아하는 개발자. 오래전 스 프링 프레임워크의 매력에 빠진 뒤로 스프링의 변화에 맞춰 기술을 공부하고 이 를 이용해 일을 하는 것을 좋아한다. 최신 스프링에 추가된 리액티브 함수형 프로 그래밍 기술을 블록체인 코어 개발을 비롯한 여러 분야에 적용해보고 있다. 토비 의 스프링이라는 책을 썼고, 유튜브에서 토비의 봄 TV라는 코딩 방송을 하기도 한 다.
  2. 2. • • • • •
  3. 3. • • • • • •
  4. 4. • o o • o ó
  5. 5. q
  6. 6. q
  7. 7. q
  8. 8. q
  9. 9. q
  10. 10. q ?
  11. 11. q
  12. 12. q
  13. 13. WebFlux MVC
  14. 14. WebFlux MVC
  15. 15. WebFlux MVC
  16. 16.
  17. 17. • WebFlux MVC
  18. 18. • • • • • • •
  19. 19. • o o • o o o •
  20. 20. • o o o o o o o • o
  21. 21. • 10:00
  22. 22. • •
  23. 23. • o o • o o o o o
  24. 24. public UserOrder orders(String email) { try { User user = findUserApi(email); List<Order> orders = getOpenOrders(user); return new UserOrder(email, orders); } catch(Exception e) { return UserOrder.FAIL; } }
  25. 25. public UserOrder orders(String email) { try { User user = findUserApi(email); List<Order> orders = getOpenOrders(user); return new UserOrder(email, orders); } catch(Exception e) { return UserOrder.FAIL; } } 두번의 동기 API 호출 RestTemplate
  26. 26. public DeferredResult<UserOrder> asyncOrders(String email) { DeferredResult dr = new DeferredResult(); asyncFindUserApi(email).addCallback( user -> asyncOrdersApi(user).addCallback( orders -> { dr.setResult(new UserOrder(email, orders)); }, e -> dr.setErrorResult(UserOrder.FAIL) ), e -> dr.setErrorResult(UserOrder.FAIL)); return dr; }
  27. 27. public DeferredResult<UserOrder> asyncOrders(String email) { DeferredResult dr = new DeferredResult(); asyncFindUserApi(email).addCallback( user -> asyncOrdersApi(user).addCallback( orders -> { dr.setResult(new UserOrder(email, orders)); }, e -> dr.setErrorResult(UserOrder.FAIL) ), e -> dr.setErrorResult(UserOrder.FAIL)); return dr; } 두번의 비동기 API 호출 AsyncRestTemplate
  28. 28. public DeferredResult<UserOrder> asyncOrders(String email) { DeferredResult dr = new DeferredResult(); asyncFindUserApi(email).addCallback( user -> asyncOrdersApi(user).addCallback( orders -> { dr.setResult(new UserOrder(email, orders)); }, e -> dr.setErrorResult(UserOrder.FAIL) ), e -> dr.setErrorResult(UserOrder.FAIL)); return dr; } 비동기 결과처리를 위한 콜백 매 단계마다 중첩
  29. 29. public DeferredResult<UserOrder> asyncOrders(String email) { DeferredResult dr = new DeferredResult(); asyncFindUserApi(email).addCallback( user -> asyncOrdersApi(user).addCallback( orders -> { dr.setResult(new UserOrder(email, orders)); }, e -> dr.setErrorResult(UserOrder.FAIL) ), e -> dr.setErrorResult(UserOrder.FAIL)); return dr; } 매 단계마다 반복되는 예외 콜백
  30. 30. public CompletableFuture<UserOrder> asyncOrders2(String email) { return asyncFindUser2(email) .thenCompose(user -> asyncOrders2(user)) .thenApply(orders -> new UserOrder(email, orders)) .exceptionally(e -> UserOrder.FAIL); }
  31. 31. public CompletableFuture<UserOrder> asyncOrders2(String email) { return asyncFindUser2(email) .thenCompose(user -> asyncOrders2(user)) .thenApply(orders -> new UserOrder(email, orders)) .exceptionally(e -> UserOrder.FAIL); } 비동기 결과처리 함수 이용 중첩되지 않음
  32. 32. public CompletableFuture<UserOrder> asyncOrders2(String email) { return asyncFindUser2(email) .thenCompose(user -> asyncOrders2(user)) .thenApply(orders -> new UserOrder(email, orders)) .exceptionally(e -> UserOrder.FAIL); } Exceptional Programming 예외처리 단일화
  33. 33. public CompletableFuture<UserOrder> asyncOrders3(String email) { try { var user = await(asyncFindUser2(email)); var orders = await(asyncGetOrders2(user)); return completedFuture(new UserOrder(email, orders)); } catch(Exception e) { return completedFuture(UserOrder.FAIL); } } Java8+ 빌드타임, 로딩타임, 런타임 코드생성
  34. 34. public Mono<UserOrder> asyncOrders4(String email) { return asyncFindUser4(email) .flatMap(user -> asyncGetOrders4(user)) .map(orders -> new UserOrder(email, orders)) .onErrorReturn(UserOrder.FAIL); } WebClient 이용 CompletableFuture와 유사해 보이지만…
  35. 35. public Mono<UserOrder> asyncOrders4(String email) { return asyncFindUser4(email) .flatMap(user -> asyncGetOrders4(user)) .map(orders -> new UserOrder(email, orders)) .onErrorReturn(UserOrder.FAIL); } Mono<T> (0, 1) Flux<T> (0 … n)
  36. 36. Mono<User> -> Mono<List<Order>> -> Mono<UserOrder>
  37. 37. • o o o o o • o ßà o o o o o
  38. 38. • • •
  39. 39. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(1)); }
  40. 40. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(1)); }
  41. 41. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(1)); }
  42. 42. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(1)); }
  43. 43. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(1)); } 테스트 성공!!
  44. 44. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(2)); } 테스트 성공???
  45. 45. @Test void mono3() throws InterruptedException { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); CountDownLatch latch = new CountDownLatch(1); mono.subscribe(item -> { assertThat(item).isEqualTo(2); latch.countDown(); }); latch.await(); } 테스트가 끝나지 않음!!
  46. 46. @Test void mono4() throws InterruptedException { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); CountDownLatch latch = new CountDownLatch(1); AtomicInteger item = new AtomicInteger(); mono.subscribe( i -> item.set(i), e -> latch.countDown(), latch::countDown ); latch.await(); assertThat(item.get()).isEqualTo(1); } 테스트에서 동시성을 제어해야 하는 번거로움!
  47. 47. @Test void mono5() throws InterruptedException { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); Integer item = mono.block(); assertThat(item).isEqualTo(1); }
  48. 48. @Test void mono5() throws InterruptedException { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); Integer item = mono.block(); assertThat(item).isEqualTo(1); } 데이터 스트림이 종료될 때까지 대기
  49. 49. • • •
  50. 50. • • • •
  51. 51. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(2) .verifyComplete(); }
  52. 52. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(2) .verifyComplete(); } Flux/Mono
  53. 53. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(2) .verifyComplete(); } 동작을 검증
  54. 54. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(1) .verifyComplete(); } StepVerifier 생성
  55. 55. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(1) .verifyComplete(); } 첫번째 데이터 아이템 값
  56. 56. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(1) .verifyComplete(); } 스트림 완료
  57. 57. @Test void stepVerifer2() { var flux = Flux.just(1,2,3) .concatWith(Mono.error(new RuntimeException())) .subscribeOn(Schedulers.single()); StepVerifier.create(flux) .expectNext(1) .expectNext(2) .expectNext(3) .expectError(RuntimeException.class) .verify(); }
  58. 58. @Test void stepVerifer2() { var flux = Flux.just(1,2,3) .concatWith(Mono.error(new RuntimeException())) .subscribeOn(Schedulers.single()); StepVerifier.create(flux) .expectNext(1) .expectNext(2) .expectNext(3) .expectError(RuntimeException.class) .verify(); } 3 데이터 + 에러발생 스트림
  59. 59. @Test void stepVerifer2() { var flux = Flux.just(1,2,3) .concatWith(Mono.error(new RuntimeException())) .subscribeOn(Schedulers.single()); StepVerifier.create(flux) .expectNext(1) .expectNext(2) .expectNext(3) .expectError(RuntimeException.class) .verify(); } 첫번째 데이터 1
  60. 60. @Test void stepVerifer2() { var flux = Flux.just(1,2,3) .concatWith(Mono.error(new RuntimeException())) .subscribeOn(Schedulers.single()); StepVerifier.create(flux) .expectNext(1) .expectNext(2) .expectNext(3) .expectError(RuntimeException.class) .verify(); } 두번째 데이터 2
  61. 61. @Test void stepVerifer2() { var flux = Flux.just(1,2,3) .concatWith(Mono.error(new RuntimeException())) .subscribeOn(Schedulers.single()); StepVerifier.create(flux) .expectNext(1) .expectNext(2) .expectNext(3) .expectError(RuntimeException.class) .verify(); } 두번째 데이터 3
  62. 62. @Test void stepVerifer2() { var flux = Flux.just(1,2,3) .concatWith(Mono.error(new RuntimeException())) .subscribeOn(Schedulers.single()); StepVerifier.create(flux) .expectNext(1) .expectNext(2) .expectNext(3) .expectError(RuntimeException.class) .verify(); } 에러나고 종료
  63. 63. • o • o o • o o o o
  64. 64. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/api/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")); }
  65. 65. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/api/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")); } 원격 API 호출+로직
  66. 66. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/api/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")); } 원격 API 요청 준비
  67. 67. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/api/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")); } 원격 API 실행
  68. 68. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/api/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")); } 응답 HTTP 상태 코드 처리
  69. 69. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/api/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")); } API 응답 body 변환
  70. 70. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/api/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")); } 결과에 비즈니스 로직 적용
  71. 71. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/api/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")); } 예외적인 결과 대응
  72. 72. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/api/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")); } 작성하기 매우 편리함 웹 플럭스 첫걸음은 WebClient로부터
  73. 73. @GetMapping(value="/api2", produces = "text/event-stream") public Flux<String> helloStream() { return client.get() .uri("/stream") .accept(MediaType.APPLICATION_STREAM_JSON) .exchange() .flatMapMany(res -> res.bodyToFlux(User.class)) .filter(user -> user.getId() > 1) .map(user -> user.toString()); } HTTP 스트리밍 API 요청도 간단
  74. 74. Flux 테스트로 작성 - StepVerifier
  75. 75. private WebClient.Builder builder; private ExchangeFunction exchangeFunction; @Captor private ArgumentCaptor<ClientRequest> captor; @BeforeEach void setUp() { MockitoAnnotations.initMocks(this); this.exchangeFunction = mock(ExchangeFunction.class); when(this.exchangeFunction.exchange(this.captor.capture())).thenReturn(Mono.empty()); this.builder = WebClient.builder().baseUrl("/").exchangeFunction(this.exchangeFunction); } @Test void getRequest() { this.builder.build().get().uri("/hello").exchange(); ClientRequest request = this.captor.getValue(); Mockito.verify(this.exchangeFunction).exchange(request); verifyNoMoreInteractions(this.exchangeFunction); assertEquals("/hello", request.url().toString()); assertEquals(new HttpHeaders(), request.headers()); assertEquals(Collections.emptyMap(), request.cookies()); } 가능은 하지만 과연?
  76. 76. private MockWebServer server; private WebClient webClient; @Before public void setup() { var connector = new ReactorClientHttpConnector(); this.server = new MockWebServer(); this.webClient = WebClient .builder() .clientConnector(connector) .baseUrl(this.server.url("/").toString()) .build(); } com.squareup.okhttp3:mockwebserver WebClientIntegrationTests 유용한 샘플
  77. 77. @Test public void shouldReceiveResponseHeaders() { prepareResponse(response -> response .setHeader("Content-Type", "text/plain") .setBody("Hello Spring!")); Mono<HttpHeaders> result = this.webClient.get() .uri("/greeting?name=Spring").exchange() .map(response -> response.headers().asHttpHeaders()); StepVerifier.create(result).consumeNextWith( httpHeaders -> { assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType()); assertEquals(13L, httpHeaders.getContentLength()); }) .expectComplete().verify(Duration.ofSeconds(3)); expectRequestCount(1); expectRequest(request -> { assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); assertEquals("/greeting?name=Spring", request.getPath()); }); }
  78. 78. @Test public void shouldReceiveResponseHeaders() { prepareResponse(response -> response .setHeader("Content-Type", "text/plain") .setBody("Hello Spring!")); Mono<HttpHeaders> result = this.webClient.get() .uri("/greeting?name=Spring").exchange() .map(response -> response.headers().asHttpHeaders()); StepVerifier.create(result).consumeNextWith( httpHeaders -> { assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType()); assertEquals(13L, httpHeaders.getContentLength()); }) .expectComplete().verify(Duration.ofSeconds(3)); expectRequestCount(1); expectRequest(request -> { assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); assertEquals("/greeting?name=Spring", request.getPath()); }); } MockWebServer의 응답 준비
  79. 79. @Test public void shouldReceiveResponseHeaders() { prepareResponse(response -> response .setHeader("Content-Type", "text/plain") .setBody("Hello Spring!")); Mono<HttpHeaders> result = this.webClient.get() .uri("/greeting?name=Spring").exchange() .map(response -> response.headers().asHttpHeaders()); StepVerifier.create(result).consumeNextWith( httpHeaders -> { assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType()); assertEquals(13L, httpHeaders.getContentLength()); }) .expectComplete().verify(Duration.ofSeconds(3)); expectRequestCount(1); expectRequest(request -> { assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); assertEquals("/greeting?name=Spring", request.getPath()); }); } WebClient 코드 실행
  80. 80. @Test public void shouldReceiveResponseHeaders() { prepareResponse(response -> response .setHeader("Content-Type", "text/plain") .setBody("Hello Spring!")); Mono<HttpHeaders> result = this.webClient.get() .uri("/greeting?name=Spring").exchange() .map(response -> response.headers().asHttpHeaders()); StepVerifier.create(result).consumeNextWith( httpHeaders -> { assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType()); assertEquals(13L, httpHeaders.getContentLength()); }) .expectComplete().verify(Duration.ofSeconds(3)); expectRequestCount(1); expectRequest(request -> { assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); assertEquals("/greeting?name=Spring", request.getPath()); }); } 응답 결과 검증
  81. 81. @Test public void shouldReceiveResponseHeaders() { prepareResponse(response -> response .setHeader("Content-Type", "text/plain") .setBody("Hello Spring!")); Mono<HttpHeaders> result = this.webClient.get() .uri("/greeting?name=Spring").exchange() .map(response -> response.headers().asHttpHeaders()); StepVerifier.create(result).consumeNextWith( httpHeaders -> { assertEquals(MediaType.TEXT_PLAIN, httpHeaders.getContentType()); assertEquals(13L, httpHeaders.getContentLength()); }) .expectComplete().verify(Duration.ofSeconds(3)); expectRequestCount(1); expectRequest(request -> { assertEquals("*/*", request.getHeader(HttpHeaders.ACCEPT)); assertEquals("/greeting?name=Spring", request.getPath()); }); } MockWebServer 검증
  82. 82. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")) } •
  83. 83. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")) } • WebClient 호출
  84. 84. @GetMapping("/api") public Mono<String> helloApi() { return client.get() .uri("/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")) } • 일반 Mono/Flux 코드
  85. 85. interface HelloService { Mono<String> hello(); } @Component public class RemoteHelloService implements HelloService { public Mono<String> hello() { return client.get() .uri("/hello") .retrieve() .onStatus(HttpStatus::is4xxClientError, res->Mono.error(new IllegalArgumentException())) .onStatus(HttpStatus::is5xxServerError, res->Mono.error(new RuntimeException())) .bodyToMono(String.class) } }
  86. 86. @Autowired HelloService helloService; @GetMapping("/api") public Mono<String> helloApi() { return this.helloService.hello() .map(body -> body.toUpperCase()) .switchIfEmpty(Mono.just("Empty")) .doOnError(c -> c.printStackTrace()); } 단순한 리액티브 API를 이용하는 코드 Mock 서비스로 대체 가능
  87. 87. • o • o o o o o o o
  88. 88. public interface HttpHandler { Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response); } • •
  89. 89. public interface WebHandler { Mono<Void> handle(ServerWebExchange exchange); } • • •
  90. 90. PersonRepository repository = ... PersonHandler handler = new PersonHandler(repository); RouterFunction<ServerResponse> route = route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson) .andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople) .andRoute(POST("/person"), handler::createPerson); public class PersonHandler { public Mono<ServerResponse> listPeople(ServerRequest request) { … } public Mono<ServerResponse> createPerson(ServerRequest request) { … } public Mono<ServerResponse> getPerson(ServerRequest request) { … } } 웹 요청을 처리하고 응답을 만드는 순수한 함수의 모음
  91. 91. PersonRepository repository = ... PersonHandler handler = new PersonHandler(repository); RouterFunction<ServerResponse> route = route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson) .andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople) .andRoute(POST("/person"), handler::createPerson); public class PersonHandler { public Mono<ServerResponse> listPeople(ServerRequest request) { … } public Mono<ServerResponse> createPerson(ServerRequest request) { … } public Mono<ServerResponse> getPerson(ServerRequest request) { … } } 웹 요청을 담당할 함수 핸들러를 찾음
  92. 92. PersonRepository repository = ... PersonHandler handler = new PersonHandler(repository); RouterFunction<ServerResponse> route = route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson) .andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople) .andRoute(POST("/person"), handler::createPerson); public class PersonHandler { public Mono<ServerResponse> listPeople(ServerRequest request) { … } public Mono<ServerResponse> createPerson(ServerRequest request) { … } public Mono<ServerResponse> getPerson(ServerRequest request) { … } } ServerRequest->ServerResponse로 변환하는 리액티브 핸들러
  93. 93. HttpServer.create().host("localhost").handle( new ReactorHttpHandlerAdapter(toHttpHandler( route(path("/hello"), req -> ok().body(fromObject("Hello Functional"))))) ).bind().block(); 스프링 컨테이너도 필요없음
  94. 94. • o o o • o o o o o
  95. 95. • o bindToApplicationContext 이용 @RunWith(SpringRunner.class) @WebFluxTest public class WebClientBootTest { @Autowired WebTestClient webTestClient; @Test public void hello() { webTestClient.get().uri("/hello/{name}", "Spring") .exchange() .expectStatus() .isOk() .expectBody(String.class) .isEqualTo("Hello Spring"); } }
  96. 96. • o bindToApplicationContext 이용 @RunWith(SpringRunner.class) @WebFluxTest public class WebClientBootTest { @Autowired WebTestClient webTestClient; @Test public void hello() { webTestClient.get().uri("/hello/{name}", "Spring") .exchange() .expectStatus() .isOk() .expectBody(String.class) .isEqualTo("Hello Spring"); } } SpringBoot 앱을 MockServer에 배포 테스트에 사용할 WebTestClient 생성
  97. 97. • o bindToApplicationContext 이용 @RunWith(SpringRunner.class) @WebFluxTest public class WebClientBootTest { @Autowired WebTestClient webTestClient; @Test public void hello() { webTestClient.get().uri("/hello/{name}", "Spring") .exchange() .expectStatus().isOk() .expectBody(String.class) .isEqualTo("Hello Spring"); } } WebClient 처럼 API 호출하고
  98. 98. • o bindToApplicationContext 이용 @RunWith(SpringRunner.class) @WebFluxTest public class WebClientBootTest { @Autowired WebTestClient webTestClient; @Test public void hello() { webTestClient.get().uri("/hello/{name}", "Spring") .exchange() .expectStatus().isOk() .expectBody(String.class) .isEqualTo("Hello Spring"); } } API 호출 응답 결과 검증
  99. 99. • var client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build(); client.get().uri("/hello/{name}", "Spring") .exchange() .expectStatus() .isOk() .expectBody(String.class) .isEqualTo("Hello");
  100. 100. • WebHttpHandlerBuilder가 이용할 수 있는 WebHandler API 구성 을 가진 ApplicationContext를 이용 var context = new AnnotationConfigApplicationContext(MyConfiguration.class); WebTestClient client = WebTestClient.bindToApplicationContext(context).build(); client.get().uri("/hello/{name}", "Spring") .exchange() .expectStatus() .isOk() .expectBody(String.class) .isEqualTo("Hello Spring");
  101. 101. • WebHttpHandlerBuilder가 이용할 수 있는 WebHandler API 구성 을 가진 ApplicationContext를 이용 WebTestClient client = WebTestClient.bindToController( new MyController(), new HelloApi() ).build(); client.get().uri("/hello/{name}", "Spring") .exchange() .expectStatus() .isOk() .expectBody(String.class) .isEqualTo("Hello Spring");
  102. 102. • WebHttpHandlerBuilder가 이용할 수 있는 WebHandler API 구성 을 가진 ApplicationContext를 이용 WebTestClient client = WebTestClient.bindToController( new MyController(), new HelloApi() ).build(); client.get().uri("/hello/{name}", "Spring") .exchange() .expectStatus() .isOk() .expectBody(String.class) .isEqualTo("Hello Spring"); 특정 컨트롤러/핸들러만으로 테스트 대상 구성
  103. 103. • Mono<ServerResponse> handler(ServerRequest request) { return ServerResponse.ok().body(Mono.just("hello"),String.class); } @Test void routerFunction() { RouterFunction<ServerResponse> route = route(GET("/rf"), this::handler); WebTestClient client = WebTestClient.bindToRouterFunction(route) .build(); client.get().uri("/rf") .exchange() .expectStatus().isOk() .expectBody(String.class) .isEqualTo("hello"); }
  104. 104. • • o • o o o

×