Files

3.9 KiB

Timeout 예시

좋은 예시

예시 1. 전역 기본값은 공통 설정으로 둔다

spring:
  http:
    clients:
      connect-timeout: 500ms
      read-timeout: 2s

좋은 이유:

  • 서비스 전체 기본값이 한 곳에 있다
  • 모든 client에 최소 timeout 정책이 적용된다.

예시 2. provider별 차이는 HTTP service group에서 override한다

spring:
  http:
    clients:
      connect-timeout: 500ms
    serviceclient:
      keycloak:
        base-url: https://keycloak.example.com
        read-timeout: 3s
      payment:
        base-url: https://payment.example.com
        read-timeout: 5s

좋은 이유:

  • 공통 기본값과 provider별 차이가 함께 보인다
  • Spring Boot가 제공하는 group-level connect/read timeout 구조와 맞는다.

예시 3. RestClient는 주입된 builder를 사용한다

@Service
class KeycloakTokenClient {

    private final RestClient restClient;

    KeycloakTokenClient(RestClient.Builder builder, KeycloakProperties properties) {
        this.restClient = builder
                .baseUrl(properties.baseUrl())
                .build();
    }
}

좋은 이유:

  • Boot auto-configuration과 공통 timeout/customizer를 따른다
  • RestClient.create()로 공통 구성을 우회하지 않는다.

예시 4. WebClient는 Reactor Netty timeout을 명시적으로 구성할 수 있다

@Bean
WebClient paymentWebClient(WebClient.Builder builder) {
    HttpClient httpClient = HttpClient.create()
            .responseTimeout(Duration.ofSeconds(3))
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000);

    return builder
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .baseUrl("https://payment.example.com")
            .build();
}

좋은 이유:

  • connect timeout과 response timeout을 분리한다
  • Reactor Netty의 구체 timeout 지점을 활용한다.

예시 5. timeout 값은 operation별로 명시적 override만 허용한다

Mono<ResponseDto> callLongRunningOperation(RequestDto request) {
    return webClient.post()
            .uri("/reports")
            .bodyValue(request)
            .retrieve()
            .bodyToMono(ResponseDto.class)
            .timeout(Duration.ofSeconds(8));
}

좋은 이유:

  • “이 operation만 더 길다”는 의도가 코드에 드러난다
  • 기본값과 다른 이유를 문서화하기 쉽다

주의:

  • reactive 전체 timeout()은 최후 수단에 가깝고, 가능하면 client-specific timeout이 더 우선이다.

나쁜 예시

예시 1. 외부 호출에 timeout이 없다

@Service
class BadExternalClient {

    private final RestClient restClient = RestClient.create("https://example.com");
}

나쁜 이유:

  • 공통 timeout/customizer/관측 구성을 우회한다
  • 무제한 또는 의도 불명확한 대기에 빠질 수 있다.

예시 2. 너무 낮은 timeout을 근거 없이 하드코딩한다

webClient.get()
        .uri("/token")
        .retrieve()
        .bodyToMono(TokenResponse.class)
        .timeout(Duration.ofMillis(20));

나쁜 이유:

  • TLS handshake, 새 연결, DNS 비용을 고려하지 않은 값일 수 있다
  • 배포 직후/콜드 커넥션에서 false timeout을 유발하기 쉽다.

예시 3. timeout 값을 서비스 전체에 하나의 숫자로 강제한다

external:
  timeout-ms: 1000

나쁜 이유:

  • provider별 latency와 business 중요도가 다를 수 있다
  • connect/read/response 구분도 사라진다
  • operation별 차이를 담기 어렵다

예시 4. timeout 이후 side effect API를 무심코 재시도한다

try {
    paymentClient.capture(request);
} catch (TimeoutException ex) {
    paymentClient.capture(request);
}

나쁜 이유:

  • timeout이 side effect 미발생을 보장하지 않는다
  • idempotency 검토 없이 중복 실행 위험이 생긴다.