Java 아키텍처·Spring 용어 사전
회복탄력성Circuit Breaker · Resilience4j · 폴백

서킷 브레이커

실패율이 임계치를 넘으면 호출을 아예 차단하는 장치. 죽은 의존성을 계속 두드리지 않게 한다.

실패율이 임계치를 넘으면 아예 호출하지 않고 즉시 실패시키는 장치. 장애 전파를 끊는다.

왜 필요한가

결제 서비스가 느려졌다 (응답 30초)

  • 주문 서비스의 스레드들이 거기 묶인다
  • 주문 서비스의 스레드 풀이 마른다
  • 주문과 무관한 API 까지 멈춘다
  • 그 앞단도 멈춘다 (연쇄 장애)

한 서비스의 장애가 시스템 전체로 번진다

어차피 실패할 호출을 계속 시도하는 것이 문제다. 차라리 즉시 실패시키고 폴백을 주는 게 낫다.

세 가지 상태

CLOSED (정상)
  호출을 통과시키며 실패율을 집계한다
     ↓ 실패율이 임계치(예: 50%)를 넘으면
OPEN (차단)
  호출하지 않고 즉시 실패시킨다 → 폴백 실행
     ↓ 일정 시간(wait duration) 후
HALF_OPEN (탐색)
  소수의 호출만 통과시켜 회복됐는지 본다
     ↓ 성공하면 CLOSED · 실패하면 다시 OPEN
@CircuitBreaker(name = "payment", fallbackMethod = "fallback")
@TimeLimiter(name = "payment")
PaymentResult pay(Order o) { return paymentClient.pay(o); }

PaymentResult fallback(Order o, Throwable t) {
    return PaymentResult.pending();     // 큐에 넣고 나중에 처리
}
resilience4j.circuitbreaker.instances.payment:
  slidingWindowSize: 100
  failureRateThreshold: 50          # 50% 실패하면 OPEN
  waitDurationInOpenState: 10s
  permittedNumberOfCallsInHalfOpenState: 10
  minimumNumberOfCalls: 20          # 이만큼 모여야 판단한다 (초기 오탐 방지)

minimumNumberOfCalls가 중요하다 — 없으면 첫 호출 하나가 실패했다고 회로가 열린다.

서킷 브레이커 하나로는 부족하다

  • 타임아웃 — 가장 먼저. 무한히 기다리지 않는다 (이게 없으면 나머지가 무의미)
  • 재시도 — 일시적 실패에만. 지수 백오프 + 지터
  • 서킷 브레이커 — 반복 실패하면 아예 차단
  • 벌크헤드 — 한 의존성의 폭주를 격리 (동시 호출 수 제한)
  • rate limiter — 과부하 자체를 막는다

재시도에 지터가 필요한 이유

고정 간격으로 재시도하면

  • 실패한 클라이언트들이 '동시에' 다시 몰린다 (thundering herd)
  • 회복 중인 서비스를 다시 쓰러뜨린다

지수 백오프(1s, 2s, 4s) + 무작위 지터로 시각을 흩뿌린다

데코레이터 순서가 동작을 바꾼다

Retry( CircuitBreaker( TimeLimiter( 호출 ) ) )

  • 회로가 열려도 재시도한다 (대개 원하지 않는 동작)

CircuitBreaker( Retry( TimeLimiter( 호출 ) ) )

  • 재시도까지 다 실패한 것을 '한 번의 실패' 로 집계한다 (권장)

Resilience4j의 기본 순서를 확인하지 않고 조합하면 의도와 다르게 동작한다.

면접 함정

  • "서킷 브레이커만 있으면 된다" → 타임아웃이 없으면 회로가 열리기 전에 스레드가 먼저 마른다.
  • "재시도하면 성공률이 오른다" → 상대가 과부하면 재시도가 부하를 키운다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 회복탄력성 — 재시도·타임아웃·벌크헤드·RateLimit