[Spring] 서버 간 통신하기: WebClient
본문 바로가기

Development/Spring

[Spring] 서버 간 통신하기: WebClient

 

이미지 출처: https://velog.velcdn.com/images/bsh5659/post/92345ae8-97e7-442f-ba7c-b10a594f7b70/image.png

WebClient

  • 일반적으로 실제 운영환경에 적용되는 애플리케이션은 정식 버전으로 출시된 스프링 부트의 버전보다 낮은 경우가 많기 때문에 RestTemplate을 많이 사용하고있다. 하지만 최신 버전에서는 RestTemplate이 지원 중단되어 WebClient를 사용할 것을 권고하고 있다.
  • Spring WebFlux는 HTTP 요청을 수행하는 클라이언트로 WebClient를 제공한다.
  • WebClient는 리액터(Reactor) 기반으로 동작하는 API이다. 리액터 기반이므로 스레드와 동시성 문제를 벗어나 비동기 형식으로 사용할 수 있다.

WebClient 특징

  • 논블로킹(Non-Blocking) I/O를 지원
  • 리액티브 스트림(Reactive Streams)의 백 프레셔(Back Pressure)를 지원
  • 적은 하드웨어 리소스로 동시성을 지원
  • 함수형 API를 지원
  • 동기, 비동기 상호작용을 지원
  • 스트리밍을 지원

WebClient를 사용하려면 WebFlux 모듈에 대한 의존성을 추가해야 한다.

Maven

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Gradle

dependencies {
    // ... other dependencies

    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

WebFlux는 클라이언트와 서버 간 리액티브 애플리케이션 개발을 지원하기 위해 스프링 프레임워크 5에서 새롭게 추가된 모듈이다.

최근 프로그래밍 추세에 맞춰 스프링에도 리액티브 프로그래밍(Reactive Programming) 이 도입되면서 여러 동시적 기능이 제공되고 있다.

리액티브 프로그래밍(Reactive Programming)

* Google Gemini의 답변입니다.
리액티브 프로그래밍(Reactive Programming)은 이벤트 스트림과 논블로킹 데이터 처리를 기반으로 하는 프로그래밍 패러다임입니다. 기존의 명령형 프로그래밍과는 다른 사고 방식을 요구하며, 다음과 같은 특징을 지니고 있습니다.
- 데이터 스트림(Data Streams): 데이터를 연속적인 흐름(스트림)으로 취급합니다. 이벤트가 발생할 때마다 데이터를 처리하는 방식으로, 전체 데이터를 한꺼번에 로딩하지 않고 필요한 부분만 처리할 수 있습니다.
- 논블로킹(Non-Blocking): 시스템은 사용자 입력이나 이벤트를 기다리는 동안 계속 실행됩니다. 특정 작업이 완료될 때까지 프로그램 전체가 멈추는 일이 없으며, 효율적인 리소스 활용이 가능합니다.
- 백프레셔(Backpressure): 데이터 처리 속도가 데이터 생성 속도를 따라갈 수 없는 상황 (백프레셔) 에서 시스템이 데이터 손실이나 버퍼 오버플로우를 방지하는 메커니즘을 제공합니다.

리액티브 프로그래밍의 장점
높은 응답성(Responsiveness): 사용자 입력이나 이벤트에 빠르게 반응할 수 있습니다.확장성(Scalability): 시스템 부하가 증가하더라도 효율적으로 처리할 수 있습니다.복잡성 관리(Complexity Management): 비동 비속적인 특성으로 인해 복잡한 시스템을 관리하기 용이합니다.

WebClient 사용하기

WebClient를 생성하는 방법은 다음과 같이 크게 두 가지가 있다.

  • create() 메서드를 이용한 생성
  • builder()를 이용한 생성

WebClient를 활용한 GET 요청 예제

builder()를 활용한 WebClient 생성 방법

public String getName() {
    // WebClient 객체를 생성하고 기본 URL 및 헤더를 설정
    WebClient webClient = WebClient.builder()
        .baseUrl("http://localhost:9090")  // 기본 URL 설정
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)  // 기본 헤더 설정
        .build();

    // GET 요청을 보내고, 응답을 문자열로 반환
    return webClient.get()
        .uri("/api/v1/crud-api")  // URI 확장 설정
        .retrieve()  // 요청에 대한 응답 추출
        .bodyToMono(String.class)  // 응답 본문을 문자열로 변환
        .block();  // 논블로킹을 블로킹 방식으로 전환
}

