Spring/Spring Boot

[Spring] WebClient 사용방법 가이드

민돌v 2022. 11. 23. 09:45
728x90

아래의 글 순서대로 읽으시면 해당 글을 이해하시는데 족흠 도움이 됩니다 🙏

⚙️ Block, Non-Block, sync(동기), Async(비동기) 의 간단한 개념 
[Spring] Webclient 란❓ (RestTemplate vs WebClient) 
[Spring] WebClient 사용방법 가이드 (now)
[spring] WebFlux란 + Reactor 객체란 (Mono<> 와 Flux<>)

 

 

저번 포스팅에 이어서 WebClient 사용방법에 대해 공부해보겠습니다.

 


📌 Spring WebClient 사용하기

공식문서에 나온내용을 토대로 해석해놓은 블로그글이 굉장히 많기 때문에
공식문서를 하나씩 보기보다는, 간단한 사용방법과 제가 어려웠던 부분을 토대로 정리를 해보겠습니다.

 

1) WebClient 종속성 추가하기

<!-- https://mvnrepository.com/artifact/org.springframework/spring-webflux -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webflux</artifactId>
    <version>5.3.23</version>
</dependency>

 

 

2) WebClient 객체 생성하기

WebClient에 구현된 정적 팩토리 메소드를 이용해, uri 를 생성시 설정해주거나, 설정없이 생성할 수 있습니다.

  • WebClient.create()
  • WebClient.create(String baseUrl)
  • WebCleint.build() - create() 가 default 설정이라면, build() 를 이용해 커스텀하게 설정을 변경할 수 있습니다.

💡 WebClient 는 기본적으로 Immutable 하게 생성되므로 싱클톤으로 객체를 생성해서 설정을 그때그때 변경해서 사용할려면 mutable() 속성을 사용하여 생성할 수 있습니다.

 

✅ WebClient Build() 로 객체 얻어오기

public class WebClientUtil {
    
    public static WebClient getBaseUrl(final String uri) {
        return WebClient.builder()
                .baseUrl(uri)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
                .build()
                .mutate()
                .build();
    }
}
@Test
@DisplayName("WebClient build Singleton 사용하기 - kakao api get")
void getKakaoToken() {
    WebClient webClient = WebClientUtil.getBaseUrl(REDIRECT_URI);
    String token = webClient.get()
        .uri(
            "/oauth/authorize?client_id=" + KAKAO_REST_API_KEY
                + "&redirect_uri=" + REDIRECT_URI
                + "&response_type=code")
        .retrieve()
        .bodyToMono(String.class)
        .block();
    System.out.println("token = " + token);
    Assertions.assertThat(token).isNotNull();
}

✅ Create()로 바로 사용하기

@Test
@DisplayName("WebClient create 사용")
void create(){
    WebClient.create()
            .get()
            .uri("")
            .accept(MediaType.APPLICATION_JSON)
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .body()
            .retrieve()
            .bodyToMono(String.class)
            .block();
}

 

👏🏻 여기서 block 은 비동기 논블락 방식으로 Mono 나 Flux 객체를 동기식 데이터 객체로 받아오기위한 메소드입니다.


 

 

3) WebClient Post Request / Response 핸들링하기

  • get(), post(), put(), patch() 다 비슷하기 때문에 Webclient 로 post 사용방법만 알아 보겠습니다.
  • WebClient를 사용하여 응답을 받는 방식으로 retrive() 와 exchange() 2가지 매소드를 이용할 수 있습니다.

 

🔥  retrive() vs exchage()

  • retrvive( ) : retrive() 메소드는 CleintResponse 개체의 body를 받아 디코딩하고 사용자가 사용할 수 있도록 미리 만든 개체를 제공하는 간단한 메소드 입니다. 
  • exchage( ) : ClientResponse를 상태값, 헤더와 함께 가져오는 메소드입니다.

 

➡️ retrive() 는 예외를 처리하기 좋은 api 를 가지고 있지않아, 응답 상태 및 헤더와 함게 더 세밀한 조정이 하고싶을 떄는 exchange를 사용하라고 합니다.

➡️ 하지만, exchange()를 통해 세밀한 조정이 가능하지만, Response 컨텐츠에 대해 모든 처리를 직접 하게되면 메모리 누수가 발생할 가능성이 존재하기 때문에 retrive()를 권장한다고 합니다.

 

 

📌 retrive( )

  • retrive() 를 사용할 때는, toEntity(), bodyToMono(), bodyToFlux() 이렇게 response를 받아올 수 있습니다.
  • bodyToFlux, bodyToMono 는 가져온 body를 각각 Reactor의 Flux와 Mono 객체로 바꿔줍니다.

 

1. 먼저 가장 기본적인 방법으로 retrive() 와 toEntity() 를 이용해 ResponseEntity<T> 로 받는 방법입니다.

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);

 

2. 당연히 헤더를 제외한, 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);

 

3. Block() 을 이용해서 Mono 나 Flux가 아닌 동기식 스타일 일반 객체로 받을 수도 있습니다.

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();

 

 

