λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
BackEnd🌱/Spring

WebClientμ—μ„œ μ—λŸ¬ μ²˜λ¦¬μ™€ μž¬μ‹œλ„ν•˜λŠ” 방법

by dkswnkk 2023. 8. 3.

μ„œλ‘ 

HTTP μš”μ²­μ€ λ„€νŠΈμ›Œν¬ 지연, μΌμ‹œμ μΈ μ„œλ²„ 였λ₯˜, 잘λͺ»λœ μš”μ²­ λ“± λ‹€μ–‘ν•œ 이유둜 μ‹€νŒ¨ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이런 경우 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ μ‹€ν–‰ 흐름에 문제λ₯Ό μΌμœΌν‚¬ 수 μžˆκΈ°μ— μ—λŸ¬λ₯Ό μ˜¬λ°”λ₯΄κ²Œ μ²˜λ¦¬ν•΄μ•Ό ν•©λ‹ˆλ‹€.

이번 κΈ€μ—μ„œλŠ” WebClientλ₯Ό μ‚¬μš©μ‹œ HTTPμš”μ²­ κ³Όμ •μ—μ„œ λ°œμƒν•œ μ—λŸ¬λ₯Ό μ²˜λ¦¬ν•˜λŠ” 방법과 λ³΅κ΅¬ν•˜κΈ° μœ„ν•΄ μž¬μ‹œλ„ μ²˜λ¦¬ν•˜λŠ” μ „λž΅μ— λŒ€ν•΄ 정리해 λ³΄κ² μŠ΅λ‹ˆλ‹€.
 

 

μ—λŸ¬ 처리

onErrorReturn()

onErrorReturn()은 μ—λŸ¬κ°€ λ°œμƒν–ˆμ„ λ•Œ 주어진 default 값을 λ°˜ν™˜ν•˜λŠ” λ©”μ„œλ“œμž…λ‹ˆλ‹€. μ΄λŠ” 비동기 ν†΅μ‹ μ—μ„œ μ€‘μš”ν•œ 역할을 ν•˜λŠ”λ°, 특히 λ„€νŠΈμ›Œν¬ 였λ₯˜ λ˜λŠ” μ„œλ²„μ˜ 문제둜 μΈν•œ μ—λŸ¬κ°€ λ°œμƒν–ˆμ„ λ•Œ 톡신 자체λ₯Ό μ€‘λ‹¨μ‹œν‚€μ§€ μ•Šκ³ , λ””ν΄νŠΈ 값을 λ°˜ν™˜ν•˜μ—¬ μ •μƒμ μœΌλ‘œ 계속 μž‘λ™ν•  수 μžˆλ„λ‘ ν•  수 있기 λ•Œλ¬Έμž…λ‹ˆλ‹€.

webClient.get()
    .uri("/some-endpoint")
    .retrieve()
    .onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.just(new CustomClientException()))
    .onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.just(new CustomServerException()))
    .bodyToMono(String.class)
    .onErrorReturn("Default Value")
    .subscribe(System.out::println);

μœ„ μ½”λ“œμ—μ„œλŠ” 4xx ν΄λΌμ΄μ–ΈνŠΈ μ—λŸ¬λ‚˜ 5xx μ„œλ²„ μ—λŸ¬κ°€ λ°œμƒν–ˆμ„ κ²½μš°μ—λŠ” μ‚¬μš©μžκ°€ μ •μ˜ν•œ μ˜ˆμ™Έλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. 그리고 onErrorReturn()을 μ‚¬μš©ν•˜μ—¬ μ˜ˆμ™Έλ₯Ό λ°˜ν™˜ν•˜λŠ” λŒ€μ‹  μ΅œμ’…μ μœΌλ‘œ default 값을 λ°˜ν™˜ν•˜λ„λ‘ μ„€μ •λ©λ‹ˆλ‹€. 이λ₯Ό 톡해 μ—λŸ¬κ°€ λ°œμƒν•΄λ„ 데이터 흐름이 μ€‘λ‹¨λ˜μ§€ μ•Šκ³  μ§€μ •λœ  "Default Value"λΌλŠ” 값이 λ°˜ν™˜λ˜μ–΄ 후속 μ²˜λ¦¬κ°€ κ°€λŠ₯ν•©λ‹ˆλ‹€.

 

onErrorResume()