위 코드는 http://localhost:9090/api/v1/crud-api URL로 get 요청을 보내는 예제 코드이다.

WebClient 생성 및 설정

  • builder() 메서드 사용: WebClient 객체를 생성할 때 builder() 메서드를 사용하여 확장 가능한 설정을 적용할 수 있다.
    • baseUrl(): 기본으로 사용할 URL을 설정한다.
    • defaultHeader(): 기본 HTTP 헤더를 설정한다.
    • build(): 설정을 완료하고 WebClient 객체를 생성한다.

확장 가능한 WebClient 설정 메서드

  • defaultHeader(): 기본 HTTP 헤더를 설정한다.
  • defaultCookie(): 기본 쿠키를 설정한다.
  • defaultUriVariables(): 기본 URI 변수 확장값을 설정한다.
  • filter(): 요청에 대해 필터를 설정한다.

 

WebClient의 HTTP 메서드와 URI 설정

  • WebClient의 HTTP 메서드: WebClient는 HTTP 요청을 보낼 때 get(), post(), put(), delete() 등 다양한 HTTP 메서드를 지원한다.
    • 예: webClient.get(), webClient.post() 등을 통해 HTTP GET 또는 POST 요청을 보낼 수 있다.
  • URI 확장: uri() 메서드를 사용하여 기본 URL에 추가적인 경로와 파라미터를 지정할 수 있다.
    • 예: uri("/api/v1/resource")는 기본 URL에 /api/v1/resource를 추가하여 완전한 URI를 만든다.

응답 처리와 리액티브 스트림의 Mono

  • 응답 처리: retrieve() 메서드는 요청에 대한 응답을 처리하는데 사용된다. 이 메서드는 서버의 응답을 추출하고, 후속 처리를 위한 다양한 메서드를 연결할 수 있다.
    • bodyToMono() 메서드를 사용하여 응답 본문을 특정 타입으로 변환한다. 이때 변환된 데이터는 Mono로 감싸지며, Mono는 리액티브 스트림에서 단일 결과를 나타낸다.
Mono와 Flux의 개념
Mono:Mono는 리액티브 스트림에서 단일 값을 비동기적으로 제공하는 발행자(Publisher)이다. 즉, Mono는 0 또는 1개의 데이터 요소를 비동기적으로 반환한다.예를 들어, 서버에서 하나의 데이터 항목을 가져오거나, 단일 작업의 결과를 반환할 때 사용된다.
Flux:Flux는 여러 값을 비동기적으로 제공하는 발행자이다. Flux는 0부터 N개의 데이터 요소를 스트리밍할 수 있다.예를 들어, 여러 개의 데이터를 연속적으로 전송해야 하는 경우(예: 데이터 스트리밍, 다수의 결과 반환) Flux를 사용한다.

 

WebClient는 기본적으로 논블로킹(Non-Blocking)방식으로 동작하기 때문에 기존에 사용하던 코드의 구조를 블로킹 구조로 바꿔줄 필요가 있는 경우, block() 이라는 메서드를 추가해서 블로킹 형식으로 동작하게끔 설정할 수 있다.

한 번 빌드된 WebClient는 변경할 수 없으며, 다음과 같이 복사해서 사용할 수는 있다.

WebClient webClient = WebClient.create("http://localhost:9090");
WebClient clone = webClient.mutate().build();

create를 활용한 WebClient 생성 방법: PathVariable

