참고자료: https://www.baeldung.com/spring-webclient-simultaneous-calls
1. 개요
우리는 보통 애플리케이션에서 HTTP 요청을 할 때 순차적으로 한다. 하지만 이 요청들을 동시에 호출하고 싶은 경우도 많을 것이다.
예를들어 여러 데이터 공급자들로부터 데이터를 가져오거나 애플리케이션의 성능을 올리고 싶을 때 동시에 호출하고 싶을 것이다.
여기서는 spring reactive WebClient 를 사용해서 서비스를 병렬로 호출하는 방법들에 대해 다룬다.
2. 리액티브 프로그래밍에 대하여 간단히 알아보기
WebClient 는 Spring 5 에서 도입되었으며 Spring Web Reactive 모듈에 포함되어 있다. WebClient 는 HTTP 요청을 리액티브, 논블록킹한 방식으로 할 수 있는 인터페이스를 제공한다.
웹플럭스를 사용하는 리액티브 프로그래밍에 대하여 깊이 알아보고자 한다면 Guide to Spring 5 WebFlux를 확인하자.
3. 간단한 Simple User Service 예제
여기서는 간단한 User API 를 사용한다. 이 API 는 파라미터로 id 를 사용해서 사용자를 조회하는 getUser 메소드 하나를 갖는다.
파라미터로 전달한 id 에 해당하는 사용자를 조회하는 요청 하나를 만드는 방법에 대해서 알아 보자.
WebClient webClient = WebClient.create("http://localhost:8080");
public Mono<User> getUser(int id) {
LOG.info(String.format("Calling getUser(%d)", id));
return webClient.get()
.uri("/user/{id}", id)
.retrieve()
.bodyToMono(User.class);
}
다음은 동시에 이 메소드를 호출하는 방법에 대하여 다룬다.
4. 동시에 WebClient 호출하기
여기거는 getUser 메소드를 동시에 호출하는 몇가지 방법에 대하여 다룬다. 또한 Flux 와 Mono 이 두개의 퍼블리셔에 대해서도 다룬다.
4.1 같은 서비스를 여러번 호출하기
5명의 사용자 정보를 동시에 호출 후, 하나의 사용자 리스트를 반환하는 예제를 생각해 보자.
public Flux<User> fetchUsers(List<Integer> userIds) {
return Flux.fromIterable(userIds)
.parallel()
.runOn(Schedulers.elastic())
.flatMap(this::getUser)
.ordered((u1, u2) -> u2.id() - u1.id());
}
위의 코드를 분석해 보면 다음과 같다.
- fromIterable 메소드를 사용해서 사용자 아이디를 전달하는 Flux 를 생성한다.
- ParallelFlux 를 생성하는 parallel 메소드를 호출한다. - parallel 메소드는 동시에 실행시키겠다는 것을 의미한다.
- 여기서는 호출하기 위해서 elastic scheduler 를 사용하기로 했으나, 다른 설정을 해도 무관하다.
- getUser 메소드를 실행시키기 위해 flatMap 을 호출하면, ParallelFlux 를 반환하게 된다.
- ParallelFlux 를 단순한 Flux 로 변환시겨야 하는데 여기서는 comparator 인터페이스를 받는 ordered 메소드를 사용한다.
연산자(operation)들이 병렬로 실행되며, 결과 순서는 알지 못한다는 것을 알아야 한다. 따라서 API 는 ordered 메소들 제공한다.
4.2. 동일한 타입을 반환하지만 서로 다른 여러 서비스들을 호출하기
동시에 여러 서비스들을 호출하는 방법에 대하여 알아본다.
이 예제에서는 같은 User 타입을 반환하지만 다른 엔드포인트를 호출하는 서비스를 생성한다.
public Mono<User> getOtherUser(int id) {
return webClient.get()
.uri("/otheruser/{id}", id)
.retrieve()
.bodyToMono(User.class);
}
다음은 2개 이상의 호출을 병렬로 수행하는 코드다.
public Flux<User> fetchUserAndOtherUser(int id) {
return Flux.merge(getUser(id), getOtherUser(id))
.parallel()
.runOn(Schedulers.elastic())
.ordered((u1, u2) -> u2.id() - u1.id());
}
여기서는 merge 메소드를 사용했는데 fromIterable 메소드와 주요 차이점은 merge 메소드는 2개 이상의 Flux 들을 하나의 결과로 결합 할 수 있다는 것이다.
4.3 서로 다른 서비스이며, 반환 타입도 각각 다른 여러 서비스들 호출하기
두개의 다른 서비스가 같은 타입을 반환할 가성은 낮은 편이다. 각 서비스가 다른 응답 타입을 반환하는 케이스가 더 일반적일 것이다. 여기서의 목표는 두개이상의 응답들을 통합하는 것이다.
Mono 클래스는 zip 메소드를 제공하는데 이 메소드는 2개 이상의 결과를 결합한다.
public Mono fetchUserAndItem(int userId, int itemId) {
Mono<User> user = getUser(userId).subscribeOn(Schedulers.elastic());
Mono<Item> item = getItem(itemId).subscribeOn(Schedulers.elastic());
return Mono.zip(user, item, UserWithItem::new);
}
여기서 중요한 부분은 zip 메소드에 결과를 전달하기 전에 subscribeOn 을 호출해야 한다는 것이다.
하지만 subscribeOn 메소드는 Mono 를 구독하지 못한다.
subscribeOn 에는 subcribe 가 호출될 때 사용할 Scheduler 가 무엇인지가 기술된다. 이 예제에서는 elastic 스케줄러를 사용하고 있는데 이 스케줄러는 구독이 실행될 때 마다 하나의 전용 스레드에서 동작되도록 보장한다.
마지막 단계는 zip 메소드를 호출하는 것인데, 이 메소드는 user 와 item Mono 클래스들을 UserWithItem 타입의 새로운 Mono 클래스로 통합한다. UserWithItem 은 User 와 item 을 담는 단순한 POJO 객체다.
5. 테스팅
여기서는 위의 코드들을 테스트할 수 있는 방법에 대해서 알아볼 것이다. 특히 서비스가 호출이 병렬고 일어나고 있는지를 검증한다.
목 서버 생성을 위해 Wiremock 을 사용할 것이며, fetchUsers 메소드를 테스트 한다.
@Test
public void givenClient_whenFetchingUsers_thenExecutionTimeIsLessThanDouble() {
int requestsNumber = 5;
int singleRequestTime = 1000;
for (int i = 1; i <= requestsNumber; i++) {
stubFor(get(urlEqualTo("/user/" + i)).willReturn(aResponse().withFixedDelay(singleRequestTime)
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(String.format("{ \"id\": %d }", i))));
}
List<Integer> userIds = IntStream.rangeClosed(1, requestsNumber)
.boxed()
.collect(Collectors.toList());
Client client = new Client("http://localhost:8089");
long start = System.currentTimeMillis();
List<User> users = client.fetchUsers(userIds);
long end = System.currentTimeMillis();
long totalExecutionTime = end - start;
assertEquals("Unexpected number of users", requestsNumber, users.size());
assertTrue("Execution time is too big", 2 * singleRequestTime > totalExecutionTime);
}
이 예제는 user 서비스를 목킹하고 일초내에 요청에 응답하도록 한다. WebClient를 사용해서 5번의 호출을 했을 때, 각 호출이 동시에 발생하여 2초이상 걸리지 않을 것을 가정하여 테스트한다.
아래 로그를 더 자세히 보면 각 요청마다 다른 스레드에서 동작하는 것을 확인할 수 있다.
[elastic-6] INFO c.b.r.webclient.simultaneous.Client - Calling getUser(5)
[elastic-3] INFO c.b.r.webclient.simultaneous.Client - Calling getUser(2)
[elastic-5] INFO c.b.r.webclient.simultaneous.Client - Calling getUser(4)
[elastic-2] INFO c.b.r.webclient.simultaneous.Client - Calling getUser(1)
[elastic-4] INFO c.b.r.webclient.simultaneous.Client - Calling getUser(3)
WebClient 테스트를 위한 기술들을 더 알아 보고 싶다면, Mocking a WebClient in Spring 가이드를 확인하자.
6. 결론
여기서는 Spring 5 Reactive WebClient 를 사용해서 HTTP 서비스 호출을 동시에 호출하는 몇가지 방법들에 대하여 다뤘다.
첫번째는 같은 서비스를 병렬로 호출하는 방법이었으며, 그 다음은 서로 다른 타입들을 반환하는 2개의 서비스들을 호출하는 예제를 보았다. 그리고 나서 목 서버를 사용해서 작성한 코드를 테스트 할 수 있는 방법에 대하여 알아보았다.
샘플 소스는 github.com/eugenp/tutorials/tree/master/spring-5-reactive-client 에서 확인할 수 있다.
'SpringFramework' 카테고리의 다른 글
스프링 웹플럭스 인증 with JWT + RBAC (0) | 2021.05.01 |
---|---|
@Bean (0) | 2011.05.31 |
@Resource, @Autowired 사용시기 (0) | 2011.05.30 |
스프링에 대한 대표적인 오해중의 하나 (0) | 2011.05.28 |
<property> 요소 사용방법 (0) | 2011.05.28 |