SlideShare a Scribd company logo
1 of 128
Download to read offline
호주에 거주하고 있지만 여러 나라를 다니며 일하기 좋아하는 개발자. 오래전 스
프링 프레임워크의 매력에 빠진 뒤로 스프링의 변화에 맞춰 기술을 공부하고 이
를 이용해 일을 하는 것을 좋아한다. 최신 스프링에 추가된 리액티브 함수형 프로
그래밍 기술을 블록체인 코어 개발을 비롯한 여러 분야에 적용해보고 있다. 토비
의 스프링이라는 책을 썼고, 유튜브에서 토비의 봄 TV라는 코딩 방송을 하기도 한
다.
•
•
•
•
•
•
•
•
•
•
•
•
o
o
•
o ó
q
q
q
q
q
q
?
q
q
WebFlux
MVC
WebFlux
MVC
WebFlux
MVC
•
•
WebFlux
MVC
•
•
•
•
•
•
•
•
o
o
•
o
o
o
•
•
o
o
o
o
o
o
o
•
o
•
10:00
•
•
•
o
o
•
o
o
o
o
o
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;
}
}
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
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;
}
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
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;
}
비동기 결과처리를 위한 콜백
매 단계마다 중첩
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;
}
매 단계마다 반복되는
예외 콜백
public CompletableFuture<UserOrder> asyncOrders2(String email) {
return asyncFindUser2(email)
.thenCompose(user -> asyncOrders2(user))
.thenApply(orders -> new UserOrder(email, orders))
.exceptionally(e -> UserOrder.FAIL);
}
public CompletableFuture<UserOrder> asyncOrders2(String email) {
return asyncFindUser2(email)
.thenCompose(user -> asyncOrders2(user))
.thenApply(orders -> new UserOrder(email, orders))
.exceptionally(e -> UserOrder.FAIL);
} 비동기 결과처리 함수 이용
중첩되지 않음
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
예외처리 단일화
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+
빌드타임, 로딩타임, 런타임 코드생성
public Mono<UserOrder> asyncOrders4(String email) {
return asyncFindUser4(email)
.flatMap(user -> asyncGetOrders4(user))
.map(orders -> new UserOrder(email, orders))
.onErrorReturn(UserOrder.FAIL);
} WebClient 이용
CompletableFuture와 유사해 보이지만…
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)
Mono<User> -> Mono<List<Order>> -> Mono<UserOrder>
•
o
o
o
o
o
•
o ßà
o
o
o
o
o
•
•
•
@Test
void mono() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
mono.subscribe(item -> assertThat(item).isEqualTo(1));
}
@Test
void mono() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
mono.subscribe(item -> assertThat(item).isEqualTo(1));
}
@Test
void mono() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
mono.subscribe(item -> assertThat(item).isEqualTo(1));
}
@Test
void mono() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
mono.subscribe(item -> assertThat(item).isEqualTo(1));
}
@Test
void mono() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
mono.subscribe(item -> assertThat(item).isEqualTo(1));
}
테스트 성공!!
@Test
void mono() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
mono.subscribe(item -> assertThat(item).isEqualTo(2));
}
테스트 성공???
@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();
}
테스트가 끝나지 않음!!
@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);
}
테스트에서 동시성을 제어해야 하는
번거로움!
@Test
void mono5() throws InterruptedException {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
Integer item = mono.block();
assertThat(item).isEqualTo(1);
}
@Test
void mono5() throws InterruptedException {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
Integer item = mono.block();
assertThat(item).isEqualTo(1);
}
데이터 스트림이 종료될 때까지 대기
•
•
•
•
•
•
•
@Test
void stepVerifer() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
StepVerifier.create(mono)
.expectNext(2)
.verifyComplete();
}
@Test
void stepVerifer() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
StepVerifier.create(mono)
.expectNext(2)
.verifyComplete();
}
Flux/Mono
@Test
void stepVerifer() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
StepVerifier.create(mono)
.expectNext(2)
.verifyComplete();
}
동작을 검증
@Test
void stepVerifer() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
StepVerifier.create(mono)
.expectNext(1)
.verifyComplete();
}
StepVerifier 생성
@Test
void stepVerifer() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
StepVerifier.create(mono)
.expectNext(1)
.verifyComplete();
}
첫번째 데이터 아이템 값
@Test
void stepVerifer() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
StepVerifier.create(mono)
.expectNext(1)
.verifyComplete();
}
스트림 완료
@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();
}
@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 데이터 + 에러발생 스트림
@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
@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
@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
@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();
}
에러나고 종료
•
o
•
o
o
•
o
o
o
o
@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"));
}
@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 호출+로직
@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 요청 준비
@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 실행
@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 상태 코드 처리
@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 변환
@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"));
}
결과에 비즈니스 로직 적용
@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"));
}
예외적인 결과 대응
@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로부터
@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 요청도 간단
Flux 테스트로 작성 - StepVerifier
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());
}
가능은 하지만 과연?
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 유용한 샘플
@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());
}); }
@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의 응답 준비
@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 코드 실행
@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());
}); }
응답 결과 검증
@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 검증
@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"))
}
•
@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 호출
@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 코드
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)
}
}
@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 서비스로 대체 가능
•
o
•
o
o
o
o
o
o
o
public interface HttpHandler {
Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response);
}
•
•
public interface WebHandler {
Mono<Void> handle(ServerWebExchange exchange);
}
•
•
•
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) { … }
}
웹 요청을 처리하고 응답을 만드는 순수한 함수의 모음
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) { … }
}
웹 요청을 담당할 함수 핸들러를 찾음
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로 변환하는 리액티브 핸들러
HttpServer.create().host("localhost").handle(
new ReactorHttpHandlerAdapter(toHttpHandler(
route(path("/hello"),
req -> ok().body(fromObject("Hello Functional")))))
).bind().block();
스프링 컨테이너도 필요없음
•
o
o
o
•
o
o
o
o
o
•
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");
}
}
•
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 생성
•
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 호출하고
•
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 호출 응답 결과 검증
•
var client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
client.get().uri("/hello/{name}", "Spring")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("Hello");
• 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");
• 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");
• 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");
특정 컨트롤러/핸들러만으로
테스트 대상 구성
•
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");
}
•
•
o
•
o
o
o
스프링5 웹플럭스와 테스트 전략

