SlideShare ist ein Scribd-Unternehmen logo
1 von 128
Downloaden Sie, um offline zu lesen
호주에 거주하고 있지만 여러 나라를 다니며 일하기 좋아하는 개발자. 오래전 스
프링 프레임워크의 매력에 빠진 뒤로 스프링의 변화에 맞춰 기술을 공부하고 이
를 이용해 일을 하는 것을 좋아한다. 최신 스프링에 추가된 리액티브 함수형 프로
그래밍 기술을 블록체인 코어 개발을 비롯한 여러 분야에 적용해보고 있다. 토비
의 스프링이라는 책을 썼고, 유튜브에서 토비의 봄 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 웹플럭스와 테스트 전략

Weitere ähnliche Inhalte

Was ist angesagt?

Spring Data JPA from 0-100 in 60 minutes
Spring Data JPA from 0-100 in 60 minutesSpring Data JPA from 0-100 in 60 minutes
Spring Data JPA from 0-100 in 60 minutesVMware Tanzu
 
CDC Stream Processing with Apache Flink
CDC Stream Processing with Apache FlinkCDC Stream Processing with Apache Flink
CDC Stream Processing with Apache FlinkTimo Walther
 
From Java 11 to 17 and beyond.pdf
From Java 11 to 17 and beyond.pdfFrom Java 11 to 17 and beyond.pdf
From Java 11 to 17 and beyond.pdfJosé Paumard
 
An introduction to SQLAlchemy
An introduction to SQLAlchemyAn introduction to SQLAlchemy
An introduction to SQLAlchemymengukagan
 
Spring Boot and REST API
Spring Boot and REST APISpring Boot and REST API
Spring Boot and REST API07.pallav
 
Callbacks, Promises, and Coroutines (oh my!): Asynchronous Programming Patter...
Callbacks, Promises, and Coroutines (oh my!): Asynchronous Programming Patter...Callbacks, Promises, and Coroutines (oh my!): Asynchronous Programming Patter...
Callbacks, Promises, and Coroutines (oh my!): Asynchronous Programming Patter...Domenic Denicola
 
TypeScript for Java Developers
TypeScript for Java DevelopersTypeScript for Java Developers
TypeScript for Java DevelopersYakov Fain
 
JavaScript: Variables and Functions
JavaScript: Variables and FunctionsJavaScript: Variables and Functions
JavaScript: Variables and FunctionsJussi Pohjolainen
 
Spring boot introduction
Spring boot introductionSpring boot introduction
Spring boot introductionRasheed Waraich
 
Jpa 잘 (하는 척) 하기
Jpa 잘 (하는 척) 하기Jpa 잘 (하는 척) 하기
Jpa 잘 (하는 척) 하기경원 이
 
What is Ajax technology?
What is Ajax technology?What is Ajax technology?
What is Ajax technology?JavaTpoint.Com
 
Coroutines in Kotlin. In-depth review
Coroutines in Kotlin. In-depth reviewCoroutines in Kotlin. In-depth review
Coroutines in Kotlin. In-depth reviewDmytro Zaitsev
 

Was ist angesagt? (20)

Spring Data JPA from 0-100 in 60 minutes
Spring Data JPA from 0-100 in 60 minutesSpring Data JPA from 0-100 in 60 minutes
Spring Data JPA from 0-100 in 60 minutes
 
CDC Stream Processing with Apache Flink
CDC Stream Processing with Apache FlinkCDC Stream Processing with Apache Flink
CDC Stream Processing with Apache Flink
 
From Java 11 to 17 and beyond.pdf
From Java 11 to 17 and beyond.pdfFrom Java 11 to 17 and beyond.pdf
From Java 11 to 17 and beyond.pdf
 
Google Cloud Dataflow
Google Cloud DataflowGoogle Cloud Dataflow
Google Cloud Dataflow
 
Php and MySQL
Php and MySQLPhp and MySQL
Php and MySQL
 
An introduction to SQLAlchemy
An introduction to SQLAlchemyAn introduction to SQLAlchemy
An introduction to SQLAlchemy
 
Spring Boot and REST API
Spring Boot and REST APISpring Boot and REST API
Spring Boot and REST API
 
Spring data jpa
Spring data jpaSpring data jpa
Spring data jpa
 