📌 exchange( )

  • deprecated 되었습니다.
  • 메모리 누수 및/또는 연결 가능성으로 인해 5.3부터 'exchangeToMono(Function)' , 'exchangeToFlux(Function)' 를 사용하라고 합니다.
  • 또한 오류 상태 처리와 함께 ResponseEntity 를 통해 응답 상태 및 헤더에 대한 액세스를 제공하는 retrieve() 사용을 권장합니다!
Mono<Person> entityMono = client.get()
    .uri("/persons/1")
    .accept(MediaType.APPLICATION_JSON)
    .exchangeToMono(response -> {
        if (response.statusCode().equals(HttpStatus.OK)) {
            return response.bodyToMono(Person.class);
        }
        else {
            return response.createException().flatMap(Mono::error);
        }
    });
    
Flux<Person> entityMono = client.get()
      .uri("/persons")
      .accept(MediaType.APPLICATION_JSON)
      .exchangeToFlux(response -> {
          if (response.statusCode().equals(HttpStatus.OK)) {
              return response.bodyToFlux(Person.class);
          }
          else {
              return response.createException().flatMapMany(Mono::error);
          }
      });

 

 

📌 Mono, Flux 사용 방법

  • 위에서 사용했던 block 방식과 non blocking 방식으로 사용할 수 있습니다.
  • Non-Blocking 방식으로 처리하고자 할 때에는 .subscribe() 를 통해 callback 함수를 지정할 수 있습니다.
// blocking
Mono<Employee> employeeMono = webClient.get(). ...
employeeMono.block()

// non-blocking
Mono<Employee> employeeFlux = webClient.get(). ...
employeeFlux.subscribe(employee -> { ... });

 

 

📌 알리고 API 사용 예제, post()

 

👏🏻 저는 알리고 토큰 생성 API를 예제로 진행해보았습니다.

 

  • 알리고에서는 Post 로 formData 에 값을 넣어서 요청하면, Json 형태로 응답값을 준다고 API 스펙에 명시되어있습니다.

[Request 양식]

curl -X POST "https://kakaoapi.aligo.in/akv10/token/create/30/s/" \
	--data-urlencode "apikey=xxxxx" \
	--data-urlencode "userid=xxxxx"

[Response 양식]

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
    "code": 0
    "message": "정상적으로 생성하였습니다."
    "token": tokeeeeeeeeeeennnnnnncoooooodoe
    "urlencode": tokeeeeeeeeeeennnnnnncoooooodourlllllllelela
}

 

Json 으로 들어오는 Body만 받기위해서 Dto 객체를 새롭게 만들어 주었습니다.

@Getter
@NoArgsConstructor
public class AligoApi {
    Integer code;
    String message;
    String token;
    String urlencode;
}

Json 바디값을 객체로 받아오기위해서, 생성자나 Setter 가 필요하진 않았습니다. 이유는 저도 모르겠군여..

🔥그 이유는, bodyToMono(Class<T> bodyResponse) 에서 생성자를 "리플렉션" 으로 생성해주었기 때문입니다.

 

이제 Webclient post 예제입니다.

  • 제가 사용한 방법이니 더 좋은 방법있으면 알려주세요~!
public String generateToken() {
    MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
    formData.add("apikey",APIKEY);
    formData.add("userid",USER_ID);

    Mono<AligoApi> result = WebClient.create().post()
        .uri(ALIGO_HOST + GENERATE_TOKEN_URL)
        .accept(MediaType.APPLICATION_JSON)
        .contentType(MediaType.APPLICATION_FORM_URLENCODED)
        .body(BodyInserters.fromFormData(formData))
        //혹은 바로 formData를 생성할 수 있습니다.
        //.body(BodyInserters.fromFormData("apikey",APIKEY).with("userid",USER_ID))
        .retrieve()
        .bodyToMono(AligoApi.class)
        .timeout(Duration.ofMillis(1000))
        .blockOptional().orElseThrow(
        	() -> new AliGoAPICallException("알리고 토큰을 생성하지 못했습니다.")
         );
        
   return response.getToken();
}

 

 

알리고 응답 실페 케이스

  • 생성자 없어도, null 값이 잘 들어옵니다.. 정말 신기하네
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
	"code": -99
    "message": "계정 아이디(=userid) 파라메더 정보가 전달되지 않았습니다."
}


 

 

🙏 retrive 이냐 exchagne 이냐에 따라서 errorHandling 방법이 달라집니다. 저는 이것에 대해 다루지는 않지만

아래 참조 중 gngsn 님의 WebClient 가이드링크에 상세하게 나와있습니다.

 

 

WebClient 는 Spring Reactor를 기반으로 하고있기 때문에 block, Non-Block & 동기, 비동기의 개념

또 그것을 다루는 Mono 와 Flux 객체의 개념을 알아야한다는 생각이 드는군요.

다음에는 이것에 대해 공부해보자!!

 

 


💡 참고

 

반응형