Zettelkasten

Circuit Breaker는 죽은 의존성에 헛발질해 호출자 자원이 고갈되는 걸 막는 패턴이다

·수정 1

요약

  • Circuit Breaker의 진짜 목적은 "죽은 상대 살려주기"가 아니라 호출자(나) 보호다. 느린 실패 호출이 스레드/커넥션을 점유해 연쇄 장애(cascading failure)를 일으키는 걸 막는다.
  • CLOSED → OPEN → HALF_OPEN 3상태로, OPEN이면 즉시 실패(fail-fast) 시켜 자원을 지킨다.
  • 직접 구현하기 까다로워 라이브러리(Node: cockatiel/opossum, Java: Resilience4j, .NET: Polly)나 서비스 메시(Istio 등)에 위임하는 게 정석이다.

본문

왜 멈추는가 — 핵심은 "느림이 에러보다 무섭다"

죽은 서비스는 보통 즉시 에러를 주지 않고, 응답을 안 준 채 질질 끌다가 타임아웃(예: 30초)난다. 이게 호출자를 죽인다.

내 서버가 동시 처리 가능한 요청이 100개(스레드/커넥션 100개)라고 하면:

결제 서비스 다운 → 응답을 30초씩 끌다 타임아웃

요청 1~100  → 결제 호출 → 각각 30초 대기 → 스레드 100개 전부 점유
요청 101    → 처리할 스레드 없음 → ❌ 결제와 무관한 기능까지 전부 멈춤

결제 하나 죽었는데 상품 조회까지 막히는 연쇄 장애가 일어난다. 죽어가는 호출들이 자원을 다 빨아먹기 때문이다.

Circuit Breaker가 OPEN이면 호출을 시도조차 않고 즉시 실패시킨다(0.001초). 스레드가 막히지 않으니 나머지 기능은 멀쩡하다. 이걸 fail-fast라 한다.

멈추는 이유 우선순위

이유 누구를 위해 중요도
느린 호출이 호출자 자원을 고갈시키는 것 차단 (fail-fast) 나(호출자) 가장 큼
연쇄 장애로 전체 시스템이 무너지는 것 방지 시스템 전체
죽은 서비스가 회복할 틈 주기 상대 서비스 보조
헛된 재시도/비용 낭비 방지 효율 보조

"상대 살려주기"는 흔한 오해다. 부차적 효과일 뿐, 1차 목적은 호출자 자기 방어다.

3가지 상태

CLOSED(정상) ──에러율 임계 초과──> OPEN(차단)
   ↑                                  │
   │                            일정 시간 경과
   └──성공── HALF_OPEN(간보기) <───────┘
            (한 번만 찔러봄)
  • CLOSED: 평소. 요청 통과. 실패율을 집계한다.
  • OPEN: 차단. 요청을 보내지 않고 즉시 실패. 일정 시간(보통 지수 백오프) 후 HALF_OPEN으로.
  • HALF_OPEN: 시험 호출 1회. 성공하면 CLOSED 복귀, 실패하면 다시 OPEN.

언제 쓰는가 — 판단 기준 3가지 (모두 yes면 도입)

  1. 네트워크 너머의 의존성인가? (외부 API, 다른 서비스, DB/캐시)
  2. 자주 반복 호출되는가? (집계할 트래픽이 있어야 의미가 있다)
  3. 그게 죽었을 때 내 서비스도 같이 느려지거나 멈추나?

→ 하나라도 no면 보통 try/catch + (필요시) retry로 충분. 1회성 배치, 순수 인메모리 로직, 실패 무시해도 되는 fire-and-forget에는 불필요하다.

retry vs circuit breaker — 역할이 정반대

retry           = "한 번 삐끗했네, 다시 해보자"  → 낙관적 (일시적 실패)
circuit breaker = "계속 안 되네, 그만 두들기자"  → 비관적 (지속적 장애)

보통 함께 쓴다: 몇 번 retry → 그래도 반복 실패하면 breaker가 OPEN. 여기에 fallback(대체 응답/대체 의존성)을 더해 retry → fallback → breaker 3종 세트로 외부 의존성을 다루는 경우가 많다.

직접 만들지 마라 — 라이브러리/인프라에 위임

동시성·타이머·상태전환 race condition 때문에 정확히 구현하기 까다롭다. 검증된 도구를 쓴다.

생태계 라이브러리
Node.js cockatiel, opossum
Java Resilience4j (구: Hystrix)
.NET Polly
Go sony/gobreaker

마이크로서비스 규모가 크면 서비스 메시(Istio/Linkerd + Envoy 사이드카)가 서비스 간 트래픽을 가로채 circuit breaking/retry/timeout을 처리한다. 이 경우 앱 코드는 한 줄도 안 쓰고 정책을 YAML로 선언하며, 언어와 무관하게 정책이 통일된다. 반대로 외부 SaaS(LLM, 결제 PG 등)는 메시 밖이라 앱 코드 라이브러리로 직접 거는 게 자연스럽다.

에러 선별이 중요하다

모든 에러를 차단 대상으로 삼으면 안 된다. 재시도하면 나아질 에러(429 과부하, 5xx 서버 에러, abort/timeout)만 집계하고, 클라이언트 잘못(4xx 대부분)이나 비즈니스 예외는 제외한다. 특히 abort/timeout(=느려서 끊김)을 차단 대상에 넣는 게 핵심인데, 느린 호출이야말로 자원 고갈의 주범이기 때문이다.

관련 노트

참고