본문 바로가기
Back-end/Spring Boot

[SpringBoot] WebClient 이해하기(feat. block/non-block과 동기/비동기)

by whatamigonnabe 2022. 8. 1.

WebClient란?

WebClient는 간단히 말하면 http Request를 보내고 Response를 받아오는 일련의 과정을 편리하게 하는 패키지의 인터페이스이다. 

원래 같은 역할을 하는 가장 대표적인 기능이 RestTemplate이지만, Spring 5.0부터 maintenance 모드가 되었고, WebClient를 사용하라고 권고하고 있다.

RestTemplate 클래스의 Note

그렇다면 WebClient와 RestTemplate의 공통점과 차이점은 무엇일까? 공통점은 위에서 말한 것처럼, Http Request를 보내고 Response를 받아오는 역할을 하는 것이고, 차이점은 RestTemplate는 block방식이고, WebClient는 non-block방식입니다.

그리고 RestTemplate은 동기 방식을 사용하고, WebClient는 동기와 비동기 방식을 모두 지원합니다.

 

Block? Non-Block?

쉽게 말해 블락은 요청을 보내놓고 응답이 올 때까지 아무일도 못하는 것이고, non-block은 요청을 보낸 후 응답이 오는지에 상관 없이 다른 일을 진행하다가 요청을 받으면 그때 처리하는 것입니다. non-block 방식을 사용하게 되면, 동시 접속자 수가 1000명 이상으로 많아졌을 때 block에 비해 속도가 월등히 높아집니다.

 

동기? 비동기?

우선 영어로는 synchronous operations, asynchronous operations입니다. 동기는 어떤 요청을 한 후, 요청을 한 쪽에서 응답을 계속 궁금해하는 것이고, 비동기는 요청을 한 후 궁금해하지 않는 것입니다. 조금 더 자세히 설명을 하자면, 동기는 요청을 한 후 응답의 리턴을 기다리거나, 바로 처리를 못했다는 return을 받더라도, 계속해서 처리의 완료여부를 확인하는 것입니다. 반면에 비동기는 작업의 완료 여부를 신경쓰지 않고, 응답을 하는 측에 callback을 전달해서 작업 완료후 callback을 호출하게 하는 것입니다. 이것에 대해 그림으로 직관적 잘표현한 그림이 있어서 가져왔습니다. 이렇게 비동기 방식을 사용하게 되면, 큰 전체 서비스 중 일부가 실패해도, 전체에 미치는 영향을 줄이고, 비교적 쉽게 대처할 수 있습니다.

출처: https://velog.io/@wonhee010/%EB%8F%99%EA%B8%B0vs%EB%B9%84%EB%8F%99%EA%B8%B0-feat.-blocking-vs-non-blocking

WebClient 사용방법

Dependencies 추가

maven -> pom.xml

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

gradle -> build.gradle

dependencies { compile 'org.springframework.boot:spring-boot-starter-webflux' }

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

참고로 'compile' 명령어는 depreciated되어서, implementationdmf 

Instance 생성

인스턴스를 생성하는 방법은 세 가지가 있습니다. 

WebClient webClient = WebClient.create();
WebClient client = WebClient.create("http://localhost:8080"); //base Url을 할당하여 생성
//모든 속성을 직접 설정하여 생성
WebClient client = WebClient.builder()
  .baseUrl("http://localhost:8080")
  .defaultCookie("cookieKey", "cookieValue")
  .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 
  .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
  .build();

HTTP method 설정

두 가지 방법이 있습니다.

UriSpec<RequestBodySpec> uriSpec = webClient.method(HttpMethod.POST);
UriSpec<ReqeustBodySpec> uriSpec = webClient.post();

URI 설정

//1번째 방법
ReqeustBodySpec bodySpec = uriSpec.uri("/resource");

//2번째 방법(UriBuilder 활용하기)
ReqeustBodySpec bodySpec = uriSpec.uri(uriBuilder -> uriBuilder.pathSegment("/resource").build());