onErrorResume()λŠ” μ—λŸ¬ λ°œμƒ μ‹œ λŒ€μ²΄ν•  수 μžˆλŠ” μƒˆλ‘œμš΄ 데이터 μŠ€νŠΈλ¦Όμ„ μ œκ³΅ν•˜λŠ” λ©”μ„œλ“œμž…λ‹ˆλ‹€. onErrorReturn()와 λΉ„μŠ·ν•˜μ§€λ§Œ, 더 일반적인 λ°©λ²•μœΌλ‘œ μ—λŸ¬λ₯Ό μ²˜λ¦¬ν•©λ‹ˆλ‹€.

webClient.get()
    .uri("/some-endpoint")
    .retrieve()
    .bodyToMono(String.class)
    .onErrorResume(e -> {
        log.error("Error: ", e);
        return Mono.just("Fallback");
    })
    .subscribe(System.out::println);

μœ„μ˜ μ½”λ“œμ—μ„œ onErrorResume()은 onErrorReturn()κ³Ό 달리 μƒˆλ‘œμš΄ Monoλ₯Ό μƒμ„±ν•˜κ³  "Fallback"μ΄λΌλŠ” 값을 λ°˜ν™˜ν•©λ‹ˆλ‹€. 이 경우, μ›λž˜μ˜ Monoμ—μ„œ μ—λŸ¬κ°€ λ°œμƒν•˜λ©΄ onErrorResume()에 μ˜ν•΄ μƒˆλ‘œμš΄ Mono둜 ꡐ체되고, "Fallback" 값을 λ°˜ν™˜ν•˜κ²Œ λ©λ‹ˆλ‹€.

 

doOnError()

doOnError()λŠ” μ—λŸ¬κ°€ λ°œμƒν–ˆμ„ λ•Œ 좔가적인 λ‘œμ§μ„ μˆ˜ν–‰ν•˜λ„λ‘ ν•©λ‹ˆλ‹€. 이 방법을 μ‚¬μš©ν•˜λ©΄ μ—λŸ¬κ°€ λ°œμƒν•˜λ©΄μ„œλ„ λͺ…μ‹œμ μœΌλ‘œ μ—λŸ¬λ₯Ό μ²˜λ¦¬ν•˜μ§€ μ•Šκ³  λ”μš± ꡬ체적인 λ‘œμ§μ„ μˆ˜ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, μ—λŸ¬κ°€ λ°œμƒν•  λ•Œλ§ˆλ‹€ μΉ΄μš΄ν„°λ₯Ό μ¦κ°€μ‹œν‚€κ±°λ‚˜ 둜그λ₯Ό 좜λ ₯ν•˜λŠ” λ“±μ˜ μž‘μ—…μ„ μˆ˜ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

webClient.get()
    .uri("/some-endpoint")
    .retrieve()
    .bodyToMono(String.class)
    .doOnError(e -> {
        log.error("Error occurred: ", e);
        errorCounter.incrementAndGet();
    })
    .subscribe(System.out::println);

μœ„ μ½”λ“œμ—μ„œ, doOnError()λŠ” μ—λŸ¬κ°€ λ°œμƒν–ˆμ„ λ•Œ 둜그λ₯Ό 좜λ ₯ν•˜κ³  μ—λŸ¬ μΉ΄μš΄ν„°λ₯Ό μ¦κ°€μ‹œν‚΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ 이 κ²½μš°μ—λ„ onErrorReturn()μ΄λ‚˜ onErrorResume()처럼 μ—λŸ¬λ₯Ό μ²˜λ¦¬ν•˜κ±°λ‚˜ μƒˆλ‘œμš΄ 슀트림으둜 κ΅μ²΄λŠ” ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

 

onErrorMap()

onErrorMap()은 μ›λž˜μ˜ μ˜ˆμ™Έλ₯Ό λ‹€λ₯Έ μ˜ˆμ™Έλ‘œ λ³€ν™˜ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. μ΄λŠ” λ‹€μ–‘ν•œ νƒ€μž…μ˜ μ˜ˆμ™Έλ₯Ό νŠΉμ • μ˜ˆμ™Έλ‘œ λ§€ν•‘ν•˜μ—¬, μƒμœ„ λ ˆλ²¨μ—μ„œ μΌκ΄€λœ μ˜ˆμ™Έ 처리λ₯Ό ν•  수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€.

