에디블로그
Engineer's Field Notes

AI 자동화로 매일
한 편씩 쓰는
엔지니어 운영 노트

Claude Code · 자동화 파이프라인 · 사고 회고까지. 잘 굴러간 기록 + 깨진 흔적도 같이 남깁니다.

사람이 할 수 있는 일은,
AI도 할 수 있어야 합니다.
매일 한 편 쓰면서 검증 중.
— 이번 주 가장 많이 읽힌 글 TOP 3
백엔드/분산 & 운영

[분산시스템] 서킷 브레이커 패턴: Resilience4j 설정, fallback 설계, 연쇄 장애 차단

반응형
[분산시스템] 서킷 브레이커 패턴: Resilience4j 설정, fallback 설계, 연쇄 장애 차단

[분산시스템] 서킷 브레이커 패턴: Resilience4j 설정, fallback 설계, 연쇄 장애 차단

외부 결제사가 느려졌을 뿐인데 내 서비스 전체가 죽는 일이 있어요. 결제와 무관한 상품 조회까지요. 장애가 호출 그래프를 타고 번지는 연쇄 장애인데, 그걸 끊는 차단기가 서킷 브레이커예요. 이름 그대로 두꺼비집이에요 — 누전이 감지되면 회로를 내려서 집 전체가 타는 걸 막는 거죠.

연쇄 장애가 번지는 구조에서 시작해, 서킷의 3상태(CLOSED·OPEN·HALF_OPEN), Resilience4j 실전 설정, 그리고 도입의 진짜 본체인 fallback 설계까지 짚어요.

01. 연쇄 장애, 남의 장애가 내 장애가 되는 길

연쇄 장애 전파 경로. 외부 결제사의 응답 지연으로 내 호출 스레드들이 묶이고 스레드 풀과 커넥션이 고갈되어 결제와 무관한 요청까지 처리하지 못하며 서비스 전체가 마비된다

경로는 늘 같아요. 의존 서비스가 느려짐 → 내 호출 스레드가 타임아웃까지 묶임 → 그런 스레드가 쌓여 풀 고갈 → 무관한 요청까지 처리 불가. OSIV의 커넥션 고갈이나 풀 말림과 정확히 같은 역학이에요 — 자원이 "일"이 아니라 "대기"에 소모되는 거죠.

여기서 통찰 하나 — 상대가 이미 죽었다면, 계속 부르는 것 자체가 자해예요. 어차피 실패할 호출에 스레드와 시간을 태우고 회복 중인 상대에게 부하까지 더해요. "빨리 포기하고, 잠시 안 부르기"가 양쪽 모두를 살려요. 그걸 자동화한 게 서킷 브레이커예요.

02. 3상태, 차단·탐색·복구

서킷 브레이커 상태머신. CLOSED에서 실패율이나 느린 호출 비율이 임계를 넘으면 OPEN으로 전환되어 호출을 즉시 실패시키고, wait duration이 지나면 HALF_OPEN에서 시험 호출을 N건만 통과시켜 성공하면 CLOSED로 복귀하고 실패하면 OPEN으로 회귀한다
  • CLOSED(정상) — 호출을 통과시키며 최근 호출들의 실패율을 집계해요.
  • OPEN(차단) — 실패율이 임계(예: 50%)를 넘으면 회로를 열어요. 이후 호출은 상대를 부르지도 않고 즉시 실패해요. 내 스레드는 0ms에 풀려나고 상대는 부하 없이 회복할 시간을 벌어요.
  • HALF_OPEN(탐색) — 대기 시간이 지난 뒤 기본 설정에선 자동 전이가 아니라, 그 다음 호출이 들어올 때 HALF_OPEN으로 바뀌며 소수의 시험 호출만 통과시켜요(자동 전이는 옵션). 성공하면 CLOSED 복귀, 실패하면 다시 OPEN. 복구를 자동으로 감지하는 장치예요.

중요한 디테일 — 느린 호출도 실패로 칠 수 있어요. 진짜 무서운 건 에러보다 "30초 걸리는 성공"이거든요(스레드를 제일 오래 묶으니까). Resilience4j는 slow call 비율로도 회로를 열 수 있어요.

03. Resilience4j 실전 설정

resilience4j:
  circuitbreaker:
    instances:
      paymentApi:
        sliding-window-size: 50              # 최근 50회 기준으로 집계
        minimum-number-of-calls: 20          # 최소 20회는 모여야 판단 (오픈 오판 방지)
        failure-rate-threshold: 50           # 실패율 50% 넘으면 OPEN
        slow-call-duration-threshold: 3s     # 3초 넘으면 "느린 호출"
        slow-call-rate-threshold: 80         # 느린 호출 80% 넘어도 OPEN
        wait-duration-in-open-state: 10s     # 10초 차단 후 HALF_OPEN
        permitted-number-of-calls-in-half-open-state: 5