More Related Content

What's hot

シェル芸初心者によるシェル芸入門
シェル芸初心者によるシェル芸入門シェル芸初心者によるシェル芸入門
シェル芸初心者によるシェル芸入門icchy
 
글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례
글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례
글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례if kakao
 
Spring Boot × Vue.jsでSPAを作る
Spring Boot × Vue.jsでSPAを作るSpring Boot × Vue.jsでSPAを作る
Spring Boot × Vue.jsでSPAを作るGo Miyasaka
 
JJUG CCC 2017 Spring Seasar2からSpringへ移行した俺たちのアプリケーションがマイクロサービスアーキテクチャへ歩み始めた
JJUG CCC 2017 Spring Seasar2からSpringへ移行した俺たちのアプリケーションがマイクロサービスアーキテクチャへ歩み始めたJJUG CCC 2017 Spring Seasar2からSpringへ移行した俺たちのアプリケーションがマイクロサービスアーキテクチャへ歩み始めた
JJUG CCC 2017 Spring Seasar2からSpringへ移行した俺たちのアプリケーションがマイクロサービスアーキテクチャへ歩み始めたKoichi Sakata
 
NDC14 - Rx와 Functional Reactive Programming으로 고성능 서버 만들기
NDC14 - Rx와 Functional Reactive Programming으로 고성능 서버 만들기NDC14 - Rx와 Functional Reactive Programming으로 고성능 서버 만들기
NDC14 - Rx와 Functional Reactive Programming으로 고성능 서버 만들기Jong Wook Kim
 
C# Game Server
C# Game ServerC# Game Server
C# Game Serverlactrious
 
[NDC2017 : 박준철] Python 게임 서버 안녕하십니까 - 몬스터 슈퍼리그 게임 서버
[NDC2017 : 박준철] Python 게임 서버 안녕하십니까 - 몬스터 슈퍼리그 게임 서버[NDC2017 : 박준철] Python 게임 서버 안녕하십니까 - 몬스터 슈퍼리그 게임 서버
[NDC2017 : 박준철] Python 게임 서버 안녕하십니까 - 몬스터 슈퍼리그 게임 서버준철 박
 
Airflowで真面目にjob管理
Airflowで真面目にjob管理Airflowで真面目にjob管理
Airflowで真面目にjob管理msssgur
 
Xamarin で良くやっていたあれを MAUI でする話
Xamarin で良くやっていたあれを MAUI でする話Xamarin で良くやっていたあれを MAUI でする話
Xamarin で良くやっていたあれを MAUI でする話m ishizaki
 
自己紹介LT(公開版)
自己紹介LT(公開版)自己紹介LT(公開版)
自己紹介LT(公開版)Ken Muryoi
 
新社会人が今すぐ使える、​ExcelでC#を使う方法
新社会人が今すぐ使える、​ExcelでC#を使う方法新社会人が今すぐ使える、​ExcelでC#を使う方法
新社会人が今すぐ使える、​ExcelでC#を使う方法Tetsuo Honda
 
インフラエンジニアの綺麗で優しい手順書の書き方
インフラエンジニアの綺麗で優しい手順書の書き方インフラエンジニアの綺麗で優しい手順書の書き方
インフラエンジニアの綺麗で優しい手順書の書き方Shohei Koyama
 
nioで作ったBufferedWriterに変えたら例外になった
nioで作ったBufferedWriterに変えたら例外になったnioで作ったBufferedWriterに変えたら例外になった
nioで作ったBufferedWriterに変えたら例外になったchibochibo
 
C++11とゲーム製作
C++11とゲーム製作C++11とゲーム製作
C++11とゲーム製作uchan_nos
 
ネットワークエンジニア的Ansibleの始め方
ネットワークエンジニア的Ansibleの始め方ネットワークエンジニア的Ansibleの始め方
ネットワークエンジニア的Ansibleの始め方akira6592
 
Elastic Stack 을 이용한 게임 서비스 통합 로깅 플랫폼 - elastic{on} 2019 Seoul
Elastic Stack 을 이용한 게임 서비스 통합 로깅 플랫폼 - elastic{on} 2019 SeoulElastic Stack 을 이용한 게임 서비스 통합 로깅 플랫폼 - elastic{on} 2019 Seoul
Elastic Stack 을 이용한 게임 서비스 통합 로깅 플랫폼 - elastic{on} 2019 SeoulSeungYong Oh
 
ASP.NET Core WebAPIでODataを使おう
ASP.NET Core WebAPIでODataを使おうASP.NET Core WebAPIでODataを使おう
ASP.NET Core WebAPIでODataを使おうDevTakas
 

What's hot (20)

シェル芸初心者によるシェル芸入門
シェル芸初心者によるシェル芸入門シェル芸初心者によるシェル芸入門
シェル芸初心者によるシェル芸入門
 
글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례
글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례
글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례
 
