WebClient 간단 정리 - Kim-Taesu/study GitHub Wiki

WebClient

  • Reactor 기반 API를 사용하여 추가적인 쓰레드나 동시성 설정 필요 없이 비동기 로직을 선언적으로 구성할 수 있다.
  • fully non-bloking을 지원한다.

Configuration

  • Webclient를 생성하는 가장 간단한 방법은 static factory 메서드를 사용하는 방법이다.

    WebClient.create();
    WebClient.create(String baseUrl);
  • WebClient.builder() 메서드로도 생성할 수 있다.

    • uriBuildeFactory : 기본 URL로 사용할 커스텀 UriBuilderFactory를 설정할 수 있다.

    • defaultUriVariables : URI template를 확장할 때 사용되는 기본 값

    • defaultHeader : 모든 요청에 대한 헤더

    • defaultCookie : 모든 요청에 대한 쿠키

    • defaultRequest : 모든 요청을 커스터마이징할 consumer

    • filter : 모든 요청에 대한 필터

    • exchangeStrategies : HTTP 메시지 읽기/쓰기 커스터마이징할 때 사용

    • clientConnector : HTTP client 라이브러리 설정

      WebClient client = WebClient.builder()
              .codecs(configurer -> ... )
              .build();
  • WebClient는 immutable 하다. 하지만 clone해서 수정할 수 있다.

    WebClient client1 = WebClient.builder()
            .filter(filterA).filter(filterB).build();
    
    WebClient client2 = client1.mutate()
            .filter(filterC).filter(filterD).build();
    
    // client1 has filterA, filterB
    
    // client2 has filterA, filterB, filterC, filterD

MaxInMemorySize

  • 코덱은 메모리 문제를 피하기 위해 메모리에서 데이터를 버퍼링하는데 제한이 있다.
    • 기본 256kb로 설전된다.
  • 아래 설정으로 바꿀 수 있다.
WebClient webClient = WebClient.builder()
        .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
        .build();

retrieve()

  • response를 추출하는 방법을 선언할 수 있다.
WebClient client = WebClient.create("https://example.org");

Mono<ResponseEntity<Person>> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .toEntity(Person.class);
  • body만 가져올 수도 있다.
WebClient client = WebClient.create("https://example.org");

Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono(Person.class);
  • 디코딩된 stream 형태로도 가져올 수 있다.
Flux<Quote> result = client.get()
        .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
        .retrieve()
        .bodyToFlux(Quote.class);
  • 기본적으로 4xx, 5xx 응답은 WebClientResponseException이 발생한다.
    • onStatus로 해당 응답을 핸들링 할 수 있다.
Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError, response -> ...)
        .onStatus(HttpStatus::is5xxServerError, response -> ...)
        .bodyToMono(Person.class);

Exchange

  • 응답에 대해 더 많은 제어가 필요한 경우 사용된다.
Mono<Object> entityMono = client.get()
        .uri("/persons/1")
        .accept(MediaType.APPLICATION_JSON)
        .exchangeToMono(response -> {
            if (response.statusCode().equals(HttpStatus.OK)) {
                return response.bodyToMono(Person.class);
            }
            else if (response.statusCode().is4xxClientError()) {
                // Suppress error status code
                return response.bodyToMono(ErrorContainer.class);
            }
            else {
                // Turn to error
                return response.createException().flatMap(Mono::error);
            }
        });

RequestBody

  • request body는 Mono, Flux 형태의 비동기 타입으로 인코딩될 수 있다.
Mono<Person> personMono = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body(personMono, Person.class)
        .retrieve()
        .bodyToMono(Void.class);
  • 인코딩된 객체 스트림 예제
Flux<Person> personFlux = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_STREAM_JSON)
        .body(personFlux, Person.class)
        .retrieve()
        .bodyToMono(Void.class);
  • request body에 저장될 실제 객체가 있다면 bodyValue 메서드로 쉽게 변환할 수 있다.
Person person = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(person)
        .retrieve()
        .bodyToMono(Void.class);

Form Data

  • MultiValueMap<String, String> 타입의 값을 body에 저장할 수 있다.
  • content 타입은 자동으로 FormHttpMessageWriter에 의해 application/x-www-form-urlencoded로 설정된다.
MultiValueMap<String, String> formData = ... ;

Mono<Void> result = client.post()
        .uri("/path", id)
        .bodyValue(formData)
        .retrieve()
        .bodyToMono(Void.class);
  • BodyInserters 사용 예제
import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(fromFormData("k1", "v1").with("k2", "v2"))
        .retrieve()
        .bodyToMono(Void.class);

Multipart Data

  • multipart data를 전송하기 위해 MultiValueMap<String, ?> 타입의 값이 필요하다.
  • MultipartBodyBuilder는 multi part 요청을 준비할 수 있는 편리한 API를 제공.
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request

MultiValueMap<String, HttpEntity<?>> parts = builder.build();
  • 각 part 데이터마다 content-type을 설정할 필요가 없다.

    • 직렬화하기 위해 사용되는 HttpMessageWriter에 따라 결정된다.
    • Resource일 경우 파일 확장자에 따라 자동으로 결정된다.
    • 필요한 경우 각 part 마다 MediaType을 명시적으로 설정할 수 있다.
  • MultipartBodyBuilder를 Body에 저장후 요청하면 된다.

MultipartBodyBuilder builder = ...;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(builder.build())
        .retrieve()
        .bodyToMono(Void.class);
  • MultiValueMap에 non-string 값(form data, application/x-www-form-urlencoded)이 있다면 Content-Type을 꼭 multipart/form-data로 설정하지 않아도 된다.

  • BodyInserters 사용 예제

import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
        .retrieve()
        .bodyToMono(Void.class);

Synchronous Use

  • 동기 방식으로 사용하려면 각 result에 bloking을 걸면 된다.
Person person = client.get().uri("/person/{id}", i).retrieve()
    .bodyToMono(Person.class)
    .block();

List<Person> persons = client.get().uri("/persons").retrieve()
    .bodyToFlux(Person.class)
    .collectList()
    .block();
  • 여러 요청에 대한 결과를 동기적으로 확인해야 하는 경우, 각 응답을 bloking하지 않고 전체 응답이 완료될 때 까지 bloking 하는 방법이 좋다.
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
        .retrieve().bodyToMono(Person.class);

Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
        .retrieve().bodyToFlux(Hobby.class).collectList();

Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
            Map<String, String> map = new LinkedHashMap<>();
            map.put("person", person);
            map.put("hobbies", hobbies);
            return map;
        })
        .block();
⚠️ **GitHub.com Fallback** ⚠️