1. 개요

많은 프레임워크와 프로젝트에서  반응형 프로그래밍과 비동기식 요청 처리 를 도입하고 있습니다 . 따라서 Spring 5 는 WebFlux 프레임워크 의 일부로  반응형  WebClient 구현을 도입했습니다  .

이 사용방법(예제)에서는 WebClient 를 사용하여 REST API Endpoints을 반응적  으로 사용하는 방법을 배웁니다 .

2. REST API 엔드포인트

시작하려면 다음 GET Endpoints 을 사용하여 샘플 REST API 를 정의해 보겠습니다 .

  • /products – 모든 제품 가져오기
  • /products/{id} – ID로 제품 가져오기
  • /products/{id}/attributes/{attributeId} – ID로 제품 속성 가져오기
  • /products/?name={이름}&deliveryDate={배송일}&color={color} – 제품 찾기
  • /products/?tag[]={tag1}&tag[]={tag2} – 태그로 제품 가져오기
  • /products/?category={category1}&category={category2} – 카테고리별로 제품 가져오기

여기서 우리는 몇 가지 다른 URI를 정의했습니다. 잠시 후 WebClient 를 사용하여 각 유형의 URI를 빌드하고 전송하는 방법을 알아낼 것입니다 .

태그 및 카테고리로 제품을 가져오기 위한 URI에는 쿼리 매개변수로 배열이 포함되어 있습니다. 그러나 URI에서 배열을 표현하는 방법에 대한 엄격한 정의가 없기 때문에 구문이 다릅니다 . 이것은 주로 서버 측 구현에 따라 다릅니다. 따라서 두 가지 경우를 모두 다룰 것입니다.

3. 웹클라이언트 설정

먼저 WebClient 인스턴스를 만들어야 합니다 . 이 문서에서는 유효한 URI가 요청되었는지 확인하기 위해 모의 개체 를 사용합니다.

클라이언트 및 관련 모의 개체를 정의해 보겠습니다.

exchangeFunction = mock(ExchangeFunction.class);
ClientResponse mockResponse = mock(ClientResponse.class);
when(mockResponse.bodyToMono(String.class))
  .thenReturn(Mono.just("test"));

when(exchangeFunction.exchange(argumentCaptor.capture()))
  .thenReturn(Mono.just(mockResponse));

webClient = WebClient
  .builder()
  .baseUrl("https://example.com/api")
  .exchangeFunction(exchangeFunction)
  .build();

또한 클라이언트의 모든 요청 앞에 추가될 기본 URL을 전달합니다.

마지막으로 특정 URI가 기본 ExchangeFunction 인스턴스 에 전달되었는지 확인하기 위해 다음 도우미 메서드를 사용합니다.

private void verifyCalledUrl(String relativeUrl) {
    ClientRequest request = argumentCaptor.getValue();
    assertEquals(String.format("%s%s", BASE_URL, relativeUrl), request.url().toString());
    
    verify(this.exchangeFunction).exchange(request);
    verifyNoMoreInteractions(this.exchangeFunction);
}

WebClientBuilder 클래스에는 UriBuilder 인스턴스를 인수로 제공하는 uri ( ) 메서드 가 있습니다. 일반적으로 다음과 같은 방식으로 API를 호출합니다.

webClient.get()
  .uri(uriBuilder -> uriBuilder
    //... building a URI
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

이 사용방법(예제)에서는 URI를 구성하기 위해 UriBuilder 를 광범위하게 사용할 것 입니다. 다른 메서드를 사용하여 URI를 빌드한 다음 생성된 URI를 문자열로 전달할 수 있다는 점은 주목할 가치가 있습니다.

4. URI 경로 구성 요소

경로 구성 요소는 슬래시(/)로 구분된 일련의 경로 세그먼트로 구성됩니다 . 먼저 URI에 변수 세그먼트인 /products 가 없는 간단한 경우부터 시작하겠습니다 .

webClient.get()
  .uri("/products")
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products");

이 경우 문자열 을 인수로 전달할 수 있습니다.

다음으로 /products/{id} 엔드포인트를 가져오고 해당 URI를 빌드합니다.

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/{id}")
    .build(2))
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/2");

위의 코드에서 실제 세그먼트 값이 build() 메서드로 전달되는 것을 볼 수 있습니다.

비슷한 방식으로 /products/{id}/attributes/{attributeId} 엔드포인트 에 대한 여러 경로 세그먼트가 있는 URI를 만들 수 있습니다 .

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/{id}/attributes/{attributeId}")
    .build(2, 13))
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/2/attributes/13");

URI는 필요한 만큼 많은 경로 세그먼트를 가질 수 있지만 최종 URI 길이는 제한을 초과하지 않아야 합니다. 마지막으로 build() 메서드 에 전달된 실제 세그먼트 값의 올바른 순서를 유지해야 한다는 점을 기억해야 합니다 .

5. URI 쿼리 매개변수

일반적으로 쿼리 매개변수는 title=Baeldung 과 같은 간단한 키-값 쌍 입니다. 이러한 URI를 빌드하는 방법을 살펴보겠습니다.

5.1. 단일 값 매개변수

단일 값 매개변수로 시작하여 /products/?name={name}&deliveryDate={deliveryDate}&color={color} 엔드포인트를 사용합니다. 쿼리 매개변수를 설정하기 위해 UriBuilder 인터페이스 의 queryParam() 메서드를 호출합니다.

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "AndroidPhone")
    .queryParam("color", "black")
    .queryParam("deliveryDate", "13/04/2019")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");