Railsで作るBFFの功罪
Railsで作るBFFの功罪Railsで作るBFFの功罪
Railsで作るBFFの功罪
 
Spring Boot × Vue.jsでSPAを作る
Spring Boot × Vue.jsでSPAを作るSpring Boot × Vue.jsでSPAを作る
Spring Boot × Vue.jsでSPAを作る
 
WebSocket / WebRTCの技術紹介
WebSocket / WebRTCの技術紹介WebSocket / WebRTCの技術紹介
WebSocket / WebRTCの技術紹介
 
JJUG CCC 2017 Spring Seasar2からSpringへ移行した俺たちのアプリケーションがマイクロサービスアーキテクチャへ歩み始めた
JJUG CCC 2017 Spring Seasar2からSpringへ移行した俺たちのアプリケーションがマイクロサービスアーキテクチャへ歩み始めたJJUG CCC 2017 Spring Seasar2からSpringへ移行した俺たちのアプリケーションがマイクロサービスアーキテクチャへ歩み始めた
JJUG CCC 2017 Spring Seasar2からSpringへ移行した俺たちのアプリケーションがマイクロサービスアーキテクチャへ歩み始めた
 
Spring AMQP × RabbitMQ
Spring AMQP × RabbitMQSpring AMQP × RabbitMQ
Spring AMQP × RabbitMQ
 
NDC14 - Rx와 Functional Reactive Programming으로 고성능 서버 만들기
NDC14 - Rx와 Functional Reactive Programming으로 고성능 서버 만들기NDC14 - Rx와 Functional Reactive Programming으로 고성능 서버 만들기
NDC14 - Rx와 Functional Reactive Programming으로 고성능 서버 만들기
 
C# Game Server
C# Game ServerC# Game Server
C# Game Server
 
[NDC2017 : 박준철] Python 게임 서버 안녕하십니까 - 몬스터 슈퍼리그 게임 서버
[NDC2017 : 박준철] Python 게임 서버 안녕하십니까 - 몬스터 슈퍼리그 게임 서버[NDC2017 : 박준철] Python 게임 서버 안녕하십니까 - 몬스터 슈퍼리그 게임 서버
[NDC2017 : 박준철] Python 게임 서버 안녕하십니까 - 몬스터 슈퍼리그 게임 서버
 
Airflowで真面目にjob管理
Airflowで真面目にjob管理Airflowで真面目にjob管理
Airflowで真面目にjob管理
 
Xamarin で良くやっていたあれを MAUI でする話
Xamarin で良くやっていたあれを MAUI でする話Xamarin で良くやっていたあれを MAUI でする話
Xamarin で良くやっていたあれを MAUI でする話
 
自己紹介LT(公開版)
自己紹介LT(公開版)自己紹介LT(公開版)
自己紹介LT(公開版)
 
新社会人が今すぐ使える、​ExcelでC#を使う方法
新社会人が今すぐ使える、​ExcelでC#を使う方法新社会人が今すぐ使える、​ExcelでC#を使う方法
新社会人が今すぐ使える、​ExcelでC#を使う方法
 
インフラエンジニアの綺麗で優しい手順書の書き方
インフラエンジニアの綺麗で優しい手順書の書き方インフラエンジニアの綺麗で優しい手順書の書き方
インフラエンジニアの綺麗で優しい手順書の書き方
 
nioで作ったBufferedWriterに変えたら例外になった
nioで作ったBufferedWriterに変えたら例外になったnioで作ったBufferedWriterに変えたら例外になった
nioで作ったBufferedWriterに変えたら例外になった
 
C++11とゲーム製作
C++11とゲーム製作C++11とゲーム製作
C++11とゲーム製作
 
ネットワークエンジニア的Ansibleの始め方
ネットワークエンジニア的Ansibleの始め方ネットワークエンジニア的Ansibleの始め方
ネットワークエンジニア的Ansibleの始め方
 
Elastic Stack 을 이용한 게임 서비스 통합 로깅 플랫폼 - elastic{on} 2019 Seoul
Elastic Stack 을 이용한 게임 서비스 통합 로깅 플랫폼 - elastic{on} 2019 SeoulElastic Stack 을 이용한 게임 서비스 통합 로깅 플랫폼 - elastic{on} 2019 Seoul
Elastic Stack 을 이용한 게임 서비스 통합 로깅 플랫폼 - elastic{on} 2019 Seoul
 
ASP.NET Core WebAPIでODataを使おう
ASP.NET Core WebAPIでODataを使おうASP.NET Core WebAPIでODataを使おう
ASP.NET Core WebAPIでODataを使おう
 

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

Swift, functional programming, and the future of Objective-C
Swift, functional programming, and the future of Objective-CSwift, functional programming, and the future of Objective-C
Swift, functional programming, and the future of Objective-CAlexis Gallagher
 
What can be done with Java, but should better be done with Erlang (@pavlobaron)
What can be done with Java, but should better be done with Erlang (@pavlobaron)What can be done with Java, but should better be done with Erlang (@pavlobaron)
What can be done with Java, but should better be done with Erlang (@pavlobaron)Pavlo Baron
 
Top 10 RxJs Operators in Angular
Top 10 RxJs Operators in Angular Top 10 RxJs Operators in Angular
Top 10 RxJs Operators in Angular Jalpesh Vadgama
 
Julio Capote, Twitter
Julio Capote, TwitterJulio Capote, Twitter
Julio Capote, TwitterOntico
 
Dev Day 2019: Mike Sperber – Software Design für die Seele
Dev Day 2019: Mike Sperber – Software Design für die SeeleDev Day 2019: Mike Sperber – Software Design für die Seele
Dev Day 2019: Mike Sperber – Software Design für die SeeleDevDay Dresden
 
