14.2 WebClient를 활용한 비동기 논블로킹 API 호출
RestTemplate이 구시대의 동기적(Blocking) 클라이언트였다면, 현재 스프링 부트 생태계의 비동기(Non-Blocking) HTTP 통신 표준은 압도적으로 WebClient 입니다.
외부 API(카카오 페이 결제 등)와 연동할 때 "1초 안에 응답 안 오면 강제 폭파", "에러 나면 딱 3번까지만 다시 재요청(Retry)" 과 같은 실무 등급의 안정성 방어벽 코드를 짜는 법을 알아봅시다.
🛡️ 1. 안전한 WebClient 기본 무장 설정 (Timeout)
결제망 API가 죽어서 영원히 응답 통신 패킷을 안 내려주는 경우, 우리 백엔드 서버마저도 무한 대기 상태(좀비화)에 빠져버립니다. 클라이언트를 만들 때 애초에 "타임아웃(Timeout)" 타이머 신관을 장착해야 합니다.
@Configuration
public class WebClientConfig {
@Bean
public WebClient kakaoPayWebClient() {
// [비동기 네트워크 엔진(Netty) 하부 타임아웃 세팅]
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000) // 3초 내로 연결 접속 안 되면 터짐
.responseTimeout(Duration.ofSeconds(5)) // 데이터 바디가 5초 동안 지연되면 얄짤없음
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(5, TimeUnit.SECONDS))
.addHandlerLast(new WriteTimeoutHandler(5, TimeUnit.SECONDS)));
return WebClient.builder()
.baseUrl("https://kapi.kakao.com/v1/payment")
.defaultHeader("Authorization", "KakaoAK 12341234") // 매번 들어가는 기본 공통 헤더
.clientConnector(new ReactorClientHttpConnector(httpClient)) // 무장된 Netty 엔진 장착
.build();
}
}
🔄 2. API 타 비동기 통신 및 마법의 재시도(Retry) 기법
외부 인증 서버에 호출을 날리다 보면, "일시적인 네트워크 순단 502 에러" 등으로 1번 실패할지라도 방금 그거 1번 더 쏘면 운 좋게 바로 성공하는 케이스가 실무에 아주 많습니다.
Mono 뒤에 붙이는 체이닝으로 기적 같은 선언형 재시도(Retry)를 매우 은아하게 짤 수 있습니다.
@Service
@RequiredArgsConstructor
public class PaymentApiService {
private final WebClient webClient; // 위에서 세팅한 무장 벤츠
public Mono<PaymentResultDto> approvePayment(String orderId) {
return webClient.post()
.uri("/approve")
.bodyValue(new PayRequest(orderId))
.retrieve()
// 1. 에러 응답 코드(4xx, 5xx) 가 날아왔을 때의 분기 처리
.onStatus(HttpStatus::is4xxClientError, response -> {
System.out.println("니가 잘못 보냈잖음!");
return Mono.error(new IllegalArgumentException("결제 파라미터 엉망"));
})
.onStatus(HttpStatus::is5xxServerError, response -> {
System.out.println("상대 카카오 페이 서버가 터진 거임!");
return Mono.error(new ExternalServerException("카카오 연결 불가"));
})
// 2. 정상 응답일 경우 DTO 껍데기로 매핑 조립
.bodyToMono(PaymentResultDto.class)
// 3. ✨ [마법의 백오프 재시도 (Exponential Backoff Retry)] ✨
// 카카오 서버 에러나 커넥션 타입아웃 예외가 터졌을 때만, 최대 3번까지 재요청 수행함!!
// 첫 실패 시 1초 기다리다 다시 빔 쏘고, 또 실패하면 2초 뒤, 또 실패하면 4초 뒤... 갈수록 간격이 커짐(Backoff)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.filter(throwable -> throwable instanceof ExternalServerException
|| throwable instanceof TimeoutException)
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
throw new RuntimeException("3번 눈물의 발악을 했지만 최종 망했습니다.");
}))
// 4. 마지막에 타임아웃 강제 브레이크 한 번 더 보험
.timeout(Duration.ofSeconds(10));
}
}
🎯 3. 고수 팁 (Pro Tips)
💡 주의해야 할
exchangeToMonovsretrieve의 미세한 차이점
retrieve(): 가장 일반적이고 간결합니다. 상태 코드 필터링(onStatus)과 바디 변환 처리를 손쉽게 끝낼 수 있으며, 메모리 릭(누수) 방어 처리가 완벽히 되어있습니다.exchangeToMono(): 과거exchange()의 최신 대체품입니다. 상태 코드뿐만 아니라 헤더 값들을 아주 디테일하게 까봐야 하는 등 "응답에 대한 극한의 자유도 높은 컨트롤" 이 필요할 때만 씁니다. 단, 이 콜백 안에서 응답 바디를 제대로 소모하여 다 털어먹지 않고 중간에 예외를 뱉고 끊어버리면 네트워크 소켓 커넥션이 회수되지 않고 영원히 열린 상태로 방치되어 메모리 대참사(Leak)가 일어납니다. 정말 고급 컨트롤이 아닌 이상 무조건retrieve()체인을 쓰세요!