SlideShare uma empresa Scribd logo
1 de 128
Baixar para ler 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 웹플럭스와 테스트 전략

Mais conteúdo relacionado

Mais procurados

RESTful API 설계
RESTful API 설계RESTful API 설계
RESTful API 설계Jinho Yoo
 
스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해beom kyun choi
 
Scalable JavaScript Application Architecture
Scalable JavaScript Application ArchitectureScalable JavaScript Application Architecture
Scalable JavaScript Application ArchitectureNicholas Zakas
 
Service workers
Service workersService workers
Service workersjungkees
 
Intro to OAuth2 and OpenID Connect
Intro to OAuth2 and OpenID ConnectIntro to OAuth2 and OpenID Connect
Intro to OAuth2 and OpenID ConnectLiamWadman
 
Utilizing kotlin flows in an android application
Utilizing kotlin flows in an android applicationUtilizing kotlin flows in an android application
Utilizing kotlin flows in an android applicationSeven Peaks Speaks
 
OAuth 2.0 and OpenId Connect
OAuth 2.0 and OpenId ConnectOAuth 2.0 and OpenId Connect
OAuth 2.0 and OpenId ConnectSaran Doraiswamy
 
WAF Bypass Techniques - Using HTTP Standard and Web Servers’ Behaviour
WAF Bypass Techniques - Using HTTP Standard and Web Servers’ BehaviourWAF Bypass Techniques - Using HTTP Standard and Web Servers’ Behaviour
WAF Bypass Techniques - Using HTTP Standard and Web Servers’ BehaviourSoroush Dalili
 
REST API and CRUD
REST API and CRUDREST API and CRUD
REST API and CRUDPrem Sanil
 
ASP.NET Core MVC + Web API with Overview
ASP.NET Core MVC + Web API with OverviewASP.NET Core MVC + Web API with Overview
ASP.NET Core MVC + Web API with OverviewShahed Chowdhuri
 
Kotlin @ Coupang Backend 2017
Kotlin @ Coupang Backend 2017Kotlin @ Coupang Backend 2017
Kotlin @ Coupang Backend 2017Sunghyouk Bae
 