@CircuitBreaker(name = "paymentApi", fallbackMethod = "payFallback")
public PaymentResult pay(PaymentRequest req) {
    return paymentClient.approve(req);
}

private PaymentResult payFallback(PaymentRequest req, Throwable t) {
    return PaymentResult.unavailable("결제가 지연되고 있어요. 잠시 후 다시 시도해주세요.");
}
주의 — @CircuitBreaker도 프록시 기반이에요. 자기호출에서 조용히 무시되는 그 규칙이 여기도 적용돼요. 그리고 의존성별로 인스턴스를 분리해요. 결제사와 추천 API가 한 서킷을 쓰면, 추천 장애가 결제까지 차단하는 황당한 일이 생겨요. 이름(name)이 곧 격리 단위예요.

04. 진짜 일은 fallback 설계예요

서킷 브레이커 fallback 세 갈래. 캐시된 값이나 기본값으로 대체하거나, 해당 기능만 빼고 동작하는 기능 축소를 하거나, 대체 불가한 기능은 빠르고 명확한 실패 안내를 준다

서킷이 열렸을 때 사용자에게 뭘 줄 거냐 — 이게 도입의 본체예요. 라이브러리 설정은 30분이면 끝나지만 fallback은 기능마다 비즈니스 판단이 필요해요.

  • 대체 값 — 개인화 추천이 죽으면 인기 상품 목록을, 환율 조회가 죽으면 최근 캐시값을 줘요. 사용자는 차이를 거의 못 느껴요.
  • 기능 축소(graceful degradation) — 리뷰 서비스가 죽으면 리뷰 영역만 숨기고 상품 페이지는 정상 서빙해요. "전부 아니면 전무"를 거부하는 거예요.
  • 빠른 실패 안내 — 결제처럼 대체가 불가능한 기능은, 30초 멈춤 끝의 타임아웃보다 즉시 "잠시 후 재시도해주세요"가 압도적으로 나은 경험이에요.

기능 목록을 놓고 "이 의존성이 죽으면?"을 하나씩 답해보는 것 — 그 표가 곧 fallback 설계서예요. 답이 안 나오는 기능(결제)은 그 자체로 "이 의존성은 이중화가 필요하다"는 발견이고요.

05. 운영에서 챙길 것

  • 상태 전이 알림 — 서킷이 OPEN 되는 순간이 곧 장애 감지예요. 상태 변화 이벤트를 모니터링·알림에 연결하면 의존성 장애를 사용자보다 먼저 알아요.
  • 타임아웃과 한 세트 — 서킷은 "실패를 집계"하는 장치라, 호출 자체의 타임아웃이 없으면 느린 호출이 실패로 안 잡혀요. 타임아웃(TimeLimiter)을 짧게 거는 게 선행이에요 — 이건 다음 편 주제예요.
  • 벌크헤드 — 의존성별로 스레드·동시 호출 수를 격리하는 패턴이에요. 서킷이 "시간축 격리"라면 벌크헤드는 "자원축 격리" — 같이 쓰면 연쇄 차단이 더 단단해져요.

06. 자주 만나는 문제

서킷이 너무 자주 열려요 (오탐)

minimum-number-of-calls가 작아 새벽 저트래픽에 한두 건 실패로 열리는 경우가 단골이에요. 최소 호출 수를 올리고 비즈니스 예외(잔액 부족 등)는 실패 집계에서 제외(ignore-exceptions)해요 — 그건 상대의 장애가 아니니까요.

서킷이 열렸는데 아무도 몰랐어요

fallback이 너무 매끄러워서 조용히 degrade만 된 거예요. 상태 전이 알림과 fallback 발동률 메트릭을 걸어요.

붙였는데 동작을 안 해요

자기호출(프록시)이거나 인스턴스 이름 불일치예요. actuator의 circuitbreakers 엔드포인트로 상태를 직접 확인해요.

이게 죽으면 뭘 보여줄까

서킷 브레이커는 "죽은 상대를 계속 부르는 자해"를 끊는 자동 차단기예요. 실패율과 느린 호출 비율로 OPEN되어 즉시 실패로 내 자원을 지키고 상대에게 회복 시간을 줘요. HALF_OPEN으로 복구를 탐지해요. 그런데 설정 자체는 30분이면 끝나고 진짜 무게는 의존성별 격리와 fallback 설계에 실려요.

그래서 도입의 본체는 라이브러리가 아니라 이 질문이에요 — "이 의존성이 죽으면 사용자에게 뭘 보여줄까?" 기능 목록을 놓고 하나씩 답해보면 그 표가 곧 fallback 설계서가 돼요.

서킷의 짝꿍인 타임아웃과 재시도 전략(재시도 폭풍, 백오프+지터)을 시리즈 마지막인 다음 글에서 다뤄요.

출처: Resilience4j — CircuitBreaker · Martin Fowler — CircuitBreaker

반응형

📚 같이 보면 좋은

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."