public String getNameWithPathVariable() {
        WebClient webClient = WebClient.create("http://localhost:9090");

        ResponseEntity<String> responseEntity = webClient.get()
            .uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api/{name}")
                .build("Flature"))
            .retrieve().toEntity(String.class).block();

        ResponseEntity<String> responseEntity1 = webClient.get()
            .uri("/api/v1/crud-api/{name}", "Flature")
            .retrieve()
            .toEntity(String.class)
            .block();

        return responseEntity.getBody();
    }
  • 위 예제는 PathVariable 값을 추가해 요청을 보내는 예제이다.
  • uri() 내부에 uriBuilder를 사용해 path를 설정하고 build() 메서드에 추가할 값을 넣는 것으로 pathVariable을 추가할 수 있다.
  • 또한 bodyToMono()가 아닌 toEntity()를 사용하고 있는데, 이를 통해 ResponseEntity 타입으로 응답을 전달받을 수 있다.

create를 활용한 WebClient 생성 방법: Parameter

    public String getNameWithParameter() {
        WebClient webClient = WebClient.create("http://localhost:9090");

        return webClient.get().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")
                .queryParam("name", "Flature")
                .build())
            .exchangeToMono(clientResponse -> {
                if (clientResponse.statusCode().equals(HttpStatus.OK)) {
                    return clientResponse.bodyToMono(String.class);
                } else {
                    return clientResponse.createException().flatMap(Mono::error);
                }
            })
            .block();
    }
  • 위 코드는 GET 요청 시 쿼리 파라미터를 함께 전달하는 방법이다.
  • uriBuilder를 사용하며, queryParam() 메서드를 사용해 전달하려는 값을 설정한다.
  • 또한 exchangeToMono() 를 사용하는데 이는 exchange() 메서드가 지원 중단됐기 때문이며, exchangeToFlux()를 사용할 수도 있다.
  • 그리고 clientResponse 결괏값으로 상태값에 따라 if문 분기를 만들어 상황에 따라 결괏값을 다르게 전달할 수 있다.

WebClient를 활용한 POST 요청

파라미터와 Header를 포함한 POST 요청

 public ResponseEntity<MemberDto> postWithParamAndBody() {
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:9090")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();

        MemberDto memberDTO = new MemberDto();
        memberDTO.setName("flature!!");
        memberDTO.setEmail("flature@gmail.com");
        memberDTO.setOrganization("Around Hub Studio");

        return webClient.post().uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api")
                .queryParam("name", "Flature")
                .queryParam("email", "flature@wikibooks.co.kr")
                .queryParam("organization", "Wikibooks")
                .build())
            .bodyValue(memberDTO)
            .retrieve()
            .toEntity(MemberDto.class)
            .block();
    }
  • GET 요청 방식과 크게 다르지 않지만, HTTP Body값을 담는 방법과 커스텀 헤더를 추가하는 방법이 추가될 수 있다.
  • post() 메서드를 통해 POST 메서드 통신을 정의하고, uri()는 uriBuilder로 path와 parameter를 설정한다.
    그 후 bodyValue() 메서드를 통해 HTTP body값을 설정한다. HTTP body에는 일반적으로 데이터 객체(DTO, VO 등)를 파라미터로 전달한다.

커스텀 Header를 포함한 POST 요청

 public ResponseEntity<MemberDto> postWithHeader() {
        WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:9090")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();

        MemberDto memberDTO = new MemberDto();
        memberDTO.setName("flature!!");
        memberDTO.setEmail("flature@gmail.com");
        memberDTO.setOrganization("Around Hub Studio");

        return webClient
            .post()
            .uri(uriBuilder -> uriBuilder.path("/api/v1/crud-api/add-header")
                .build())
            .bodyValue(memberDTO)
            .header("my-header", "Wikibooks API")
            .retrieve()
            .toEntity(MemberDto.class)
            .block();

위 코드는 header() 메서드를 사용해 헤더에 값을 추가한다. 일반적으로 임의로 추가한 헤더에는 외부 API를 사용하기 위해 인증된 토큰값을 담아 전달한다.

참고