본문 바로가기

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 webClient = WebClient.builder()
            .baseUrl("http://localhost:9090")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();

        return webClient.get()
            .uri("/api/v1/crud-api")
            .retrieve()
            .bodyToMono(String.class)
            .block();
    }

위 코드는 http://localhost:9090/api/v1/crud-api URL로 get 요청을 보내는 예제 코드이다.
builder()를 통해 baseUrl() 메서드에서 기본 URL을 설정하고, defaultHeader() 메서드로 헤더의 값을 설정한다.

일반적으로 WebClient 객체를 이용할 때는 이처럼 WebClient 객체를 생성한 후 재사용하는 방식으로 구현한다.


builder() 를 사용할 경우 확장할 수 있는 메서드

  • defaultHeader(): WebClient의 기본 헤더 설정
  • defaultCookie(): WebClient의 기본 쿠키 설정
  • defaultUriVariable(): WebClient의 기본 URI 확장값 설정
  • filter(): WebClient에서 발생하는 요청에 대한 필터 설정

WebClient는 get(), post(), put(), delete() 등의 HTTP 메서드로 설정할 수 있다.

그리고 URI를 확장하는 방법으로 uri() 메서드를 사용할 수 있습니다.

retrieve() 메서드는 요청에 대한 응답을 받았을 때 그 값을 추출한다.
이 메서드는 bodyToMono() 메서드를 통해 리턴 타입을 설정해서 문자열 객체를 받아오게 돼 있다.


참고로 Mono는 리액티브 스트림에 대한 선행 학습이 필요한 개념이며, Flux와 비교되는 개념이다.
Flux와 Mono는 리액티브 스트림에서 데이터를 제공하는 발행자 역할을 수행하는 Publisher의 구현체이다.

 

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를 사용하기 위해 인증된 토큰값을 담아 전달한다.

참고