From Java to Parellel Clojure - Clojure South 2019
From Java to Parellel Clojure - Clojure South 2019From Java to Parellel Clojure - Clojure South 2019
From Java to Parellel Clojure - Clojure South 2019Leonardo Borges
 
Hipster oriented programming (Mobilization Lodz 2015)
Hipster oriented programming (Mobilization Lodz 2015)Hipster oriented programming (Mobilization Lodz 2015)
Hipster oriented programming (Mobilization Lodz 2015)Jens Ravens
 
Silex and Twig (PHP Dorset talk)
Silex and Twig (PHP Dorset talk)Silex and Twig (PHP Dorset talk)
Silex and Twig (PHP Dorset talk)Dave Hulbert
 
Who killed object oriented design?
Who killed object oriented design?Who killed object oriented design?
Who killed object oriented design?Amir Barylko
 
SoCal Code Camp 2015: An introduction to Java 8
SoCal Code Camp 2015: An introduction to Java 8SoCal Code Camp 2015: An introduction to Java 8
SoCal Code Camp 2015: An introduction to Java 8Chaitanya Ganoo
 
Introduction to clojure
Introduction to clojureIntroduction to clojure
Introduction to clojureAbbas Raza
 
Дмитрий Нестерук, Паттерны проектирования в XXI веке
Дмитрий Нестерук, Паттерны проектирования в XXI векеДмитрий Нестерук, Паттерны проектирования в XXI веке
Дмитрий Нестерук, Паттерны проектирования в XXI векеSergey Platonov
 
2014 12-08 - odf plugfest - operations based odf editing with ox documents
2014 12-08 - odf plugfest - operations based odf editing with ox documents2014 12-08 - odf plugfest - operations based odf editing with ox documents
2014 12-08 - odf plugfest - operations based odf editing with ox documentsMalte Timmermann
 
ClojurianからみたElixir
ClojurianからみたElixirClojurianからみたElixir
ClojurianからみたElixirKent Ohashi
 

Similar to 스프링5 웹플럭스와 테스트 전략 (20)

Swift, functional programming, and the future of Objective-C
Swift, functional programming, and the future of Objective-CSwift, functional programming, and the future of Objective-C
Swift, functional programming, and the future of Objective-C
 
What can be done with Java, but should better be done with Erlang (@pavlobaron)
What can be done with Java, but should better be done with Erlang (@pavlobaron)What can be done with Java, but should better be done with Erlang (@pavlobaron)
What can be done with Java, but should better be done with Erlang (@pavlobaron)
 
Swoole Overview
Swoole OverviewSwoole Overview
Swoole Overview
 
COScheduler
COSchedulerCOScheduler
COScheduler
 
Fluxish Angular
Fluxish AngularFluxish Angular
Fluxish Angular
 
Top 10 RxJs Operators in Angular
Top 10 RxJs Operators in Angular Top 10 RxJs Operators in Angular
Top 10 RxJs Operators in Angular
 
Julio Capote, Twitter
Julio Capote, TwitterJulio Capote, Twitter
Julio Capote, Twitter
 
Dev Day 2019: Mike Sperber – Software Design für die Seele
Dev Day 2019: Mike Sperber – Software Design für die SeeleDev Day 2019: Mike Sperber – Software Design für die Seele
Dev Day 2019: Mike Sperber – Software Design für die Seele
 
From Java to Parellel Clojure - Clojure South 2019
From Java to Parellel Clojure - Clojure South 2019From Java to Parellel Clojure - Clojure South 2019
From Java to Parellel Clojure - Clojure South 2019
 
Hipster oriented programming (Mobilization Lodz 2015)
Hipster oriented programming (Mobilization Lodz 2015)Hipster oriented programming (Mobilization Lodz 2015)
Hipster oriented programming (Mobilization Lodz 2015)
 
Silex and Twig (PHP Dorset talk)
Silex and Twig (PHP Dorset talk)Silex and Twig (PHP Dorset talk)
Silex and Twig (PHP Dorset talk)
 
Tdd iPhone For Dummies
Tdd iPhone For DummiesTdd iPhone For Dummies
Tdd iPhone For Dummies
 
Who killed object oriented design?
Who killed object oriented design?Who killed object oriented design?
Who killed object oriented design?
 
SoCal Code Camp 2015: An introduction to Java 8
SoCal Code Camp 2015: An introduction to Java 8SoCal Code Camp 2015: An introduction to Java 8
SoCal Code Camp 2015: An introduction to Java 8
 
Introduction to clojure
Introduction to clojureIntroduction to clojure
Introduction to clojure
 
Дмитрий Нестерук, Паттерны проектирования в XXI веке
Дмитрий Нестерук, Паттерны проектирования в XXI векеДмитрий Нестерук, Паттерны проектирования в XXI веке
Дмитрий Нестерук, Паттерны проектирования в XXI веке
 
2014 12-08 - odf plugfest - operations based odf editing with ox documents
2014 12-08 - odf plugfest - operations based odf editing with ox documents2014 12-08 - odf plugfest - operations based odf editing with ox documents
2014 12-08 - odf plugfest - operations based odf editing with ox documents
 
Angular2
Angular2Angular2
Angular2
 
ELAG Workshop version 1
ELAG Workshop version 1ELAG Workshop version 1
ELAG Workshop version 1
 
ClojurianからみたElixir
ClojurianからみたElixirClojurianからみたElixir
ClojurianからみたElixir
 

More from if kakao