//3번째 방법(java.net.URL 클래스 활용하기)
RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));

Body 설정

//1번째 방법(가장 대표적인 방법)
RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("바디 내용");

//2번째 방법(Pubisher 활용하기)
RequestHeadersSpec<?> headersSpec = bodySpec.body(Mono.just(new 인스턴스 생성), 클래스.class);

//3번째 방법(BodyInserters활용하기)
RequsestHeadersSpec<?> headersSpec = bodySpec.body(BodyInserters.fromValue("바디 내용"));
	//또는
RequsestHeadersSpec<?> headersSpec = bodySpec.body(BodyInserters.fromPublisher(Mono.just("바디 내용")), String.class);

//4번째 방법(MultiValueMap 활용하기)
LinkedMultiValueMap map = new LinkedMultiValueMap();
map.add("key1", "value1");
map.add("key2", "value2");
RequsetHeadersSpec<?> headersSpec = bodySpec.body(BodyInserters.fromMultipartData(map));

 

Header 설정

ResponseSpec responseSpec = headersSpec.header(
    HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
  .accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
  .acceptCharset(StandardCharsets.UTF_8)
  .ifNoneMatch("*")
  .ifModifiedSince(ZonedDateTime.now())
  .retrieve();

 

Response 받기

이제 지금까지 준비한 request를 보내고 reponse를 받는 단계입니다.

이 단계에서는 세 가지 메서드를 이용할 수 있습니다.

exchangeToMono / exchangeToFlux / retrieve

앞의 두 exchange는 모든 http Reponse를 가져와서 핸들링하고 싶을 때 사용하고, 

retreive는 간단히 body만 가져와서 디코딩할 때 사용합니다.

그리고 exchangeToMono는 반환 값이 1개 이하일 때, exchangeToFlux는 반환 값이 N개 일 때 사용합니다. 

Mono<String> response = headersSpec.exchangeToMono(response -> {
  if (response.statusCode().equals(HttpStatus.OK)) {
      return response.bodyToMono(String.class);
  } else if (response.statusCode().is4xxClientError()) {
      return Mono.just("Error response");
  } else {
      return response.createException()
        .flatMap(Mono::error);
  }
});
Mono<String> response = headersSpec.retrieve()
  .bodyToMono(String.class);

 

Response를  스트림으로 변경하기

Flux 또는 Mono는 Spring WebFlux에서 사용하는 클래스이기 때문에, Spring MVC를 사용하고 있다면 이 리턴 값을 객체나 스트림으로 변환할 필요가 있습니다.

이를 위해서는 아래와 같이 사용할 수 있습니다.

//Flux인 경우
List<SomeData> results =
    webClient.mutate()
             .baseUrl("https://some.com/api")
             .build()
             .get()
             .uri("/resource")
             .accept(MediaType.APPLICATION_JSON)
             .retrieve()
             .bodyToFlux(SomeData.class)
             .toStream()
             .collect(Collectors.toList());
//Mono인 경우             
SomeData data = 
    webClient.mutate()
             .baseUrl("https://some.com/api")
             .build()
             .get()
             .uri("/resource/{ID}", id)
             .accept(MediaType.APPLICATION_JSON)
             .retrieve()
             .bodyToMono(SomeData.class)
             .flux()
             .toStream()
             .findFirst()
             .orElse(defaultValue);

 

참고

https://medium.com/@odysseymoon/spring-webclient-%EC%82%AC%EC%9A%A9%EB%B2%95-5f92d295edc0

https://velog.io/@wonhee010/%EB%8F%99%EA%B8%B0vs%EB%B9%84%EB%8F%99%EA%B8%B0-feat.-blocking-vs-non-blocking

https://musma.github.io/2019/04/17/blocking-and-synchronous.html

https://www.baeldung.com/spring-5-webclient