본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
강의 요약
오늘 강의에서는 Spring WebFlux의 WebClient에 대해 학습했다. WebClient는 Spring 5부터 도입된 비동기 논블로킹 HTTP 클라이언트로, RestTemplate을 대체하는 권장 방식이다. Reactor 기반의 리액티브 스트림을 지원하며, 높은 동시성 처리에 최적화되어 있다. WebClient에 대해 추가적으로 알아보자.
Connection Pool 설정
WebClient는 기본적으로 Reactor Netty를 사용하며, Connection Pool과 Timeout 설정이 기본 제공된다. 대부분의 경우 기본 설정으로 충분하지만, 대규모 동시 요청을 처리하거나 특정 트래픽 패턴에 최적화가 필요한 경우 이 두 가지를 함께 튜닝해야 한다. 잘 구성된 Connection Pool과 적절한 Timeout은 효율적인 요청 처리와 리소스 고갈 방지에 핵심적인 역할을 한다. Connection Pool 크기는 무조건 크게 설정하는 것이 좋은 것은 아니다. 너무 크면 불필요한 메모리를 사용하고, 너무 작으면 연결 대기로 인한 지연이 발생한다. 모니터링을 통해 실제 사용 패턴을 확인하고 조정하는 것이 중요하다.
기본 설정
- Connection Pool: maxConnections 500
- pendingAcquireTimeout 45초
- Timeout: 기본값 없음 (무한 대기)
ConnectionProvider pool = ConnectionProvider.builder("custom-pool")
.maxConnections(1000)
.pendingAcquireTimeout(Duration.ofSeconds(30))
.maxIdleTime(Duration.ofSeconds(20))
.build();
HttpClient httpClient = HttpClient.create(pool)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // TCP 연결 타임아웃
.responseTimeout(Duration.ofSeconds(10)); // 전체 응답 타임아웃
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
| 설정 항목 | 설명 | 선택 기준 |
| maxConnections | 최대 연결 수 | 예상 동시 요청 수보다 충분히 크게 설정 |
| pendingAcquireTimeout | 연결 대기 시간 | 평균 응답 시간의 2-3배 권장 |
| maxIdleTime | 유휴 연결 유지 시간 | 서버의 keep-alive 시간보다 짧게 설정 |
| responseTimeout | 응답 대기 시간 | API 특성에 따라 차등 적용 |
Timeout 설정 전략
Timeout은 단순히 시간 제한이 아닌, 장애 전파를 차단하는 핵심 메커니즘이다. WebClient는 여러 단계의 Timeout을 제공한다. 각 Timeout의 역할은 명확히 구분된다.
- Connect Timeout은 네트워크 장애를 빠르게 감지하고
- Response Timeout은 느린 서버로부터 시스템을 보호한다.
- Read/Write Timeout은 데이터 전송 중 발생하는 지연을 제어한다.
API 특성에 따라 Timeout을 차등 적용하는 것이 효과적이다. 빠른 조회 API는 짧은 Timeout을, 배치 처리나 대용량 데이터 전송은 긴 Timeout을 설정한다.
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // TCP 연결 타임아웃
.responseTimeout(Duration.ofSeconds(10)) // 전체 응답 타임아웃
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10)) // 읽기 타임아웃
.addHandlerLast(new WriteTimeoutHandler(10))); // 쓰기 타임아웃
Error Handling과 Retry 전략
HTTP 요청 실패는 불가피하며, 이를 어떻게 처리하느냐가 시스템 안정성을 결정한다. WebClient는 선언적인 에러 핸들링을 지원한다. Retry 전략은 에러 유형에 따라 다르게 적용해야 한다. 4xx 에러는 클라이언트 측 문제이므로 재시도가 무의미하지만, 5xx 에러나 네트워크 장애는 일시적일 수 있어 재시도가 효과적이다. Exponential Backoff를 사용하면 순간적인 부하 증가를 방지할 수 있다.
멱등성이 보장되지 않는 요청(POST, PUT 등)은 재시도 시 중복 처리 위험이 있으므로 신중하게 적용해야 한다.
webClient.get()
.uri("/api/data")
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError,
response -> Mono.error(new ClientException()))
.onStatus(HttpStatusCode::is5xxServerError,
response -> Mono.error(new ServerException()))
.bodyToMono(Data.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.filter(throwable -> throwable instanceof ServerException));
Circuit Breaker 통합
외부 서비스 장애가 전체 시스템으로 전파되는 것을 방지하기 위해 Circuit Breaker를 적용할 수 있다. Resilience4j와의 통합이 일반적이다. Circuit Breaker는 실패율이 임계값을 초과하면 요청을 차단하여 시스템 자원을 보호한다. Open 상태에서는 즉시 예외를 반환하고, Half-Open 상태에서는 일부 요청만 허용하여 서비스 복구를 확인한다.
@Bean
public WebClient webClient(CircuitBreakerRegistry registry) {
CircuitBreaker circuitBreaker = registry.circuitBreaker("external-api");
return WebClient.builder()
.filter((request, next) ->
Mono.defer(() -> circuitBreaker.run(
next.exchange(request),
throwable -> Mono.error(new ServiceUnavailableException())
))
)
.build();
}
Backpressure 처리
대량의 요청을 처리할 때는 Backpressure를 고려해야 한다. 무분별한 병렬 처리는 오히려 성능 저하를 일으킬 수 있다. flatMap의 concurrency 파라미터로 동시 요청 수를 제한하면, 서버 과부하를 방지하면서도 효율적인 처리가 가능하다. 적절한 값은 대상 서버의 처리 능력과 네트워크 대역폭을 고려하여 결정한다.
Flux.range(1, 10000)
.flatMap(id ->
webClient.get()
.uri("/items/{id}", id)
.retrieve()
.bodyToMono(Item.class),
50 // 동시 요청 수 제한
)
.subscribe();
참고 출처
- https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html
- https://medium.com/@pradeepisuru31/mastering-spring-webclient-a693f90447f0
- https://dzone.com/articles/spring-boot-webclient-optimizing-performance-and-resilience