바닥부터 시작하는 Vue 테스트와 리팩토링
바닥부터 시작하는 Vue 테스트와 리팩토링바닥부터 시작하는 Vue 테스트와 리팩토링
바닥부터 시작하는 Vue 테스트와 리팩토링if kakao
 
카카오커머스를 지탱하는 Angular
카카오커머스를 지탱하는 Angular카카오커머스를 지탱하는 Angular
카카오커머스를 지탱하는 Angularif kakao
 
프렌즈타임 웹앱 삽질기
프렌즈타임 웹앱 삽질기프렌즈타임 웹앱 삽질기
프렌즈타임 웹앱 삽질기if kakao
 
카프카 기반의 대규모 모니터링 플랫폼 개발이야기
카프카 기반의 대규모 모니터링 플랫폼 개발이야기카프카 기반의 대규모 모니터링 플랫폼 개발이야기
카프카 기반의 대규모 모니터링 플랫폼 개발이야기if kakao
 
TOROS N2 - lightweight approximate Nearest Neighbor library
TOROS N2 - lightweight approximate Nearest Neighbor libraryTOROS N2 - lightweight approximate Nearest Neighbor library
TOROS N2 - lightweight approximate Nearest Neighbor libraryif kakao
 
딥러닝을 이용한 얼굴 인식
딥러닝을 이용한 얼굴 인식딥러닝을 이용한 얼굴 인식
딥러닝을 이용한 얼굴 인식if kakao
 
딥러닝을 활용한 뉴스 메타 태깅
딥러닝을 활용한 뉴스 메타 태깅딥러닝을 활용한 뉴스 메타 태깅
딥러닝을 활용한 뉴스 메타 태깅if kakao
 
눈으로 듣는 음악 추천 시스템
눈으로 듣는 음악 추천 시스템눈으로 듣는 음악 추천 시스템
눈으로 듣는 음악 추천 시스템if kakao
 
Keynote / 2018
Keynote / 2018Keynote / 2018
Keynote / 2018if kakao
 
카카오 봇 플랫폼 소개
카카오 봇 플랫폼 소개카카오 봇 플랫폼 소개
카카오 봇 플랫폼 소개if kakao
 
다음웹툰의 UX(Animation, Transition, Custom View)
다음웹툰의 UX(Animation, Transition, Custom View)다음웹툰의 UX(Animation, Transition, Custom View)
다음웹툰의 UX(Animation, Transition, Custom View)if kakao
 
모바일 게임플랫폼과 인프라 구축 경험기
모바일 게임플랫폼과 인프라 구축 경험기모바일 게임플랫폼과 인프라 구축 경험기
모바일 게임플랫폼과 인프라 구축 경험기if kakao
 
카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개
카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개
카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개if kakao
 
카카오뱅크 모바일앱 개발 이야기
카카오뱅크 모바일앱 개발 이야기카카오뱅크 모바일앱 개발 이야기
카카오뱅크 모바일앱 개발 이야기if kakao
 
다음 모바일 첫 화면 개선기
다음 모바일 첫 화면 개선기다음 모바일 첫 화면 개선기
다음 모바일 첫 화면 개선기if kakao
 
액티브X 없는 블록체인 기반 PKI 시스템
액티브X 없는 블록체인 기반 PKI 시스템액티브X 없는 블록체인 기반 PKI 시스템
액티브X 없는 블록체인 기반 PKI 시스템if kakao
 
Klaytn: Service-Oriented Enterprise-Grade Public Blockchain Platform
Klaytn: Service-Oriented Enterprise-Grade Public Blockchain PlatformKlaytn: Service-Oriented Enterprise-Grade Public Blockchain Platform
Klaytn: Service-Oriented Enterprise-Grade Public Blockchain Platformif kakao
 
Kakao Cloud Native Platform, 9rum
Kakao Cloud Native Platform, 9rumKakao Cloud Native Platform, 9rum
Kakao Cloud Native Platform, 9rumif kakao
 
카프카, 산전수전 노하우
카프카, 산전수전 노하우카프카, 산전수전 노하우
카프카, 산전수전 노하우if kakao
 
카카오톡의 서버사이드 코틀린
카카오톡의 서버사이드 코틀린카카오톡의 서버사이드 코틀린
카카오톡의 서버사이드 코틀린if kakao
 

More from if kakao (20)

바닥부터 시작하는 Vue 테스트와 리팩토링
바닥부터 시작하는 Vue 테스트와 리팩토링바닥부터 시작하는 Vue 테스트와 리팩토링
바닥부터 시작하는 Vue 테스트와 리팩토링
 
카카오커머스를 지탱하는 Angular
카카오커머스를 지탱하는 Angular카카오커머스를 지탱하는 Angular
카카오커머스를 지탱하는 Angular
 
프렌즈타임 웹앱 삽질기
프렌즈타임 웹앱 삽질기프렌즈타임 웹앱 삽질기
프렌즈타임 웹앱 삽질기
 
카프카 기반의 대규모 모니터링 플랫폼 개발이야기
카프카 기반의 대규모 모니터링 플랫폼 개발이야기카프카 기반의 대규모 모니터링 플랫폼 개발이야기
카프카 기반의 대규모 모니터링 플랫폼 개발이야기
 
TOROS N2 - lightweight approximate Nearest Neighbor library
TOROS N2 - lightweight approximate Nearest Neighbor libraryTOROS N2 - lightweight approximate Nearest Neighbor library
TOROS N2 - lightweight approximate Nearest Neighbor library
 
딥러닝을 이용한 얼굴 인식
딥러닝을 이용한 얼굴 인식딥러닝을 이용한 얼굴 인식
딥러닝을 이용한 얼굴 인식
 