webClient.get()
    .uri("/some-endpoint")
    .retrieve()
    .bodyToMono(String.class)
    .onErrorMap(original -> new CustomException("Something went wrong", original))
    .subscribe(System.out::println);

μœ„ μ½”λ“œμ—μ„œ onErrorMap()은 μ›λž˜μ˜ μ˜ˆμ™Έλ₯Ό CustomException으둜 λ³€ν™˜ν•©λ‹ˆλ‹€.
 

 

μž¬μ‹œλ„ μ „λž΅

retry()

retry()λŠ” μ—λŸ¬ λ°œμƒ μ‹œ 주어진 횟수만큼 μž¬μ‹œλ„λ₯Ό μˆ˜ν–‰ν•˜λŠ” κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€. 이 λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜λ©΄ μΌμ‹œμ μΈ 였λ₯˜λ‘œ μΈν•œ μš”μ²­ μ‹€νŒ¨λ₯Ό μžλ™μœΌλ‘œ λ³΅κ΅¬ν•˜λŠ” 데 μœ μš©ν•©λ‹ˆλ‹€.

webClient.get()
    .uri("/some-endpoint")
    .retrieve()
    .bodyToMono(String.class)
    .retry(3)
    .subscribe(System.out::println);

μœ„μ˜ μ½”λ“œμ—μ„œ retry(3)λŠ” μ›Ή μš”μ²­μ΄ μ‹€νŒ¨ν•˜λ©΄ μ΅œλŒ€ 3λ²ˆκΉŒμ§€ μš”μ²­μ„ μž¬μ‹œλ„ν•˜λ„λ‘ μ„€μ •ν•©λ‹ˆλ‹€. λ”°λΌμ„œ μΌμ‹œμ μΈ λ„€νŠΈμ›Œν¬ 였λ₯˜λ‚˜ μ„œλ²„ 였λ₯˜ λ“±μœΌλ‘œ 인해 μš”μ²­μ΄ μ‹€νŒ¨ν•˜λ”λΌλ„ μ„€μ •ν•œ 만큼 μž¬μ‹œλ„ν•˜μ—¬ μ„±κ³΅μ μœΌλ‘œ 응닡을 받을 수 μžˆμŠ΅λ‹ˆλ‹€.

 

retryWhen()

retryWhen()λŠ” retry()보닀 μ’€ 더 μ „λž΅μ„ μ„Έλ°€ν•˜κ²Œ μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, νŠΉμ • μ‹œκ°„ κ°„κ²©μœΌλ‘œ μž¬μ‹œλ„ν•˜κ±°λ‚˜, νŠΉμ • 쑰건을 λ§Œμ‘±ν•  λ•Œλ§Œ μž¬μ‹œλ„λ₯Ό μˆ˜ν–‰ν•˜λŠ” λ“±μ˜ λ³΅μž‘ν•œ μž¬μ‹œλ„ μ „λž΅μ„ μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

webClient.get()
    .uri("/some-endpoint")
    .retrieve()
    .bodyToMono(String.class)
    .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
    .subscribe(System.out::println);

μœ„ μ½”λ“œμ—μ„œ retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))λŠ” μš”μ²­μ΄ μ‹€νŒ¨ν•˜λ©΄ μ΅œλŒ€ 3λ²ˆκΉŒμ§€ μž¬μ‹œλ„ν•˜λ˜, 각 μž¬μ‹œλ„ μ‚¬μ΄μ—λŠ” 1초의 λ”œλ ˆμ΄λ₯Ό 가지도둝 μ„€μ •ν•©λ‹ˆλ‹€. 

 

retryBackOff()

retryBackOff()λŠ” μ‹€νŒ¨ν•œ μž‘μ—…μ„ μž¬μ‹œλ„ν•˜λŠ” λ™μ•ˆ μ§€μˆ˜ λ°±μ˜€ν”„ μ „λž΅μ„ μˆ˜ν–‰ν•©λ‹ˆλ‹€. 즉, μž¬μ‹œλ„ κ°„μ˜ 지연 μ‹œκ°„μ΄ 점차적으둜 μ¦κ°€ν•˜λ„λ‘ ν•˜μ—¬, μ„œλ²„μ— λΆˆν•„μš”ν•œ λΆ€ν•˜λ₯Ό 주지 μ•Šλ„λ‘ ν•©λ‹ˆλ‹€. 지연 μ‹œκ°„μ˜ μ΅œλŒ€μΉ˜λ₯Ό μ„€μ •ν•˜κ±°λ‚˜ μž¬μ‹œλ„ 횟수의 μ΅œλŒ€μΉ˜λ₯Ό μ„€μ •ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

