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

Spring Framework - AOP
Spring Framework - AOPSpring Framework - AOP
Spring Framework - AOPDzmitry Naskou
 
Core java concepts
Core java  conceptsCore java  concepts
Core java conceptsRam132
 
Angular - Chapter 4 - Data and Event Handling
 Angular - Chapter 4 - Data and Event Handling Angular - Chapter 4 - Data and Event Handling
Angular - Chapter 4 - Data and Event HandlingWebStackAcademy
 
#살아있다 #자프링외길12년차 #코프링2개월생존기
#살아있다 #자프링외길12년차 #코프링2개월생존기#살아있다 #자프링외길12년차 #코프링2개월생존기
#살아있다 #자프링외길12년차 #코프링2개월생존기Arawn Park
 
Kotlin Jetpack Tutorial
Kotlin Jetpack TutorialKotlin Jetpack Tutorial
Kotlin Jetpack TutorialSimplilearn
 
Spring fest2020 spring-security
Spring fest2020 spring-securitySpring fest2020 spring-security
Spring fest2020 spring-security土岐 孝平
 
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)Heungsub Lee
 
도메인 주도 설계의 본질
도메인 주도 설계의 본질도메인 주도 설계의 본질
도메인 주도 설계의 본질Young-Ho Cho
 
Multithread & shared_ptr
Multithread & shared_ptrMultithread & shared_ptr
Multithread & shared_ptr내훈 정
 
ES6 presentation
ES6 presentationES6 presentation
ES6 presentationritika1
 
스프링 부트와 로깅
스프링 부트와 로깅스프링 부트와 로깅
스프링 부트와 로깅Keesun Baik
 
Java 8 Lambda Expressions & Streams
Java 8 Lambda Expressions & StreamsJava 8 Lambda Expressions & Streams
Java 8 Lambda Expressions & StreamsNewCircle Training
 
Large Table Partitioning with PostgreSQL and Django
 Large Table Partitioning with PostgreSQL and Django Large Table Partitioning with PostgreSQL and Django
Large Table Partitioning with PostgreSQL and DjangoEDB
 
MVC の Model を考える
MVC の Model を考えるMVC の Model を考える
MVC の Model を考えるtomo_masakura
 
ワタシはSingletonがキライだ
ワタシはSingletonがキライだワタシはSingletonがキライだ
ワタシはSingletonがキライだTetsuya Kaneuchi
 
ISUCONの勝ち方 YAPC::Asia Tokyo 2015
ISUCONの勝ち方 YAPC::Asia Tokyo 2015ISUCONの勝ち方 YAPC::Asia Tokyo 2015
ISUCONの勝ち方 YAPC::Asia Tokyo 2015Masahiro Nagano
 

What's hot (20)

Spring Batch
Spring BatchSpring Batch
Spring Batch
 
Spring Framework - AOP
Spring Framework - AOPSpring Framework - AOP
Spring Framework - AOP
 
Core java concepts
Core java  conceptsCore java  concepts
Core java concepts
 
Angular - Chapter 4 - Data and Event Handling
 Angular - Chapter 4 - Data and Event Handling Angular - Chapter 4 - Data and Event Handling
Angular - Chapter 4 - Data and Event Handling
 
#살아있다 #자프링외길12년차 #코프링2개월생존기
#살아있다 #자프링외길12년차 #코프링2개월생존기#살아있다 #자프링외길12년차 #코프링2개월생존기
#살아있다 #자프링외길12년차 #코프링2개월생존기
 
PHP MVC
PHP MVCPHP MVC
PHP MVC
 
Kotlin Jetpack Tutorial
Kotlin Jetpack TutorialKotlin Jetpack Tutorial
Kotlin Jetpack Tutorial
 
Spring fest2020 spring-security
Spring fest2020 spring-securitySpring fest2020 spring-security
Spring fest2020 spring-security
 
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
[야생의 땅: 듀랑고] 서버 아키텍처 Vol. 2 (자막)
 
도메인 주도 설계의 본질
도메인 주도 설계의 본질도메인 주도 설계의 본질
도메인 주도 설계의 본질
 
Multithread & shared_ptr
Multithread & shared_ptrMultithread & shared_ptr
Multithread & shared_ptr
 
ES6 presentation
ES6 presentationES6 presentation
ES6 presentation
 
스프링 부트와 로깅
스프링 부트와 로깅스프링 부트와 로깅
스프링 부트와 로깅
 
Java 8 Lambda Expressions & Streams
Java 8 Lambda Expressions & StreamsJava 8 Lambda Expressions & Streams
Java 8 Lambda Expressions & Streams
 
JavaScript Promises
JavaScript PromisesJavaScript Promises
JavaScript Promises
 
Large Table Partitioning with PostgreSQL and Django
 Large Table Partitioning with PostgreSQL and Django Large Table Partitioning with PostgreSQL and Django
