본문 바로가기

SpringFramework

스프링 WebClient 동시에 호출하기

참고자료: 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