WebClient: 에러 핸들링, 타임 아웃 설정

김기현·2025년 8월 4일

Spring WebFlux

목록 보기
16/28

WebClient: 타임아웃 설정

WebClient는 다양한 종류의 타임 아웃을 설정할 수 있다. 각 타임아웃은 다른 계층에서 작동하며 필요에 따라 세밀하게 제어할 수 있다.

1. 연결 타임아웃 (Connection Timeout)

  • 설정 방법: HttpClient를 사용하여 설정하며 연결을 설정하는 데 걸리는 최대 시간을 정의한다.

예시

public class TimeoutWebClientConfiguration {

    @Bean
    public WebClient connectionTimeoutWebClient(WebClient.Builder clientBuilder) {
        HttpClient httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);

        return clientBuilder
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .baseUrl("http://localhost:8080")
                .build();
    }
}

2. 읽기 타임아웃 (Read Timeout)

  • 설정 방법: HttpClientdoOnConnected 콜백을 통해 ReadTimeoutHandler를 추가하여 설정한다. 응답 데이터의 읽기 작업이 완료될 때까지 기다리는 최대 시간을
    정의한다.

예시

public class TimeoutWebClientConfiguration {

    @Bean
    public WebClient readTimeoutWebClient(WebClient.Builder clientBuilder) {
        HttpClient httpClient = HttpClient.create()
                .doOnConnected(connection ->
                        connection.addHandlerLast(new ReadTimeoutHandler(10, TimeUnit.SECONDS))
                );

        return clientBuilder
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}

3. 쓰기 타임아웃 (Write Timeout)

  • 설정 방법: HttpClientdoOnConnected콜백을 통해 WriteTimeoutHandler를 추가하여 설정한다. 요청 데이터를 쓰는 데 걸리는 최대 시간을 정의한다.
public class TimeoutWebClientConfiguration {

    @Bean
    public WebClient writeTimeoutWebClient(WebClient.Builder clientBuilder) {
        HttpClient httpClient = HttpClient.create()
                .doOnConnected(connection ->
                        connection.addHandlerLast(new WriteTimeoutHandler(10, TimeUnit.SECONDS))
                );

        return clientBuilder
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}

4. 응답 타임아웃 (Response Timeout)

  • 설정 방법: HttpClientresponseTimeout()메소드를 사용하여 설정한다. 연결이 설정된 후 응답이 시작되기까지 기다리는 최대 시간을 정의한다.

예시

public class TimeoutWebClientConfiguration {

    @Bean
    public WebClient responseTimeoutWebClient(WebClient.Builder clientBuilder) {
        HttpClient httpClient = HttpClient.create()
                .responseTimeout(Duration.ofSeconds(10))    // 10초

        return clientBuilder
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}

5. 반응형(Reactive)또는 시그널 타임아웃(.timeout())

  • 설정 방법: Mono또는 Flux체인에 .timeout()연산자를 사용하여 설정한다. 이는 전체 리액티브 스트림 작업(연결, 요정 전송, 응답 수신 등 모든 과정)에 대한 높은 수준의
    타임아웃이다.

@RestController
@RequiredArgsConstructor
public class TimeoutController {
    private final TimeoutService timeoutService;

    @GetMapping("/example-endpoint")
    public Mono<String> callApi() {
        return timeoutService.callExampleEndpointWithTimeout();
    }
}

@Service
public class TimeoutService {

    private final WebClient webClient;

    public TimeoutService(WebClient connectionTimeoutWebClient) {
        this.webClient = connectionTimeoutWebClient;
    }

    public Mono<String> callExampleEndpointWithTimeout() {
        return webClient.get()
                .uri("/example-endpoint") // 실제로 호출할 API 경로
                .retrieve()
                .bodyToMono(String.class)
                // 여기에 개별 요청에 대한 타임아웃 설정
                .timeout(Duration.ofSeconds(5), Mono.just("Fallback 실패")) // 5초 안에 응답이 없으면 폴백
                .doOnSuccess(success -> System.out.println("성공: " + success))
                .doOnError(error -> {
                    // 폴백 메시지는 이미 처리되었으므로, 다른 예외에 대한 로깅
                    if (!error.getMessage().equals("Fallback 실패")) {
                        System.err.println("실패: " + error.getMessage());
                    }
                });
        // .subscribe()는 이 메서드를 호출하는 곳(컨트롤러 등)에서 필요에 따라 호출하거나,
        // 리액티브 스트림을 계속 연결하여 처리하는 것이 일반적이다.
    }
}

WenClient 에러 핸들링

WebClient는 리액티브 스트림에서 에러가 발생했을 때 onError시그널을 발생시킨다. 이를 처리하는 다양한 방법이 있다.

1. HTTP 상태 코드에 따른 에러 처리 (onStatus)

특정 HTTP 상태 코드에 따라 커스텀 예외를 발생시키거나 다른 처리를 할 수 있다.


@Service
public class WebClientErrorHandlingService {
    private final WebClient webClient;

    public WebClientErrorHandlingService(WebClient webClient) {
        this.webClient = webClient;
    }