Large Table Partitioning with PostgreSQL and Django
 
MVC の Model を考える
MVC の Model を考えるMVC の Model を考える
MVC の Model を考える
 
Angular Data Binding
Angular Data BindingAngular Data Binding
Angular Data Binding
 
ワタシはSingletonがキライだ
ワタシはSingletonがキライだワタシはSingletonがキライだ
ワタシはSingletonがキライだ
 
ISUCONの勝ち方 YAPC::Asia Tokyo 2015
ISUCONの勝ち方 YAPC::Asia Tokyo 2015ISUCONの勝ち方 YAPC::Asia Tokyo 2015
ISUCONの勝ち方 YAPC::Asia Tokyo 2015
 

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
 
글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례
글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례
글로벌 게임 플랫폼에서 무정지, 무점검 서버 개발과 운영 사례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
 

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

Call Girls in Naraina Delhi 💯Call Us 🔝8264348440🔝
Call Girls in Naraina Delhi 💯Call Us 🔝8264348440🔝Call Girls in Naraina Delhi 💯Call Us 🔝8264348440🔝
Call Girls in Naraina Delhi 💯Call Us 🔝8264348440🔝soniya singh
 
Building Real-Time Data Pipelines: Stream & Batch Processing workshop Slide
Building Real-Time Data Pipelines: Stream & Batch Processing workshop SlideBuilding Real-Time Data Pipelines: Stream & Batch Processing workshop Slide
Building Real-Time Data Pipelines: Stream & Batch Processing workshop SlideChristina Lin
 
Unit 1.1 Excite Part 1, class 9, cbse...
Unit 1.1 Excite Part 1, class 9, cbse...Unit 1.1 Excite Part 1, class 9, cbse...
Unit 1.1 Excite Part 1, class 9, cbse...aditisharan08
 
EY_Graph Database Powered Sustainability
EY_Graph Database Powered SustainabilityEY_Graph Database Powered Sustainability
EY_Graph Database Powered SustainabilityNeo4j
 
buds n tech IT solutions
buds n  tech IT                solutionsbuds n  tech IT                solutions
buds n tech IT solutionsmonugehlot87
 
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed Data
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed DataAlluxio Monthly Webinar | Cloud-Native Model Training on Distributed Data
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed DataAlluxio, Inc.
 
chapter--4-software-project-planning.ppt
chapter--4-software-project-planning.pptchapter--4-software-project-planning.ppt
chapter--4-software-project-planning.pptkotipi9215
 
Russian Call Girls in Karol Bagh Aasnvi ➡️ 8264348440 💋📞 Independent Escort S...
Russian Call Girls in Karol Bagh Aasnvi ➡️ 8264348440 💋📞 Independent Escort S...Russian Call Girls in Karol Bagh Aasnvi ➡️ 8264348440 💋📞 Independent Escort S...
Russian Call Girls in Karol Bagh Aasnvi ➡️ 8264348440 💋📞 Independent Escort S...soniya singh
 
Cloud Management Software Platforms: OpenStack
Cloud Management Software Platforms: OpenStackCloud Management Software Platforms: OpenStack
Cloud Management Software Platforms: OpenStackVICTOR MAESTRE RAMIREZ
 
why an Opensea Clone Script might be your perfect match.pdf
why an Opensea Clone Script might be your perfect match.pdfwhy an Opensea Clone Script might be your perfect match.pdf
why an Opensea Clone Script might be your perfect match.pdfjoe51371421
 
Professional Resume Template for Software Developers
Professional Resume Template for Software DevelopersProfessional Resume Template for Software Developers
Professional Resume Template for Software DevelopersVinodh Ram
 
Engage Usergroup 2024 - The Good The Bad_The Ugly
Engage Usergroup 2024 - The Good The Bad_The UglyEngage Usergroup 2024 - The Good The Bad_The Ugly
Engage Usergroup 2024 - The Good The Bad_The UglyFrank van der Linden
 
XpertSolvers: Your Partner in Building Innovative Software Solutions
XpertSolvers: Your Partner in Building Innovative Software SolutionsXpertSolvers: Your Partner in Building Innovative Software Solutions
XpertSolvers: Your Partner in Building Innovative Software SolutionsMehedi Hasan Shohan
 
Der Spagat zwischen BIAS und FAIRNESS (2024)
Der Spagat zwischen BIAS und FAIRNESS (2024)Der Spagat zwischen BIAS und FAIRNESS (2024)
Der Spagat zwischen BIAS und FAIRNESS (2024)OPEN KNOWLEDGE GmbH
 
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...MyIntelliSource, Inc.
 
What is Binary Language? Computer Number Systems
What is Binary Language?  Computer Number SystemsWhat is Binary Language?  Computer Number Systems
What is Binary Language? Computer Number SystemsJheuzeDellosa
 
Implementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureImplementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureDinusha Kumarasiri
 
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptxKnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptxTier1 app
 
Asset Management Software - Infographic
Asset Management Software - InfographicAsset Management Software - Infographic
Asset Management Software - InfographicHr365.us smith
 

Recently uploaded (20)

Call Girls in Naraina Delhi 💯Call Us 🔝8264348440🔝
Call Girls in Naraina Delhi 💯Call Us 🔝8264348440🔝Call Girls in Naraina Delhi 💯Call Us 🔝8264348440🔝
Call Girls in Naraina Delhi 💯Call Us 🔝8264348440🔝
 
Building Real-Time Data Pipelines: Stream & Batch Processing workshop Slide
Building Real-Time Data Pipelines: Stream & Batch Processing workshop SlideBuilding Real-Time Data Pipelines: Stream & Batch Processing workshop Slide
Building Real-Time Data Pipelines: Stream & Batch Processing workshop Slide
 
Unit 1.1 Excite Part 1, class 9, cbse...
Unit 1.1 Excite Part 1, class 9, cbse...Unit 1.1 Excite Part 1, class 9, cbse...
Unit 1.1 Excite Part 1, class 9, cbse...
 
EY_Graph Database Powered Sustainability
EY_Graph Database Powered SustainabilityEY_Graph Database Powered Sustainability
EY_Graph Database Powered Sustainability
 
buds n tech IT solutions
buds n  tech IT                solutionsbuds n  tech IT                solutions
buds n tech IT solutions
 
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed Data
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed DataAlluxio Monthly Webinar | Cloud-Native Model Training on Distributed Data
Alluxio Monthly Webinar | Cloud-Native Model Training on Distributed Data
 
chapter--4-software-project-planning.ppt
chapter--4-software-project-planning.pptchapter--4-software-project-planning.ppt
chapter--4-software-project-planning.ppt
 
Russian Call Girls in Karol Bagh Aasnvi ➡️ 8264348440 💋📞 Independent Escort S...
Russian Call Girls in Karol Bagh Aasnvi ➡️ 8264348440 💋📞 Independent Escort S...Russian Call Girls in Karol Bagh Aasnvi ➡️ 8264348440 💋📞 Independent Escort S...
Russian Call Girls in Karol Bagh Aasnvi ➡️ 8264348440 💋📞 Independent Escort S...
 
Cloud Management Software Platforms: OpenStack
Cloud Management Software Platforms: OpenStackCloud Management Software Platforms: OpenStack
Cloud Management Software Platforms: OpenStack
 
why an Opensea Clone Script might be your perfect match.pdf
why an Opensea Clone Script might be your perfect match.pdfwhy an Opensea Clone Script might be your perfect match.pdf
why an Opensea Clone Script might be your perfect match.pdf
 
Professional Resume Template for Software Developers
Professional Resume Template for Software DevelopersProfessional Resume Template for Software Developers
Professional Resume Template for Software Developers
 
Engage Usergroup 2024 - The Good The Bad_The Ugly
Engage Usergroup 2024 - The Good The Bad_The UglyEngage Usergroup 2024 - The Good The Bad_The Ugly
Engage Usergroup 2024 - The Good The Bad_The Ugly
 
XpertSolvers: Your Partner in Building Innovative Software Solutions
XpertSolvers: Your Partner in Building Innovative Software SolutionsXpertSolvers: Your Partner in Building Innovative Software Solutions
XpertSolvers: Your Partner in Building Innovative Software Solutions
 
Call Girls In Mukherjee Nagar 📱 9999965857 🤩 Delhi 🫦 HOT AND SEXY VVIP 🍎 SE...
Call Girls In Mukherjee Nagar 📱  9999965857  🤩 Delhi 🫦 HOT AND SEXY VVIP 🍎 SE...Call Girls In Mukherjee Nagar 📱  9999965857  🤩 Delhi 🫦 HOT AND SEXY VVIP 🍎 SE...
Call Girls In Mukherjee Nagar 📱 9999965857 🤩 Delhi 🫦 HOT AND SEXY VVIP 🍎 SE...
 
Der Spagat zwischen BIAS und FAIRNESS (2024)
Der Spagat zwischen BIAS und FAIRNESS (2024)Der Spagat zwischen BIAS und FAIRNESS (2024)
Der Spagat zwischen BIAS und FAIRNESS (2024)
 
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
 
What is Binary Language? Computer Number Systems
What is Binary Language?  Computer Number SystemsWhat is Binary Language?  Computer Number Systems
What is Binary Language? Computer Number Systems
 
Implementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureImplementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with Azure
 
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptxKnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
 
Asset Management Software - Infographic
Asset Management Software - InfographicAsset Management Software - Infographic
Asset Management Software - Infographic
 

스프링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.