castle.log
BlogWorkHistory
GitHub

외부 서비스 장애 대응 전략 정리

2026년 05월 28일  8일 전
Resilience
API
Operations
외부 서비스 장애 대응 전략 정리

외부 API 지연/장애 시 서버 자원 고갈연쇄 장애를 막기 위한 방어적 설계운영 전략을 정리합니다. Java/Spring 실무 예시 포함.


1) 근본 원칙: 빠른 실패와 자원 격리

  • 빠른 실패: 타임아웃·서킷 브레이커로 지연 전파를 차단합니다.
  • 자원 격리: 벌크헤드(커넥션 풀/스레드풀/큐)를 서비스별로 분리하여 A 장애가 B,C로 전파되는 것을 차단합니다.
  • 관측 가능성: 지연·오류율을 실시간 관측하고 자동 완화(rate limit/load shedding)를 적용합니다.

2) 타임아웃 체계(필수)

  • 커넥션 타임아웃: TCP 연결 수립 한도(예: 200~500ms). 네트워크 단절/방화벽 문제에 즉시 실패.
  • 리드 타임아웃: 응답 바디 수신 한도(예: 1~3s; 비즈니스 SLO에 부합하게).
  • 풀 획득 타임아웃: 커넥션 풀에서 대기 한도를 제한(예: 100~300ms). 대기열 팽창을 방지.
  • 요청 데드라인(Deadline): 전체 체인에서 총 예산을 부여(예: 800ms). 하위 호출이 남은 예산을 상속하도록 설계.

타임아웃은 지수 백오프+재시도으로 체감 지연을 초과하지 않도록 합리화하십시오.


3) 벌크헤드(Bulkhead) — 자원 격리

  • 커넥션 풀 분리: 외부 시스템 도메인/기능별로 별도 풀(최대/대기 한도도 분리).
  • 스레드풀/큐 분리: 동기 호출을 처리하는 워커 스레드/큐를 서비스별로 분리, 큐 길이 제한으로 역압(backpressure).
  • 프로세스/호스트 수준 격리: 고가치 트래픽은 전용 노드로 라우팅(카나리/셀 기반).
# 예: Spring WebClient(reactor-netty) 커넥션 풀을 외부 서비스별로 분리
reactor.netty.pool.maxConnections: 100 # 서비스 A
reactor.netty.pool.pendingAcquireMaxCount: 50 # 대기 한도
# 서비스 B는 별도 HttpClient 인스턴스로 구성(풀이 분리됨)

4) 서킷 브레이커(Circuit Breaker) — 연쇄 장애 차단

  • 상태: Closed(정상) → 실패율/슬로 콜 비율 초과 시 Open(즉시 실패) → Half-Open에서 소수 테스트 성공 시 Closed로 복귀.
  • 효과: 장애 지속 시 빠른 실패스레드/커넥션 점유 시간을 제거하여 자체 서비스 보호.
// Resilience4j 예시
@CircuitBreaker(name = "svcA", fallbackMethod = "fallback")
public String callA(...) { ... }

@Bulkhead(name = "svcA-bulk", type = Bulkhead.Type.THREADPOOL)
@TimeLimiter(name = "svcA-tl")
public CompletableFuture<String> callAAsync(...) { ... }

임계값은 요청 수 최소 기준(sliding window)과 함께 설정하고, Slow Call Rate(예: > 1s)를 별도로 감지하십시오.


5) 재시도(Retry) — 지수 백오프 + Jitter

  • 지수 백오프(예: 100ms, 200ms, 400ms, 800ms)와 Full Jitter동시 폭주 동기화(thundering herd) 를 방지.
  • 멱등성 보장: 재시도 가능한 안전한 메서드(GET/멱등 POST)만 자동 재시도. 멱등 키(idempotency key) 사용.
  • 종단 간 데드라인 준수: 남은 시간 < 다음 시도 예상 소요면 재시도 중단.
resilience4j.retry:
  instances:
    svcA:
      maxAttempts: 3
      waitDuration: 200ms
      enableExponentialBackoff: true
      exponentialBackoffMultiplier: 2.0
      retryExceptions:
        - java.io.IOException
        - java.net.SocketTimeoutException

6) Rate Limiting / Load Shedding

  • 클라이언트 측: 토큰 버킷으로 호출률 제한, 큐 길이 제한으로 적시에 드롭(SLO 보호).
  • 서버 측: HTTP 429/503로 명시적 거절, Retry-After 힌트 제공.
  • 부하 급증 시: 우선순위 큐(핵심 경로 우선), 샘플링/축약 응답.