여기에서 세 개의 쿼리 매개변수를 추가하고 실제 값을 즉시 할당했습니다. 반대로 정확한 값 대신 자리 표시자를 남길 수도 있습니다.

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "{title}")
    .queryParam("color", "{authorId}")
    .queryParam("deliveryDate", "{date}")
    .build("AndroidPhone", "black", "13/04/2019"))
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13%2F04%2F2019");

이는 빌더 객체를 체인에서 추가로 전달할 때 특히 유용할 수 있습니다.

위의 두 코드 스니펫 사이에는 한 가지 중요한 차이점이 있습니다 . 예상 URI에 주의를 기울이면 URI가 다르게 인코딩 된다는 것을 알 수 있습니다 . 특히 마지막 예제에서 슬래시 문자 ( / ) 가 이스케이프되었습니다.

일반적으로 RFC3986 은 쿼리에서 슬래시 인코딩을 요구하지 않습니다. 그러나 일부 서버 측 애플리케이션에는 이러한 변환이 필요할 수 있습니다. 따라서 이 사용방법(예제)의 뒷부분에서 이 동작을 변경하는 방법을 살펴보겠습니다.

5.2. 배열 매개변수

값의 배열을 전달해야 할 수도 있으며 쿼리 문자열에서 배열을 전달하는 데 엄격한 규칙이 없습니다. 따라서 쿼리 문자열의 배열 표현은 프로젝트마다 다르며 일반적으로 기본 프레임워크에 따라 다릅니다 . 이 기사에서는 가장 널리 사용되는 형식을 다룰 것입니다.

/products/?tag[]={tag1}&tag[]={tag2} 엔드포인트 부터 시작하겠습니다 .

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("tag[]", "Snapdragon", "NFC")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?tag%5B%5D=Snapdragon&tag%5B%5D=NFC");

보시다시피 최종 URI에는 여러 태그 매개변수와 인코딩된 대괄호가 포함되어 있습니다. queryParam () 메서드는 변수 인수를 값으로 허용하므로 메서드를 여러 번 호출할 필요가 없습니다.

또는 대괄호를 생략하고 키 는 같지만 값이 다른 여러 쿼리 매개변수를 전달할 수 있습니다. /products/?category={category1}&category={category2} :

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("category", "Phones", "Tablets")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?category=Phones&category=Tablets");

마지막으로 배열을 인코딩하는 데 광범위하게 사용되는 방법이 하나 더 있는데, 쉼표로 구분된 값을 전달하는 것입니다. 이전 예제를 쉼표로 구분된 값으로 변환해 보겠습니다.

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("category", String.join(",", "Phones", "Tablets"))
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?category=Phones,Tablets");

쉼표로 구분된 문자열을 만들기 위해 String 클래스 의 join() 메서드 를 사용 하고 있습니다. 응용 프로그램에서 예상하는 다른 구분 기호를 사용할 수도 있습니다.

6. 인코딩 모드

이전에 URL 인코딩을 어떻게 언급했는지 기억하십니까?

기본 동작이 요구 사항에 맞지 않으면 변경할 수 있습니다. WebClient 인스턴스 를 빌드하는 동안 UriBuilderFactory 구현 을 제공해야 합니다. 이 경우 DefaultUriBuilderFactory 클래스를 사용합니다. 인코딩을 설정하기 위해 setEncodingMode() 메서드를 호출합니다. 다음 모드를 사용할 수 있습니다.

  • TEMPLATE_AND_VALUES : URI 템플릿을 미리 인코딩하고 확장 시 URI 변수를 엄격하게 인코딩합니다.
  • VALUES_ONLY : URI 템플릿을 인코딩하지 않고 URI 변수를 템플릿으로 확장한 후 엄격하게 인코딩
  • URI_COMPONENTS : URI 변수를 확장한 후 URI 구성 요소 값을 인코딩합니다.
  • NONE : 인코딩이 적용되지 않습니다.

기본값은 TEMPLATE_AND_VALUES 입니다. 모드를 URI_COMPONENTS 로 설정해 보겠습니다 .

DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT);
webClient = WebClient
  .builder()
  .uriBuilderFactory(factory)
  .baseUrl(BASE_URL)
  .exchangeFunction(exchangeFunction)
  .build();

결과적으로 다음 어설션이 성공합니다.

webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "AndroidPhone")
    .queryParam("color", "black")
    .queryParam("deliveryDate", "13/04/2019")
    .build())
  .retrieve()
  .bodyToMono(String.class)
  .block();

verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");

물론 URI 생성을 수동으로 처리하기 위해 완전한 사용자 정의 UriBuilderFactory 구현을 제공할 수 있습니다.

7. 결론

이 문서에서는 WebClientDefaultUriBuilder 를 사용하여 다양한 유형의 URI를 빌드하는 방법을 배웠습니다 .

그 과정에서 쿼리 매개변수의 다양한 유형과 형식을 다루었습니다. 마지막으로 URL 빌더의 기본 인코딩 모드를 변경하여 마무리했습니다.

항상 그렇듯이 기사의 모든 코드 스니펫은 GitHub 리포지토리에서 사용할 수 있습니다 .

REST footer banner