Callbacks, Promises, and Coroutines (oh my!): Asynchronous Programming Patter...
Callbacks, Promises, and Coroutines (oh my!): Asynchronous Programming Patter...Callbacks, Promises, and Coroutines (oh my!): Asynchronous Programming Patter...
Callbacks, Promises, and Coroutines (oh my!): Asynchronous Programming Patter...
 
TypeScript for Java Developers
TypeScript for Java DevelopersTypeScript for Java Developers
TypeScript for Java Developers
 
Java script cookies
Java script   cookiesJava script   cookies
Java script cookies
 
Introduction to Spring Boot
Introduction to Spring BootIntroduction to Spring Boot
Introduction to Spring Boot
 
JavaScript: Variables and Functions
JavaScript: Variables and FunctionsJavaScript: Variables and Functions
JavaScript: Variables and Functions
 
Sequelize
SequelizeSequelize
Sequelize
 
JavaScript Promises
JavaScript PromisesJavaScript Promises
JavaScript Promises
 
Spring boot introduction
Spring boot introductionSpring boot introduction
Spring boot introduction
 
Jpa 잘 (하는 척) 하기
Jpa 잘 (하는 척) 하기Jpa 잘 (하는 척) 하기
Jpa 잘 (하는 척) 하기
 
What is Ajax technology?
What is Ajax technology?What is Ajax technology?
What is Ajax technology?
 
Spring Core
Spring CoreSpring Core
Spring Core
 
Coroutines in Kotlin. In-depth review
Coroutines in Kotlin. In-depth reviewCoroutines in Kotlin. In-depth review
Coroutines in Kotlin. In-depth review
 

Ähnlich wie 스프링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
 

Ähnlich wie 스프링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
 

Mehr von 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
 

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

Kürzlich hochgeladen

Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdf
Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdfPayment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdf
Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdfkalichargn70th171
 
Define the academic and professional writing..pdf
Define the academic and professional writing..pdfDefine the academic and professional writing..pdf
Define the academic and professional writing..pdfPearlKirahMaeRagusta1
 
Right Money Management App For Your Financial Goals
Right Money Management App For Your Financial GoalsRight Money Management App For Your Financial Goals
Right Money Management App For Your Financial GoalsJhone kinadey
 
WSO2Con2024 - Enabling Transactional System's Exponential Growth With Simplicity
WSO2Con2024 - Enabling Transactional System's Exponential Growth With SimplicityWSO2Con2024 - Enabling Transactional System's Exponential Growth With Simplicity
WSO2Con2024 - Enabling Transactional System's Exponential Growth With SimplicityWSO2
 
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...SelfMade bd
 
AI & Machine Learning Presentation Template
AI & Machine Learning Presentation TemplateAI & Machine Learning Presentation Template
AI & Machine Learning Presentation TemplatePresentation.STUDIO
 
Architecture decision records - How not to get lost in the past
Architecture decision records - How not to get lost in the pastArchitecture decision records - How not to get lost in the past
Architecture decision records - How not to get lost in the pastPapp Krisztián
 
%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfontein
%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfontein%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfontein
%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfonteinmasabamasaba
 
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...masabamasaba
 
OpenChain - The Ramifications of ISO/IEC 5230 and ISO/IEC 18974 for Legal Pro...
OpenChain - The Ramifications of ISO/IEC 5230 and ISO/IEC 18974 for Legal Pro...OpenChain - The Ramifications of ISO/IEC 5230 and ISO/IEC 18974 for Legal Pro...
OpenChain - The Ramifications of ISO/IEC 5230 and ISO/IEC 18974 for Legal Pro...Shane Coughlan
 
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...masabamasaba
 
VTU technical seminar 8Th Sem on Scikit-learn
VTU technical seminar 8Th Sem on Scikit-learnVTU technical seminar 8Th Sem on Scikit-learn
VTU technical seminar 8Th Sem on Scikit-learnAmarnathKambale
 
Software Quality Assurance Interview Questions
Software Quality Assurance Interview QuestionsSoftware Quality Assurance Interview Questions
Software Quality Assurance Interview QuestionsArshad QA
 
%in Midrand+277-882-255-28 abortion pills for sale in midrand
%in Midrand+277-882-255-28 abortion pills for sale in midrand%in Midrand+277-882-255-28 abortion pills for sale in midrand
%in Midrand+277-882-255-28 abortion pills for sale in midrandmasabamasaba
 
%+27788225528 love spells in Knoxville Psychic Readings, Attraction spells,Br...
%+27788225528 love spells in Knoxville Psychic Readings, Attraction spells,Br...%+27788225528 love spells in Knoxville Psychic Readings, Attraction spells,Br...
%+27788225528 love spells in Knoxville Psychic Readings, Attraction spells,Br...masabamasaba
 