7) 캐시·폴백·강건한 UX

  • 읽기 캐시: 실패 시 최근 성공값(stale) 제공(stale-if-error). TTL/무효화 전략 포함.
  • 폴백 데이터: 핵심 경로를 유지하기 위해 대체 경로(로컬 규칙, 서브셋 정보, 회로 열림 시 배너 표시).
  • 요청 병합/중복 억제: 동일 키에 대한 동시 요청을 단일 외부 호출로 합치기(request collapsing).

8) 아키텍처 대안: 비동기/큐 오프로딩

  • 동기 HTTP 경로에서 핵심 UX만 동기로 처리하고, 부가 연동은 메시지 큐/작업 큐로 오프로딩(재시도/지연 시도).
  • SLA 차등: 온라인은 타임박스+폴백, 백그라운드는 재시도 무제한(보상 트랜잭션).

9) 관측과 운영(SLO 기반)

  • 지표: 오류율, 슬로 콜율, p95/99 지연, 풀 사용률, 대기열 길이, 서킷 상태.
  • 로그·추적: 코릴레이션 ID, 외부 호출 span, 오류 원인(타임아웃/커넥션/429/5xx).
  • 알림: SLO 위반 기반 경보, 자동 스케일/완화 룰 연동.
  • 카오스/장애 주입: 지연/오류 주입 테스트로 회로·재시도·폴백 동작 검증.

10) Spring/Java 구성 예시

10.1 WebClient(reactor-netty) — 타임아웃/풀/커넥션 분리

HttpClient clientA = HttpClient.create(ConnectionProvider.builder("svcA-pool")
    .maxConnections(100)
    .pendingAcquireMaxCount(50)
    .pendingAcquireTimeout(Duration.ofMillis(200))
    .build())
  .responseTimeout(Duration.ofSeconds(2))
  .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 300);

WebClient webClientA = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(clientA))
    .baseUrl("https://svc-a.example.com")
    .build();

10.2 OkHttp — 타임아웃/커넥션 풀 분리

OkHttpClient clientA = new OkHttpClient.Builder()
    .connectTimeout(300, TimeUnit.MILLISECONDS)
    .readTimeout(2, TimeUnit.SECONDS)
    .connectionPool(new ConnectionPool(50, 30, TimeUnit.SECONDS)) // A 전용 풀
    .build();

10.3 Resilience4j — Circuit/Retry/Bulkhead/Timeout

resilience4j.circuitbreaker:
  instances:
    svcA:
      slidingWindowType: COUNT_BASED
      slidingWindowSize: 50
      failureRateThreshold: 50
      slowCallRateThreshold: 50
      slowCallDurationThreshold: 1s
      minimumNumberOfCalls: 20
      waitDurationInOpenState: 10s

resilience4j.bulkhead:
  instances:
    svcA-bulk:
      maxConcurrentCalls: 30
      maxWaitDuration: 100ms

resilience4j.timelimiter:
  instances:
    svcA-tl:
      timeoutDuration: 2s

10.4 Feign + Resilience4j

@CircuitBreaker(name = "svcA", fallbackMethod = "fallback")
@Retry(name = "svcA")
@Bulkhead(name = "svcA-bulk")
public interface SvcAClient { ... }

11) 실패 모드 설계

  • 정확히 한 번 처리가 필요한 경로는 멱등 키/중복 방지 토큰으로 보강.
  • 부분 실패에 대비한 보상 트랜잭션(saga) 전략.
  • 다운스트림 정책 존중: 429/503 수신 시 재시도 간격/최대 시도 정책을 준수.

12) 체크리스트

  • 커넥션/리드/풀 획득 타임아웃이 설정되어 있는가
  • 서비스별 커넥션 풀/스레드풀이 분리되어 있는가(벌크헤드)
  • 서킷 브레이커 임계값과 Slow Call 기준이 합리적인가
  • 재시도멱등 요청에만 적용되고, 백오프+지터가 설정되어 있는가
  • Rate Limit/Load Shedding큐 길이 제한이 있는가
  • 캐시/폴백/요청 병합으로 불필요한 외부 호출을 줄였는가
  • SLO 모니터링장애 주입 테스트로 방어장치를 검증했는가
  • 온라인·오프라인 경로가 분리되어 있는가(필수만 동기 처리)

13) 요약

  • 타임아웃 + 벌크헤드 + 서킷 브레이커는 동기 외부 호출 보호의 3대장입니다.
  • 여기에 재시도(백오프+지터), Rate Limit/Load Shedding, 캐시/폴백을 더해 빠른 실패와 품질 저하 모드를 설계하십시오.
  • 운영 단계에서는 지표/추적을 기반으로 동적 튜닝장애 주입으로 회복탄력성을 상시 검증하는 것이 핵심입니다.
이전 게시글스프링 트랜잭션 AOP 동작 흐름 정리
다음 게시글Rate Limiting 정리