webClient.get()
    .uri("/some-endpoint")
    .retrieve()
    .bodyToMono(String.class)
    .retryBackoff(3, Duration.ofMillis(100), Duration.ofSeconds(1))
    .subscribe(System.out::println);

μœ„ μ½”λ“œμ—μ„œλŠ” μš”μ²­μ΄ μ‹€νŒ¨ν•˜λ©΄ μ΅œλŒ€ 3λ²ˆκΉŒμ§€ μž¬μ‹œλ„ν•˜λ©°, 각 μž¬μ‹œλ„ μ‚¬μ΄μ˜ 지연 μ‹œκ°„μ€ 초기 μ§€μ—°μ‹œκ°„μΈ 100밀리 μ΄ˆλΆ€ν„° μ‹œμž‘ν•˜μ—¬, 각 μž¬μ‹œλ„λ§ˆλ‹€ μ§€μˆ˜μ μœΌλ‘œ μ¦κ°€ν•˜λ©°, 1초λ₯Ό μ΄ˆκ³Όν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
 

기타

timeout()

timeout()은 μ§€μ •λœ μ‹œκ°„ 내에 μ›ν•˜λŠ” κ²°κ³Όκ°€ λ„λ‹¬ν•˜μ§€ μ•Šμ„ 경우 TimeoutException을 λ°œμƒμ‹œν‚΅λ‹ˆλ‹€.

webClient.get()
    .uri("/some-endpoint")
    .retrieve()
    .bodyToMono(String.class)
    .timeout(Duration.ofSeconds(1))
    .subscribe(System.out::println);

μœ„ μ½”λ“œμ—μ„œλŠ” μš”μ²­μ— λŒ€ν•œ 응닡이 1초 이내에 λ„λ‹¬ν•˜μ§€ μ•ŠμœΌλ©΄ TimeoutException이 λ°œμƒν•©λ‹ˆλ‹€.

 

repeat()

repeat()λŠ” μ„±κ³΅μ μœΌλ‘œ μ™„λ£Œλœ μž‘μ—…μ„ λ°˜λ³΅ν•˜λŠ” λ©”μ„œλ“œμž…λ‹ˆλ‹€. μ΄λŠ” 였λ₯˜κ°€ μ•„λ‹ˆλΌ μ„±κ³΅μ μœΌλ‘œ μ™„λ£Œλœ μž‘μ—…μ— λŒ€ν•œ 반볡이 ν•„μš”ν•  λ•Œ μ‚¬μš©λ˜λŠ”λ°, 예λ₯Ό λ“€μ–΄, 데이터λ₯Ό 주기적으둜 가져와야 ν•˜λŠ” 경우 repeat() ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜μ—¬ μš”μ²­μ„ μ •κΈ°μ μœΌλ‘œ λ°˜λ³΅ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

webClient.get()
    .uri("/some-endpoint")
    .retrieve()
    .bodyToMono(String.class)
    .repeat(3)
    .subscribe(System.out::println);

μœ„ μ½”λ“œλŠ” μ›Ή μš”μ²­μ„ μ„±κ³΅μ μœΌλ‘œ μ™„λ£Œν•œ 후에 3번 더 μš”μ²­μ„ λ°˜λ³΅ν•©λ‹ˆλ‹€.
 

 

마무리

WebClientλ₯Ό μ‚¬μš©ν•˜μ—¬ HTTP μš”μ²­μ„ 보낼 λ•Œ, μ΄λŸ¬ν•œ λ‹€μ–‘ν•œ μ—λŸ¬ μ²˜λ¦¬μ™€ μž¬μ‹œλ„ μ „λž΅μ„ μ‚¬μš©ν•˜λ©΄ λ°œμƒν•  수 μžˆλŠ” λ‹€μ–‘ν•œ λ¬Έμ œλ“€μ„ 보닀 효과적으둜 μ²˜λ¦¬ν•˜κ³  볡ꡬ할 수 μžˆμŠ΅λ‹ˆλ‹€.

λŒ“κΈ€