딥러닝을 활용한 뉴스 메타 태깅
딥러닝을 활용한 뉴스 메타 태깅딥러닝을 활용한 뉴스 메타 태깅
딥러닝을 활용한 뉴스 메타 태깅
 
눈으로 듣는 음악 추천 시스템
눈으로 듣는 음악 추천 시스템눈으로 듣는 음악 추천 시스템
눈으로 듣는 음악 추천 시스템
 
Keynote / 2018
Keynote / 2018Keynote / 2018
Keynote / 2018
 
카카오 봇 플랫폼 소개
카카오 봇 플랫폼 소개카카오 봇 플랫폼 소개
카카오 봇 플랫폼 소개
 
다음웹툰의 UX(Animation, Transition, Custom View)
다음웹툰의 UX(Animation, Transition, Custom View)다음웹툰의 UX(Animation, Transition, Custom View)
다음웹툰의 UX(Animation, Transition, Custom View)
 
모바일 게임플랫폼과 인프라 구축 경험기
모바일 게임플랫폼과 인프라 구축 경험기모바일 게임플랫폼과 인프라 구축 경험기
모바일 게임플랫폼과 인프라 구축 경험기
 
카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개
카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개
카카오 광고 플랫폼 MSA 적용 사례 및 API Gateway와 인증 구현에 대한 소개
 
카카오뱅크 모바일앱 개발 이야기
카카오뱅크 모바일앱 개발 이야기카카오뱅크 모바일앱 개발 이야기
카카오뱅크 모바일앱 개발 이야기
 
다음 모바일 첫 화면 개선기
다음 모바일 첫 화면 개선기다음 모바일 첫 화면 개선기
다음 모바일 첫 화면 개선기
 
액티브X 없는 블록체인 기반 PKI 시스템
액티브X 없는 블록체인 기반 PKI 시스템액티브X 없는 블록체인 기반 PKI 시스템
액티브X 없는 블록체인 기반 PKI 시스템
 
Klaytn: Service-Oriented Enterprise-Grade Public Blockchain Platform
Klaytn: Service-Oriented Enterprise-Grade Public Blockchain PlatformKlaytn: Service-Oriented Enterprise-Grade Public Blockchain Platform
Klaytn: Service-Oriented Enterprise-Grade Public Blockchain Platform
 
Kakao Cloud Native Platform, 9rum
Kakao Cloud Native Platform, 9rumKakao Cloud Native Platform, 9rum
Kakao Cloud Native Platform, 9rum
 
카프카, 산전수전 노하우
카프카, 산전수전 노하우카프카, 산전수전 노하우
카프카, 산전수전 노하우
 
카카오톡의 서버사이드 코틀린
카카오톡의 서버사이드 코틀린카카오톡의 서버사이드 코틀린
카카오톡의 서버사이드 코틀린
 

Recently uploaded

A healthy diet for your Java application Devoxx France.pdf
A healthy diet for your Java application Devoxx France.pdfA healthy diet for your Java application Devoxx France.pdf
A healthy diet for your Java application Devoxx France.pdfMarharyta Nedzelska
 
Unveiling the Future: Sylius 2.0 New Features
Unveiling the Future: Sylius 2.0 New FeaturesUnveiling the Future: Sylius 2.0 New Features
Unveiling the Future: Sylius 2.0 New FeaturesŁukasz Chruściel
 
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdf
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdfGOING AOT WITH GRAALVM – DEVOXX GREECE.pdf
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdfAlina Yurenko
 
Building a General PDE Solving Framework with Symbolic-Numeric Scientific Mac...
Building a General PDE Solving Framework with Symbolic-Numeric Scientific Mac...Building a General PDE Solving Framework with Symbolic-Numeric Scientific Mac...
Building a General PDE Solving Framework with Symbolic-Numeric Scientific Mac...stazi3110
 
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...confluent
 
Introduction Computer Science - Software Design.pdf
Introduction Computer Science - Software Design.pdfIntroduction Computer Science - Software Design.pdf
Introduction Computer Science - Software Design.pdfFerryKemperman
 
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte GermanySuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte GermanyChristoph Pohl
 
What is Fashion PLM and Why Do You Need It
What is Fashion PLM and Why Do You Need ItWhat is Fashion PLM and Why Do You Need It
What is Fashion PLM and Why Do You Need ItWave PLM
 
Intelligent Home Wi-Fi Solutions | ThinkPalm
Intelligent Home Wi-Fi Solutions | ThinkPalmIntelligent Home Wi-Fi Solutions | ThinkPalm
Intelligent Home Wi-Fi Solutions | ThinkPalmSujith Sukumaran
 
Implementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureImplementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureDinusha Kumarasiri
 
Xen Safety Embedded OSS Summit April 2024 v4.pdf
Xen Safety Embedded OSS Summit April 2024 v4.pdfXen Safety Embedded OSS Summit April 2024 v4.pdf
Xen Safety Embedded OSS Summit April 2024 v4.pdfStefano Stabellini
 
Cloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEECloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEEVICTOR MAESTRE RAMIREZ
 
What is Advanced Excel and what are some best practices for designing and cre...
What is Advanced Excel and what are some best practices for designing and cre...What is Advanced Excel and what are some best practices for designing and cre...
What is Advanced Excel and what are some best practices for designing and cre...Technogeeks
 
Ahmed Motair CV April 2024 (Senior SW Developer)
Ahmed Motair CV April 2024 (Senior SW Developer)Ahmed Motair CV April 2024 (Senior SW Developer)
Ahmed Motair CV April 2024 (Senior SW Developer)Ahmed Mater
 