    public Mono<String> onStatusHandling() {
        return webClient.get()
                .uri("/example-endpoint")
                .retrieve()
                .onStatus(
                        HttpStatusCode::is4xxClientError,
                        clientResponse ->
                                Mono.error(new RuntimeException("Client Error: " + clientResponse.statusCode().value()))
                )
                .onStatus(
                        HttpStatusCode::is5xxServerError,
                        clientResponse ->
                                Mono.error(new RuntimeException("Server Error: " + clientResponse.statusCode().value()))
                )
                .bodyToMono(String.class);
    }
}

2. 에러 발생 시 대체 값 반환 (onErrorReturn)

에러가 발생했을 때 스트림을 종료하지 않고 미리 정의된 대체값을 반환한다.


@Service
public class WebClientErrorHandlingService {
    private final WebClient webClient;

    public WebClientErrorHandlingService(WebClient webClient) {
        this.webClient = webClient;
    }

    public Mono<String> onErrorReturnHandling() {
        return webClient.get()
                .uri("/example-endpoint")
                .retrieve()
                .bodyToMono(String.class)
                .onErrorReturn("Default Value"); // 에러 발생 시 "Default Value" 반환
    }
}

3. 에러 발생 시 다른 Mono/Flux로 전환 (onErrorResume)

에러가 발생했을 때 다른 Publisher로 전환하여 폴백 로직을 수행할 수 있다.


@Service
public class WebClientErrorHandlingService {
    private final WebClient webClient;

    public WebClientErrorHandlingService(WebClient webClient) {
        this.webClient = webClient;
    }

    public Publisher<String> onErrorResumeHandling() {
        return webClient.get()
                .uri("/error-endpoint")
                .retrieve()
                .bodyToMono(String.class)
                .onErrorResume(e -> {
                    System.err.println("Error occurred: " + e.getMessage() + ". Falling back...");
                    return Mono.just("Fallback Data from Error"); // 에러 발생 시 다른 Mono 반환
                });
    }
}

4. 에러 매핑 (onErrorMap)

발생한 예외를 다른 예외로 변환하여 상위 계층으로 전달한다.


@Service
public class WebClientErrorHandlingService {
    private final WebClient webClient;

    public WebClientErrorHandlingService(WebClient webClient) {
        this.webClient = webClient;
    }

    public Mono<String> onErrorMapHandling() {
        return webClient.get()
                .uri("/error-endpoint")
                .retrieve()
                .bodyToMono(String.class)
                .onErrorMap(ReadTimeoutException.class, ex -> new HttpTimeoutException("Read Timeout Occurred!"));
    }
}

5. 재시도 (retry/retryWhen)

일시적인 네트워크 문제 등으로 인해 실패한 요청을 자동으로 재시도하도록 설정할 수 있다.

  • retry: 지정된 횟수만큼 에러 발생 시 재시도한다.
  • retryWhen:더 복잡한 재시도 로직을 정의할 수 있다. 예를 들어 특정 조건에서만 재시도하거나 지수 백오프와 같은 지연 전략을 사용할 수 있다.

@Service
public class WebClientErrorHandlingService {
    private final WebClient webClient;

    public WebClientErrorHandlingService(WebClient webClient) {
        this.webClient = webClient;
    }

    public Mono<String> retryHandling() {
        return webClient.get()
                .uri("/unstable-endpoint")
                .retrieve()
                .bodyToMono(String.class)
                .retry(3); // 최대 3번 재시도
    }

    public Mono<String> retryWhenHandling() {
        return webClient.get()
                .uri("/unstable-endpoint")
                .retrieve()
                .bodyToMono(String.class)
                .retryWhen(Retry.backoff(3, Duration.ofSeconds(2)) // 3번 재시도, 2초부터 지수적으로 증가하는 지연
                        .filter(ex -> ex instanceof WebClientRequestException || ex instanceof HttpStatusCodeException)
                        .doBeforeRetry(retrySignal -> System.out.println("Retrying... " + retrySignal.totalRetriesInARow() + " time")));
    }
}

전역 설정 vs 개별 요청 설정

전역 설정

  • WebClient.Builder를 통해 HttpClient를 구성하여 WebClient인스턴스를 생성할 때 타임아웃을 설정하면 해당 인스턴스를 사용하는 모든 요청에 동일한 타임아웃이 적용된다.
  • 이는 애플리케이션 전체에 일관된 정책을 적용할 때 유용하다.

개별 요청 설정

  • Mono또는 Flux체인데 .timeout()연산자를 사용하는 것은 해당 특정 요청에만 적용되는 타임아웃이다.
  • 이는 특정 API 호출에 대해 다른 타임아웃 정책이 필요할 때 유용하다.

일반적인 권장 사항

  • 애플리케이션 전반에 걸쳐 대부분의 외부 호출에 적용되는 기본 타임아웃 정책은 전역을 설정한다.
  • 특정 API 호출이 예상보다 오래 걸리거나 빠르게 실패해야 하는 경우에만 요청에 대한 .timeout()을 사용하여 재정의한다.
profile
백엔드 개발자를 목표로 공부하는 대학생

0개의 댓글