WSO2CON 2024 - Cloud Native Middleware: Domain-Driven Design, Cell-Based Arch...
WSO2CON 2024 - Cloud Native Middleware: Domain-Driven Design, Cell-Based Arch...WSO2CON 2024 - Cloud Native Middleware: Domain-Driven Design, Cell-Based Arch...
WSO2CON 2024 - Cloud Native Middleware: Domain-Driven Design, Cell-Based Arch...WSO2
 
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfonteinmasabamasaba
 
%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisa%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisamasabamasaba
 

Kürzlich hochgeladen (20)

Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdf
Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdfPayment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdf
Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdf
 
Define the academic and professional writing..pdf
Define the academic and professional writing..pdfDefine the academic and professional writing..pdf
Define the academic and professional writing..pdf
 
Microsoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdfMicrosoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdf
 
Right Money Management App For Your Financial Goals
Right Money Management App For Your Financial GoalsRight Money Management App For Your Financial Goals
Right Money Management App For Your Financial Goals
 
WSO2Con2024 - Enabling Transactional System's Exponential Growth With Simplicity
WSO2Con2024 - Enabling Transactional System's Exponential Growth With SimplicityWSO2Con2024 - Enabling Transactional System's Exponential Growth With Simplicity
WSO2Con2024 - Enabling Transactional System's Exponential Growth With Simplicity
 
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
Crypto Cloud Review - How To Earn Up To $500 Per DAY Of Bitcoin 100% On AutoP...
 
AI & Machine Learning Presentation Template
AI & Machine Learning Presentation TemplateAI & Machine Learning Presentation Template
AI & Machine Learning Presentation Template
 
Architecture decision records - How not to get lost in the past
Architecture decision records - How not to get lost in the pastArchitecture decision records - How not to get lost in the past
Architecture decision records - How not to get lost in the past
 
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
Abortion Pill Prices Tembisa [(+27832195400*)] 🏥 Women's Abortion Clinic in T...
 
%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfontein
%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfontein%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfontein
%in Stilfontein+277-882-255-28 abortion pills for sale in Stilfontein
 
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
%+27788225528 love spells in Huntington Beach Psychic Readings, Attraction sp...
 
OpenChain - The Ramifications of ISO/IEC 5230 and ISO/IEC 18974 for Legal Pro...
OpenChain - The Ramifications of ISO/IEC 5230 and ISO/IEC 18974 for Legal Pro...OpenChain - The Ramifications of ISO/IEC 5230 and ISO/IEC 18974 for Legal Pro...
OpenChain - The Ramifications of ISO/IEC 5230 and ISO/IEC 18974 for Legal Pro...
 
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
%+27788225528 love spells in Atlanta Psychic Readings, Attraction spells,Brin...
 
VTU technical seminar 8Th Sem on Scikit-learn
VTU technical seminar 8Th Sem on Scikit-learnVTU technical seminar 8Th Sem on Scikit-learn
VTU technical seminar 8Th Sem on Scikit-learn
 
Software Quality Assurance Interview Questions
Software Quality Assurance Interview QuestionsSoftware Quality Assurance Interview Questions
Software Quality Assurance Interview Questions
 
%in Midrand+277-882-255-28 abortion pills for sale in midrand
%in Midrand+277-882-255-28 abortion pills for sale in midrand%in Midrand+277-882-255-28 abortion pills for sale in midrand
%in Midrand+277-882-255-28 abortion pills for sale in midrand
 
%+27788225528 love spells in Knoxville Psychic Readings, Attraction spells,Br...
%+27788225528 love spells in Knoxville Psychic Readings, Attraction spells,Br...%+27788225528 love spells in Knoxville Psychic Readings, Attraction spells,Br...
%+27788225528 love spells in Knoxville Psychic Readings, Attraction spells,Br...
 
WSO2CON 2024 - Cloud Native Middleware: Domain-Driven Design, Cell-Based Arch...
WSO2CON 2024 - Cloud Native Middleware: Domain-Driven Design, Cell-Based Arch...WSO2CON 2024 - Cloud Native Middleware: Domain-Driven Design, Cell-Based Arch...
WSO2CON 2024 - Cloud Native Middleware: Domain-Driven Design, Cell-Based Arch...
 
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
 
%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisa%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisa
 

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