Software Project Health Check: Best Practices and Techniques for Your Product...
Software Project Health Check: Best Practices and Techniques for Your Product...Software Project Health Check: Best Practices and Techniques for Your Product...
Software Project Health Check: Best Practices and Techniques for Your Product...Velvetech LLC
 
Best Web Development Agency- Idiosys USA.pdf
Best Web Development Agency- Idiosys USA.pdfBest Web Development Agency- Idiosys USA.pdf
Best Web Development Agency- Idiosys USA.pdfIdiosysTechnologies1
 
Open Source Summit NA 2024: Open Source Cloud Costs - OpenCost's Impact on En...
Open Source Summit NA 2024: Open Source Cloud Costs - OpenCost's Impact on En...Open Source Summit NA 2024: Open Source Cloud Costs - OpenCost's Impact on En...
Open Source Summit NA 2024: Open Source Cloud Costs - OpenCost's Impact on En...Matt Ray
 

Recently uploaded (20)

A healthy diet for your Java application Devoxx France.pdf
A healthy diet for your Java application Devoxx France.pdfA healthy diet for your Java application Devoxx France.pdf
A healthy diet for your Java application Devoxx France.pdf
 
Advantages of Odoo ERP 17 for Your Business
Advantages of Odoo ERP 17 for Your BusinessAdvantages of Odoo ERP 17 for Your Business
Advantages of Odoo ERP 17 for Your Business
 
Unveiling the Future: Sylius 2.0 New Features
Unveiling the Future: Sylius 2.0 New FeaturesUnveiling the Future: Sylius 2.0 New Features
Unveiling the Future: Sylius 2.0 New Features
 
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdf
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdfGOING AOT WITH GRAALVM – DEVOXX GREECE.pdf
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdf
 
2.pdf Ejercicios de programación competitiva
2.pdf Ejercicios de programación competitiva2.pdf Ejercicios de programación competitiva
2.pdf Ejercicios de programación competitiva
 
Building a General PDE Solving Framework with Symbolic-Numeric Scientific Mac...
Building a General PDE Solving Framework with Symbolic-Numeric Scientific Mac...Building a General PDE Solving Framework with Symbolic-Numeric Scientific Mac...
Building a General PDE Solving Framework with Symbolic-Numeric Scientific Mac...
 
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...
 
Introduction Computer Science - Software Design.pdf
Introduction Computer Science - Software Design.pdfIntroduction Computer Science - Software Design.pdf
Introduction Computer Science - Software Design.pdf
 
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte GermanySuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
 
Hot Sexy call girls in Patel Nagar🔝 9953056974 🔝 escort Service
Hot Sexy call girls in Patel Nagar🔝 9953056974 🔝 escort ServiceHot Sexy call girls in Patel Nagar🔝 9953056974 🔝 escort Service
Hot Sexy call girls in Patel Nagar🔝 9953056974 🔝 escort Service
 
What is Fashion PLM and Why Do You Need It
What is Fashion PLM and Why Do You Need ItWhat is Fashion PLM and Why Do You Need It
What is Fashion PLM and Why Do You Need It
 
Intelligent Home Wi-Fi Solutions | ThinkPalm
Intelligent Home Wi-Fi Solutions | ThinkPalmIntelligent Home Wi-Fi Solutions | ThinkPalm
Intelligent Home Wi-Fi Solutions | ThinkPalm
 
Implementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureImplementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with Azure
 
Xen Safety Embedded OSS Summit April 2024 v4.pdf
Xen Safety Embedded OSS Summit April 2024 v4.pdfXen Safety Embedded OSS Summit April 2024 v4.pdf
Xen Safety Embedded OSS Summit April 2024 v4.pdf
 
Cloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEECloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEE
 
What is Advanced Excel and what are some best practices for designing and cre...
What is Advanced Excel and what are some best practices for designing and cre...What is Advanced Excel and what are some best practices for designing and cre...
What is Advanced Excel and what are some best practices for designing and cre...
 
Ahmed Motair CV April 2024 (Senior SW Developer)
Ahmed Motair CV April 2024 (Senior SW Developer)Ahmed Motair CV April 2024 (Senior SW Developer)
Ahmed Motair CV April 2024 (Senior SW Developer)
 
Software Project Health Check: Best Practices and Techniques for Your Product...
Software Project Health Check: Best Practices and Techniques for Your Product...Software Project Health Check: Best Practices and Techniques for Your Product...
Software Project Health Check: Best Practices and Techniques for Your Product...
 
Best Web Development Agency- Idiosys USA.pdf
Best Web Development Agency- Idiosys USA.pdfBest Web Development Agency- Idiosys USA.pdf
Best Web Development Agency- Idiosys USA.pdf
 