An Overview of Deserialization Vulnerabilities in the Java Virtual Machine (J...
An Overview of Deserialization Vulnerabilities in the Java Virtual Machine (J...An Overview of Deserialization Vulnerabilities in the Java Virtual Machine (J...
An Overview of Deserialization Vulnerabilities in the Java Virtual Machine (J...joaomatosf_
 

Mais procurados (20)

RESTful API 설계
RESTful API 설계RESTful API 설계
RESTful API 설계
 
스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해스프링 시큐리티 구조 이해
스프링 시큐리티 구조 이해
 
Express node js
Express node jsExpress node js
Express node js
 
Scalable JavaScript Application Architecture
Scalable JavaScript Application ArchitectureScalable JavaScript Application Architecture
Scalable JavaScript Application Architecture
 
Service workers
Service workersService workers
Service workers
 
OpenID Connect Explained
OpenID Connect ExplainedOpenID Connect Explained
OpenID Connect Explained
 
Intro to OAuth2 and OpenID Connect
Intro to OAuth2 and OpenID ConnectIntro to OAuth2 and OpenID Connect
Intro to OAuth2 and OpenID Connect
 
Webauthn Tutorial
Webauthn TutorialWebauthn Tutorial
Webauthn Tutorial
 
Spring aop
Spring aopSpring aop
Spring aop
 
Java reflection
Java reflectionJava reflection
Java reflection
 
Django Girls Tutorial
Django Girls TutorialDjango Girls Tutorial
Django Girls Tutorial
 
Rest API
Rest APIRest API
Rest API
 
Utilizing kotlin flows in an android application
Utilizing kotlin flows in an android applicationUtilizing kotlin flows in an android application
Utilizing kotlin flows in an android application
 
OAuth 2.0 and OpenId Connect
OAuth 2.0 and OpenId ConnectOAuth 2.0 and OpenId Connect
OAuth 2.0 and OpenId Connect
 
WAF Bypass Techniques - Using HTTP Standard and Web Servers’ Behaviour
WAF Bypass Techniques - Using HTTP Standard and Web Servers’ BehaviourWAF Bypass Techniques - Using HTTP Standard and Web Servers’ Behaviour
WAF Bypass Techniques - Using HTTP Standard and Web Servers’ Behaviour
 
REST API and CRUD
REST API and CRUDREST API and CRUD
REST API and CRUD
 
Node.js Express Framework
Node.js Express FrameworkNode.js Express Framework
Node.js Express Framework
 
ASP.NET Core MVC + Web API with Overview
ASP.NET Core MVC + Web API with OverviewASP.NET Core MVC + Web API with Overview
ASP.NET Core MVC + Web API with Overview
 
Kotlin @ Coupang Backend 2017
Kotlin @ Coupang Backend 2017Kotlin @ Coupang Backend 2017
Kotlin @ Coupang Backend 2017
 
An Overview of Deserialization Vulnerabilities in the Java Virtual Machine (J...
An Overview of Deserialization Vulnerabilities in the Java Virtual Machine (J...An Overview of Deserialization Vulnerabilities in the Java Virtual Machine (J...
An Overview of Deserialization Vulnerabilities in the Java Virtual Machine (J...
 

Semelhante a 스프링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
 

Semelhante a 스프링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
 

Mais de 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
 

Mais de 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
 
카프카, 산전수전 노하우
카프카, 산전수전 노하우카프카, 산전수전 노하우
카프카, 산전수전 노하우
 

Último

What’s New in VictoriaMetrics: Q1 2024 Updates
What’s New in VictoriaMetrics: Q1 2024 UpdatesWhat’s New in VictoriaMetrics: Q1 2024 Updates
What’s New in VictoriaMetrics: Q1 2024 UpdatesVictoriaMetrics
 
Powering Real-Time Decisions with Continuous Data Streams
Powering Real-Time Decisions with Continuous Data StreamsPowering Real-Time Decisions with Continuous Data Streams
Powering Real-Time Decisions with Continuous Data StreamsSafe Software
 
Amazon Bedrock in Action - presentation of the Bedrock's capabilities
Amazon Bedrock in Action - presentation of the Bedrock's capabilitiesAmazon Bedrock in Action - presentation of the Bedrock's capabilities
Amazon Bedrock in Action - presentation of the Bedrock's capabilitiesKrzysztofKkol1
 
JavaLand 2024 - Going serverless with Quarkus GraalVM native images and AWS L...
JavaLand 2024 - Going serverless with Quarkus GraalVM native images and AWS L...JavaLand 2024 - Going serverless with Quarkus GraalVM native images and AWS L...
JavaLand 2024 - Going serverless with Quarkus GraalVM native images and AWS L...Bert Jan Schrijver
 
2024-04-09 - From Complexity to Clarity - AWS Summit AMS.pdf
2024-04-09 - From Complexity to Clarity - AWS Summit AMS.pdf2024-04-09 - From Complexity to Clarity - AWS Summit AMS.pdf
2024-04-09 - From Complexity to Clarity - AWS Summit AMS.pdfAndrey Devyatkin
 
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptx
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptxReal-time Tracking and Monitoring with Cargo Cloud Solutions.pptx
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptxRTS corp
 
SAM Training Session - How to use EXCEL ?
SAM Training Session - How to use EXCEL ?SAM Training Session - How to use EXCEL ?
SAM Training Session - How to use EXCEL ?Alexandre Beguel
 
eSoftTools IMAP Backup Software and migration tools
eSoftTools IMAP Backup Software and migration toolseSoftTools IMAP Backup Software and migration tools
eSoftTools IMAP Backup Software and migration toolsosttopstonverter
 
Leveraging AI for Mobile App Testing on Real Devices | Applitools + Kobiton
Leveraging AI for Mobile App Testing on Real Devices | Applitools + KobitonLeveraging AI for Mobile App Testing on Real Devices | Applitools + Kobiton
Leveraging AI for Mobile App Testing on Real Devices | Applitools + KobitonApplitools
 
SensoDat: Simulation-based Sensor Dataset of Self-driving Cars
SensoDat: Simulation-based Sensor Dataset of Self-driving CarsSensoDat: Simulation-based Sensor Dataset of Self-driving Cars
SensoDat: Simulation-based Sensor Dataset of Self-driving CarsChristian Birchler
 
Understanding Flamingo - DeepMind's VLM Architecture
Understanding Flamingo - DeepMind's VLM ArchitectureUnderstanding Flamingo - DeepMind's VLM Architecture
Understanding Flamingo - DeepMind's VLM Architecturerahul_net
 
VictoriaMetrics Q1 Meet Up '24 - Community & News Update
VictoriaMetrics Q1 Meet Up '24 - Community & News UpdateVictoriaMetrics Q1 Meet Up '24 - Community & News Update
VictoriaMetrics Q1 Meet Up '24 - Community & News UpdateVictoriaMetrics
 
OpenChain AI Study Group - Europe and Asia Recap - 2024-04-11 - Full Recording
OpenChain AI Study Group - Europe and Asia Recap - 2024-04-11 - Full RecordingOpenChain AI Study Group - Europe and Asia Recap - 2024-04-11 - Full Recording
OpenChain AI Study Group - Europe and Asia Recap - 2024-04-11 - Full RecordingShane Coughlan
 
GraphSummit Madrid - Product Vision and Roadmap - Luis Salvador Neo4j
GraphSummit Madrid - Product Vision and Roadmap - Luis Salvador Neo4jGraphSummit Madrid - Product Vision and Roadmap - Luis Salvador Neo4j
GraphSummit Madrid - Product Vision and Roadmap - Luis Salvador Neo4jNeo4j
 
Introduction to Firebase Workshop Slides
Introduction to Firebase Workshop SlidesIntroduction to Firebase Workshop Slides
Introduction to Firebase Workshop Slidesvaideheekore1
 
Keeping your build tool updated in a multi repository world
Keeping your build tool updated in a multi repository worldKeeping your build tool updated in a multi repository world
Keeping your build tool updated in a multi repository worldRoberto Pérez Alcolea
 
Patterns for automating API delivery. API conference
Patterns for automating API delivery. API conferencePatterns for automating API delivery. API conference
Patterns for automating API delivery. API conferencessuser9e7c64
 
Strategies for using alternative queries to mitigate zero results
Strategies for using alternative queries to mitigate zero resultsStrategies for using alternative queries to mitigate zero results
Strategies for using alternative queries to mitigate zero resultsJean Silva
 
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full Recording
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full RecordingOpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full Recording
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full RecordingShane Coughlan
 
Salesforce Implementation Services PPT By ABSYZ
Salesforce Implementation Services PPT By ABSYZSalesforce Implementation Services PPT By ABSYZ
Salesforce Implementation Services PPT By ABSYZABSYZ Inc
 

Último (20)

What’s New in VictoriaMetrics: Q1 2024 Updates
What’s New in VictoriaMetrics: Q1 2024 UpdatesWhat’s New in VictoriaMetrics: Q1 2024 Updates
What’s New in VictoriaMetrics: Q1 2024 Updates
 
Powering Real-Time Decisions with Continuous Data Streams
Powering Real-Time Decisions with Continuous Data StreamsPowering Real-Time Decisions with Continuous Data Streams
Powering Real-Time Decisions with Continuous Data Streams
 
Amazon Bedrock in Action - presentation of the Bedrock's capabilities
Amazon Bedrock in Action - presentation of the Bedrock's capabilitiesAmazon Bedrock in Action - presentation of the Bedrock's capabilities
Amazon Bedrock in Action - presentation of the Bedrock's capabilities
 
JavaLand 2024 - Going serverless with Quarkus GraalVM native images and AWS L...
JavaLand 2024 - Going serverless with Quarkus GraalVM native images and AWS L...JavaLand 2024 - Going serverless with Quarkus GraalVM native images and AWS L...
JavaLand 2024 - Going serverless with Quarkus GraalVM native images and AWS L...
 
2024-04-09 - From Complexity to Clarity - AWS Summit AMS.pdf
2024-04-09 - From Complexity to Clarity - AWS Summit AMS.pdf2024-04-09 - From Complexity to Clarity - AWS Summit AMS.pdf
2024-04-09 - From Complexity to Clarity - AWS Summit AMS.pdf
 
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptx
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptxReal-time Tracking and Monitoring with Cargo Cloud Solutions.pptx
Real-time Tracking and Monitoring with Cargo Cloud Solutions.pptx
 
SAM Training Session - How to use EXCEL ?
SAM Training Session - How to use EXCEL ?SAM Training Session - How to use EXCEL ?
SAM Training Session - How to use EXCEL ?
 
eSoftTools IMAP Backup Software and migration tools
eSoftTools IMAP Backup Software and migration toolseSoftTools IMAP Backup Software and migration tools
eSoftTools IMAP Backup Software and migration tools
 
Leveraging AI for Mobile App Testing on Real Devices | Applitools + Kobiton
Leveraging AI for Mobile App Testing on Real Devices | Applitools + KobitonLeveraging AI for Mobile App Testing on Real Devices | Applitools + Kobiton
Leveraging AI for Mobile App Testing on Real Devices | Applitools + Kobiton
 
SensoDat: Simulation-based Sensor Dataset of Self-driving Cars
SensoDat: Simulation-based Sensor Dataset of Self-driving CarsSensoDat: Simulation-based Sensor Dataset of Self-driving Cars
SensoDat: Simulation-based Sensor Dataset of Self-driving Cars
 
Understanding Flamingo - DeepMind's VLM Architecture
Understanding Flamingo - DeepMind's VLM ArchitectureUnderstanding Flamingo - DeepMind's VLM Architecture
Understanding Flamingo - DeepMind's VLM Architecture
 
VictoriaMetrics Q1 Meet Up '24 - Community & News Update
VictoriaMetrics Q1 Meet Up '24 - Community & News UpdateVictoriaMetrics Q1 Meet Up '24 - Community & News Update
VictoriaMetrics Q1 Meet Up '24 - Community & News Update
 
OpenChain AI Study Group - Europe and Asia Recap - 2024-04-11 - Full Recording
OpenChain AI Study Group - Europe and Asia Recap - 2024-04-11 - Full RecordingOpenChain AI Study Group - Europe and Asia Recap - 2024-04-11 - Full Recording
OpenChain AI Study Group - Europe and Asia Recap - 2024-04-11 - Full Recording
 
GraphSummit Madrid - Product Vision and Roadmap - Luis Salvador Neo4j
GraphSummit Madrid - Product Vision and Roadmap - Luis Salvador Neo4jGraphSummit Madrid - Product Vision and Roadmap - Luis Salvador Neo4j
GraphSummit Madrid - Product Vision and Roadmap - Luis Salvador Neo4j
 
Introduction to Firebase Workshop Slides
Introduction to Firebase Workshop SlidesIntroduction to Firebase Workshop Slides
Introduction to Firebase Workshop Slides
 
Keeping your build tool updated in a multi repository world
Keeping your build tool updated in a multi repository worldKeeping your build tool updated in a multi repository world
Keeping your build tool updated in a multi repository world
 
Patterns for automating API delivery. API conference
Patterns for automating API delivery. API conferencePatterns for automating API delivery. API conference
Patterns for automating API delivery. API conference
 
Strategies for using alternative queries to mitigate zero results
Strategies for using alternative queries to mitigate zero resultsStrategies for using alternative queries to mitigate zero results
Strategies for using alternative queries to mitigate zero results
 
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full Recording
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full RecordingOpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full Recording
OpenChain Education Work Group Monthly Meeting - 2024-04-10 - Full Recording
 
Salesforce Implementation Services PPT By ABSYZ
Salesforce Implementation Services PPT By ABSYZSalesforce Implementation Services PPT By ABSYZ
Salesforce Implementation Services PPT By ABSYZ
 

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