Circuit Breaker는 죽은 의존성에 헛발질해 호출자 자원이 고갈되는 걸 막는 패턴이다
요약
- 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면 도입)
- 네트워크 너머의 의존성인가? (외부 API, 다른 서비스, DB/캐시)
- 자주 반복 호출되는가? (집계할 트래픽이 있어야 의미가 있다)
- 그게 죽었을 때 내 서비스도 같이 느려지거나 멈추나?
→ 하나라도 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(=느려서 끊김)을 차단 대상에 넣는 게 핵심인데, 느린 호출이야말로 자원 고갈의 주범이기 때문이다.
관련 노트
- origin별 connection pool은 HTTP 클라이언트의 Bulkhead 패턴 구현이다
- 느린 connection pool circulation으로 인한 3가지 실패 시나리오
- connection pool 고갈 문제를 layered defence를 이용해 해결할 수 있다.
참고
- https://martinfowler.com/bliki/CircuitBreaker.html
- 책: 『Release It!』 — Michael T. Nygard (Circuit Breaker 패턴 원전)
- https://github.com/connor4312/cockatiel
- https://resilience4j.readme.io/docs/circuitbreaker