- Published on
Spring Boot 3 Feign 타임아웃·재시도 함정 9가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 간 호출이 늘어날수록 장애의 대부분은 “실패”가 아니라 “느려짐”에서 시작합니다. 특히 Spring Boot 3 + OpenFeign 조합은 설정 지점이 여러 군데로 분산되어 있고(프로퍼티, 빈, HTTP 클라이언트 구현체), 타임아웃과 재시도가 서로 곱해지면서 지연이 기하급수적으로 커지는 함정이 자주 발생합니다.
이 글은 Spring Boot 3 환경에서 Feign 타임아웃·재시도를 다룰 때 실무에서 자주 밟는 함정 9가지를 “왜 그런지/어떻게 확인하는지/어떻게 고치는지” 중심으로 정리합니다.
> 네트워크 계층의 timeout 원인 분석이 필요하다면 Kubernetes apiserver i/o timeout 원인과 해결도 함께 참고하면, 애플리케이션 타임아웃과 인프라 타임아웃을 구분하는 데 도움이 됩니다.
전제: Spring Boot 3에서 Feign 타임아웃·재시도는 어디서 결정되나
Spring Cloud OpenFeign을 사용한다고 가정합니다. 타임아웃·재시도는 보통 아래 축의 조합으로 결정됩니다.
- Feign 레벨:
Request.Options(connectTimeout, readTimeout) - HTTP 클라이언트 레벨: JDK
HttpURLConnection/ Apache HttpClient 5 / OkHttp 등 구현체별 timeout - 재시도 레벨: Feign
Retryer, 혹은 Resilience4jRetry(권장) - 호출자 레벨: 호출 스레드(서블릿/리액티브), 서킷브레이커/벌크헤드/타임리미터
이제 함정들을 하나씩 보겠습니다.
1) 함정: 타임아웃을 설정했는데 전혀 안 먹는다(클라이언트 구현체 불일치)
Feign은 내부적으로 사용할 HTTP 클라이언트를 선택합니다. 프로젝트 의존성에 따라 Apache HC5나 OkHttp로 바뀌기도 하고, 아무것도 없으면 기본(URLConnection)로 갑니다. 이때 “Feign 옵션 타임아웃”과 “클라이언트 타임아웃”이 다르게 적용되거나, 특정 구현체에서만 먹는 설정을 넣어놓고 효과가 없다고 착각합니다.
체크 포인트
- 애플리케이션 부팅 로그/빈 구성을 통해 실제 사용 중인
Client구현체를 확인 feign.httpclient.enabled,feign.okhttp.enabled같은 토글을 쓰는 경우 버전별로 동작이 달라질 수 있음
권장 해결
- 한 가지 구현체로 표준화(예: Apache HC5)하고, timeout을 한 곳에서 관리
spring:
cloud:
openfeign:
httpclient:
hc5:
enabled: true
client:
config:
default:
connectTimeout: 1000
readTimeout: 2000
2) 함정: connectTimeout/readTimeout만 설정하면 “전체 요청 시간”이 제한된다고 믿는다
connectTimeout은 “TCP 연결 수립”까지, readTimeout은 “연결 이후 소켓 읽기”에 대한 타임아웃입니다. 즉,
- DNS 지연
- TLS handshake 지연
- 커넥션 풀에서 커넥션을 기다리는 시간
같은 요소는 별도 설정이 필요하거나 구현체별로 다르게 취급됩니다.
권장 해결
- Apache HC5를 쓴다면 “커넥션 풀 대기 시간(=connection request timeout)”도 함께 제한
- 전체 요청 시간 상한이 필요하면 Feign 단독보다 **Resilience4j TimeLimiter(또는 호출자 레벨 timeout)**를 병행
(구현체별 설정은 프로젝트 표준에 맞춰 문서화하세요.)
3) 함정: 재시도 3번이면 최대 3초 정도겠지? (타임아웃 × 재시도 = 최악의 곱)
가장 흔한 사고는 다음 계산을 안 하는 겁니다.
readTimeout = 2sRetryer가 3회 재시도
이면 최악의 경우 한 요청이 2s × (1 + 3) = 8s까지 늘어납니다(백오프까지 있으면 더 큼). 여기에 서버 스레드가 묶이면 연쇄적으로 장애가 커집니다.
권장 해결
- “요청 단위 예산(time budget)”을 정하고, 타임아웃/재시도/백오프를 그 예산 안에 넣기
- 재시도는 idempotent 요청에만 제한
- 429/503 등 “회복 가능” 신호에만 조건부 재시도
재시도·백오프 설계 자체는 API 호출에서도 동일합니다. 설계 원칙은 OpenAI API 429 Rate Limit 재시도·백오프 설계에서 정리한 패턴을 그대로 가져와도 좋습니다.
4) 함정: Feign 기본 Retryer를 모르고 “원치 않는 재시도”가 발생한다
Feign에는 Retryer.Default가 있고, 구성에 따라 예외(특히 RetryableException)에서 재시도가 걸릴 수 있습니다. 실무에서는 “재시도 안 한다고 생각했는데 재시도해서 더 느려졌다”가 자주 나옵니다.
권장 해결
- Feign 레벨 재시도를 명시적으로 끄고(또는 최소화) Resilience4j로 통합하는 전략을 권장
import feign.Retryer;
import org.springframework.context.annotation.Bean;
public class FeignNoRetryConfig {
@Bean
public Retryer retryer() {
return Retryer.NEVER_RETRY;
}
}
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(
name = "inventory",
url = "${inventory.url}",
configuration = FeignNoRetryConfig.class
)
interface InventoryClient {
@GetMapping("/v1/items")
String items();
}
5) 함정: POST도 재시도해버려서 중복 처리(결제/주문) 터진다
재시도는 “네트워크 실패”를 “요청이 서버에 도달했는지”와 분리해서 생각해야 합니다. 타임아웃이 났다고 해서 서버가 처리를 안 했다는 뜻이 아닙니다.
- POST 재시도 → 중복 주문/중복 결제
- PUT/PATCH 재시도 → 마지막 쓰기 덮어쓰기, 버전 충돌
권장 해결
- 멱등성 키(Idempotency-Key) 도입
- 서버가 멱등 처리할 수 없다면, 클라이언트 재시도 금지
- 재시도를 하더라도
GET, 안전한 조회성 API에만 제한
6) 함정: 4xx/5xx를 “재시도 대상”으로 잘못 분류한다
- 400/401/403/404는 대부분 재시도해봐야 소용이 없습니다(오히려 트래픽만 증가).
- 429/503/504는 재시도 후보가 될 수 있지만, 백오프 + 지터가 없으면 더 악화됩니다.
권장 해결
- 상태 코드별 정책을 분리하고, 429는
Retry-After를 존중 - 재시도는 “조건부”로: 특정 예외, 특정 상태 코드, 특정 메서드에만
7) 함정: 타임아웃은 맞는데도 요청이 계속 쌓인다(커넥션 풀/스레드 고갈)
Feign 타임아웃을 짧게 잡아도, 커넥션 풀/스레드 풀이 고갈되면 다음 현상이 나옵니다.
- 커넥션 풀에서 커넥션을 못 구해 대기
- 대기 중인 요청이 늘어나면서 지연 폭발
- 결국 타임아웃이 “연쇄적으로” 발생
이는 애플리케이션 레벨 문제처럼 보이지만, 쿠버네티스 환경에서는 노드/네트워크/프록시까지 함께 영향을 줍니다. 특정 상황에서 i/o timeout이 커지는 패턴은 앞서 언급한 글(Kubernetes apiserver i/o timeout 원인과 해결)의 진단 접근을 응용해도 좋습니다.
권장 해결
- HTTP 클라이언트 커넥션 풀 사이즈/대기시간 제한 설정
- 대량 동시 호출이 가능한 구간에 Bulkhead(격리) 적용
- 호출량이 폭증할 수 있는 엔드포인트에 rate limit/큐잉 도입
8) 함정: 로그/메트릭이 없어 “어디서 시간이 샜는지” 모른다
타임아웃이 나면 대부분 로그에는 Read timed out 정도만 남습니다. 하지만 실제로는
- DNS
- connect
- TLS handshake
- connection pool wait
- server processing
중 어디에서 지연이 발생했는지 알아야 해결이 됩니다.
권장 해결
- Micrometer + 분산추적(예: OpenTelemetry)로 외부 호출 스팬을 남기기
- Feign 로깅은 개발/장애 상황에서만 제한적으로 켜기(PII/비용 주의)
logging:
level:
# 운영에서는 필요한 클라이언트만 선별 권장
com.example.client.InventoryClient: DEBUG
feign:
client:
config:
default:
loggerLevel: basic
9) 함정: 타임아웃/재시도 정책이 “환경별로” 달라져 재현이 안 된다(설정 우선순위 혼선)
Spring Boot 3에서 설정은 application.yml, 프로파일, Config Server, 환경변수, 쿠버네티스 ConfigMap 등 여러 레이어로 덮어씌워집니다. Feign도 default와 clientName별 설정이 공존합니다.
흔한 사례
- 로컬은
default만 적용, 운영은 특정 클라이언트 설정이 존재 - Helm values로 환경변수가 주입되며 readTimeout이 의도치 않게 커짐/작아짐
권장 해결
default는 “전사 표준”만 두고, 예외는 클라이언트별로 명시- 운영에서 실제 적용 값을
/actuator/env,/actuator/configprops로 확인(민감정보 마스킹 주의)
management:
endpoints:
web:
exposure:
include: "health,info,env,configprops"
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 500
readTimeout: 1500
inventory:
readTimeout: 800
실전 체크리스트: “느려짐”을 막는 최소 기준
- 타임아웃을
connect/read만이 아니라 커넥션 풀 대기/전체 예산 관점으로 설계했는가? - 재시도는 Feign에 숨겨두지 말고, **정책 엔진(Resilience4j 등)**으로 통합했는가?
- 재시도 대상은 멱등 요청/회복 가능한 상태 코드로 제한했는가?
timeout × retry최악의 케이스를 계산해, 서버 스레드/큐 용량과 함께 검증했는가?- 커넥션 풀/스레드 풀 고갈 시나리오(부하 테스트)를 돌려봤는가?
- 추적/메트릭으로 지연 구간을 분해할 수 있는가?
마무리
Feign 자체는 “편한 HTTP 클라이언트”지만, 타임아웃·재시도는 편의 기능이 아니라 장애 증폭기가 될 수 있습니다. 핵심은 (1) 실제 사용 중인 HTTP 구현체를 고정하고, (2) 시간 예산을 기준으로 timeout/retry/backoff를 설계하며, (3) 멱등성/상태코드 기반으로 재시도를 제한하는 것입니다.
재시도 정책을 더 정교하게 다듬고 싶다면, 백오프·지터·429 처리 원칙을 다룬 OpenAI API 429 Rate Limit 재시도·백오프 설계도 함께 읽어보면 Feign 호출에도 그대로 적용할 수 있습니다.