๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Spring/Spring Boot

[Spring] WebClient ๊ธฐ์ดˆ ๊ฐœ๋…๋ถ€ํ„ฐ ์‚ฌ์šฉ๋ฐฉ๋ฒ•๊นŒ์ง€

by ๋ฏผ๋Œv 2022. 11. 23.
728x90

๐Ÿ“— Webclient ๊ธฐ๋ณธ ๊ฐœ๋… ๋ฐ ์‚ฌ์šฉํ•ด์•ผํ•˜๋Š” ์ด์œ 

 

์ €๋ฒˆ ํฌ์ŠคํŒ…์— ์ด์–ด์„œ 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 ๊ฐ์ฒด์˜ ๊ฐœ๋…์„ ์•Œ์•„์•ผํ•œ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“œ๋Š”๊ตฐ์š”.

๋‹ค์Œ์—๋Š” ์ด๊ฒƒ์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ•ด๋ณด์ž!!

 

 


๐Ÿ’ก ์ฐธ๊ณ 

 

728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€0