Open Source Summit NA 2024: Open Source Cloud Costs - OpenCost's Impact on En...
Open Source Summit NA 2024: Open Source Cloud Costs - OpenCost's Impact on En...Open Source Summit NA 2024: Open Source Cloud Costs - OpenCost's Impact on En...
Open Source Summit NA 2024: Open Source Cloud Costs - OpenCost's Impact on En...
 

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

  • 1.
  • 2. 호주에 거주하고 있지만 여러 나라를 다니며 일하기 좋아하는 개발자. 오래전 스 프링 프레임워크의 매력에 빠진 뒤로 스프링의 변화에 맞춰 기술을 공부하고 이 를 이용해 일을 하는 것을 좋아한다. 최신 스프링에 추가된 리액티브 함수형 프로 그래밍 기술을 블록체인 코어 개발을 비롯한 여러 분야에 적용해보고 있다. 토비 의 스프링이라는 책을 썼고, 유튜브에서 토비의 봄 TV라는 코딩 방송을 하기도 한 다.
  • 5.
  • 7. q
  • 8. q
  • 9. q
  • 10. q
  • 11. q
  • 12. q ?
  • 13. q
  • 14. q
  • 15.
  • 16.
  • 17.
  • 21.
  • 23.
  • 28.
  • 31. 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; } }
  • 32. 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
  • 33. 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; }
  • 34. 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
  • 35. 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; } 비동기 결과처리를 위한 콜백 매 단계마다 중첩
  • 36. 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; } 매 단계마다 반복되는 예외 콜백
  • 37. public CompletableFuture<UserOrder> asyncOrders2(String email) { return asyncFindUser2(email) .thenCompose(user -> asyncOrders2(user)) .thenApply(orders -> new UserOrder(email, orders)) .exceptionally(e -> UserOrder.FAIL); }
  • 38. public CompletableFuture<UserOrder> asyncOrders2(String email) { return asyncFindUser2(email) .thenCompose(user -> asyncOrders2(user)) .thenApply(orders -> new UserOrder(email, orders)) .exceptionally(e -> UserOrder.FAIL); } 비동기 결과처리 함수 이용 중첩되지 않음
  • 39. 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 예외처리 단일화
  • 40. 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+ 빌드타임, 로딩타임, 런타임 코드생성
  • 41. public Mono<UserOrder> asyncOrders4(String email) { return asyncFindUser4(email) .flatMap(user -> asyncGetOrders4(user)) .map(orders -> new UserOrder(email, orders)) .onErrorReturn(UserOrder.FAIL); } WebClient 이용 CompletableFuture와 유사해 보이지만…
  • 42. 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)
  • 43. Mono<User> -> Mono<List<Order>> -> Mono<UserOrder>
  • 45.
  • 46.
  • 47.
  • 48.
  • 50. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(1)); }
  • 51. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(1)); }
  • 52. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(1)); }
  • 53. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(1)); }
  • 54. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(1)); } 테스트 성공!!
  • 55. @Test void mono() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); mono.subscribe(item -> assertThat(item).isEqualTo(2)); } 테스트 성공???
  • 56. @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(); } 테스트가 끝나지 않음!!
  • 57. @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); } 테스트에서 동시성을 제어해야 하는 번거로움!
  • 58. @Test void mono5() throws InterruptedException { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); Integer item = mono.block(); assertThat(item).isEqualTo(1); }
  • 59. @Test void mono5() throws InterruptedException { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); Integer item = mono.block(); assertThat(item).isEqualTo(1); } 데이터 스트림이 종료될 때까지 대기
  • 62. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(2) .verifyComplete(); }
  • 63. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(2) .verifyComplete(); } Flux/Mono
  • 64. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(2) .verifyComplete(); } 동작을 검증
  • 65. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(1) .verifyComplete(); } StepVerifier 생성
  • 66. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(1) .verifyComplete(); } 첫번째 데이터 아이템 값
  • 67. @Test void stepVerifer() { Mono<Integer> mono = Mono.just(1) .subscribeOn(Schedulers.single()); StepVerifier.create(mono) .expectNext(1) .verifyComplete(); } 스트림 완료
  • 68. @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(); }
  • 69. @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 데이터 + 에러발생 스트림
  • 70. @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
  • 71. @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
  • 72. @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
  • 73. @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(); } 에러나고 종료
  • 74.
  • 76. @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")); }
  • 77. @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 호출+로직
  • 78. @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 요청 준비
  • 79. @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 실행
  • 80. @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 상태 코드 처리
  • 81. @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 변환
  • 82. @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")); } 결과에 비즈니스 로직 적용
  • 83. @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")); } 예외적인 결과 대응
  • 84. @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로부터
  • 85. @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 요청도 간단
  • 86. Flux 테스트로 작성 - StepVerifier
  • 87.
  • 88. 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()); } 가능은 하지만 과연?
  • 89.
  • 90. 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 유용한 샘플
  • 91. @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()); }); }
  • 92. @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의 응답 준비
  • 93. @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 코드 실행
  • 94. @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()); }); } 응답 결과 검증
  • 95. @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 검증
  • 96. @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")) } •
  • 97. @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 호출
  • 98. @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 코드
  • 99. 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) } }
  • 100. @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 서비스로 대체 가능
  • 101.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107. public interface HttpHandler { Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response); } • •
  • 108. public interface WebHandler { Mono<Void> handle(ServerWebExchange exchange); } • • •
  • 109. 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) { … } } 웹 요청을 처리하고 응답을 만드는 순수한 함수의 모음
  • 110. 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) { … } } 웹 요청을 담당할 함수 핸들러를 찾음
  • 111. 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로 변환하는 리액티브 핸들러
  • 112. HttpServer.create().host("localhost").handle( new ReactorHttpHandlerAdapter(toHttpHandler( route(path("/hello"), req -> ok().body(fromObject("Hello Functional"))))) ).bind().block(); 스프링 컨테이너도 필요없음
  • 113.
  • 115.
  • 116. • 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"); } }
  • 117. • 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 생성
  • 118. • 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 호출하고
  • 119. • 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 호출 응답 결과 검증
  • 120.
  • 121. • var client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build(); client.get().uri("/hello/{name}", "Spring") .exchange() .expectStatus() .isOk() .expectBody(String.class) .isEqualTo("Hello");
  • 122. • 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");
  • 123. • 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");
  • 124. • 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"); 특정 컨트롤러/핸들러만으로 테스트 대상 구성
  • 125. • 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"); }
  • 126.