아무튼, 쓰기

Resilience4j 딮다이브 본문

스프링

Resilience4j 딮다이브

순원이 2026. 2. 8. 01:55
Version Note: 이 문서는 Resilience4j v2.x (v2.3 이상) 및 v3.0.0 소스 코드를 기준으로 작성되었습니다. (v1.x 버전과는 설정 변수명 등이 다를 수 있습니다.)

본 글은 Resilience4j 라이브러리의 내부 코드를 분석하여, 각 모듈이 실제로 어떤 알고리즘과 데이터 구조를 사용하여 동작하는지 설명하고, 주요 설정의 기본값 과 동작 예시 레퍼런스를 제공합니다.


1. 알아보기 전에 왜 Resilience4j여야 하는가?

단순한 사용법을 넘어, 설계자의 시선에서 Resilience4j를 깊이 있게 분석해 봅니다.

1-1. 탄생 배경과 문맥

왜 Netflix Hystrix는 역사의 뒤안길로 사라지고 Resilience4j가 표준이 되었는가?

Netflix Hystrix는 Java 6 시절에 만들어져 객체 지향적인 강결합 설계와 RxJava(무거운 의존성)를 필수로 요구했습니다. 하지만 Java 8의 등장으로 Lambda와 함수형 프로그래밍이 주류가 되면서, 이에 맞춰 등장한 Resilience4j는 함수형 디자인을 채택했습니다.

덕분에 불필요한 의존성을 제거하여 가볍고, 서킷/리트라이 등 필요한 모듈만 골라 쓸 수 있는 것이 특징입니다. 결과적으로 분산 시스템의 연쇄 장애 를 방지하고, Fail Fast 를 통해 시스템의 전체적인 회복 탄력성을 확보하는 것을 목표로 합니다.

1-2. 내부 동작과 추상화

이 라이브러리는 어떻게 Lock 없이 스레드 안전성을 보장하는가?

Resilience4j는 고성능을 위해 synchronized 키워드를 거의 사용하지 않습니다. 대신 상태 변경이나 카운팅에 AtomicReference, LongAdder와 같은 CPU 수준의 원자적 연산(CAS) 을 사용하여 병목(Contention)을 제거했습니다.

메모리 효율성 측면에서도, 초기 버전(v1)에서는 BitSet을 사용했으나 v2부터는 정밀한 시간 측정을 위해 Object Ring Buffer (Measurement[]) 방식을 채택하여 최적화했습니다. (자세한 내용은 CircuitBreaker 섹션 참조)

1-3. 트레이드오프

Resilience4j를 도입함으로써 우리가 치러야 할 비용은 무엇인가?
  • 지연 시간
    • : 아무리 가볍다 해도, 모든 요청에 대해 CircuitBreaker.acquirePermission() 같은 검사 로직이 추가됩니다. (Nano-second 단위라 무시할 만하지만, 초고빈도 trading 시스템에서는 고려 대상입니다.)
  • 복잡도
    • 설정 폭탄: 타임아웃, 임계치, 윈도우 크기 등 튜닝해야 할 파라미터가 매우 많습니다. 잘못 설정하면 멀쩡한 요청도 차단해버리는 False Positive가 발생합니다.
    • 디버깅의 어려움: AOP로 감싸져 있어 스택 트레이스가 복잡해지고, "왜 차단되었는지" 즉각적으로 알기 어려울 때가 있습니다. (그래서 writableStackTraceEnabled=false 같은 옵션도 존재함).

1-4. 한계와 임계점

이 서킷브레이커 자체가 장애 포인트가 될 수 있는가?
  • 메모리 한계
    • slidingWindowSize를 너무 크게 잡으면(예: 1,000,000), 각 요청마다 비트 연산 및 집계 비용이 커져 CPU 오버헤드가 발생할 수 있습니다.
  • 분산 환경의 한계와 해결책
    • 한계: Resilience4j는 단일 JVM(인스턴스) 내부에서만 동작합니다. (서버 10대 = 서킷 10개).
    • 해결책 1 (Client-side Load Balancing): Spring Cloud LoadBalancer와 결합하여, 인스턴스별로 서킷을 격리해서 관리합니다. (A서버용 서킷 OPEN -> B서버로 라우팅).
    • 해결책 2 (Global Rate Limiting): 전체 트래픽 제어가 필요하다면 Resilience4j 대신 Redis + Bucket4j 조합이나 API Gateway (Kong, Spring Cloud Gateway) 의 RateLimiter를 써야 합니다.
    • Sticky Session: 스티키 세션을 쓴다면 인스턴스별 서킷 동작이 유의미하지만, 라운드 로빈 환경에서는 "운 나쁜" 인스턴스만 서킷이 열리는 불균형이 발생할 수 있습니다.

1-5. 대안과 타당성

꼭 Resilience4j여야 하는가? Service Mesh는?

대안 기술 장점 단점 적합한 상황

Spring Retry매우 단순함. 어노테이션 하나로 끝.서킷 브레이커(상태 관리) 기능이 없음. 무지성 재시도만 가능.단순 API 재시도용.
Istio (Service Mesh)인프라 레벨에서 처리. 언어 종속성 없음(Polyglot). 중앙 제어 가능.구축 및 운영 복잡도가 매우 높음(Kubernetes 필수). Fallback 로직 구현이 까다로움.대규모 MSA, 인프라 팀이 별도로 있는 경우.
Sentinel (Alibaba)유량 제어(Flow Control)에 특화됨. 대시보드 강력함.중국어 문서 비중이 높음. Spring Cloud Alibaba 의존성.트래픽 세이핑이 주 목적일 때.

결론

  • 비즈니스 로직(Fallback 처리, 데이터 기본값 반환 등)과 밀접하게 연동되어야 한다면 Resilience4j가 압도적으로 유리합니다.
  • 단순히 "죽은 서버로 요청 안 보내기"가 목적이라면 Istio 같은 인프라 솔루션이 더 깔끔할 수 있습니다.

2. Circuit Breaker

Circuit Breaker는 서비스의 가용성을 보호하기 위해 유한 상태 기계로 구현되어 있습니다.

2-1. 상태 관리

핵심 클래스인 CircuitBreakerStateMachine은 3가지 주요 상태(CLOSED, OPEN, HALF_OPEN)와 2가지 특수 상태(DISABLED, FORCED_OPEN)를 가집니다.

  • CLOSED: 모든 요청을 통과시킴. 실패율이나 느린 응답률을 모니터링.
  • OPEN: 모든 요청을 즉시 차단하고 CallNotPermittedException을 발생시킴. 설정된 waitDurationInOpenState만큼 대기.
  • HALF_OPEN: 제한된 수의 요청(permittedNumberOfCallsInHalfOpenState)만 허용하여 백엔드 서버가 복구되었는지 확인.
  • DISABLED: 서킷 브레이커를 강제로 끕니다. 모든 요청을 허용하며, 통계도 수집하지 않습니다. (장애 시 긴급 조치 혹은 부하 테스트용).
  • FORCED_OPEN: 서킷 브레이커를 강제로 엽니다. 모든 요청을 차단하며, 타임아웃이 지나도 자동으로 풀리지 않습니다. (완전한 점검 필요 시).

2-2. 통계 수집: Sliding Window (핵심 자료구조)

통계 수집은 Sliding Window 알고리즘을 사용하며, 메모리 효율성을 극대화하기 위해 두 가지 타입으로 구현됩니다. 두 방식 모두 공통적으로 AbstractAggregation을 상속받은 객체(Measurement 또는 PartialAggregation)를 사용하여 통계를 저장합니다.

[참고] 통계 객체(Measurement/Bucket)의 내부 구조
  • numberOfCalls: 총 호출 수 (int)
  • numberOfFailedCalls: 실패한 호출 수 (int)
  • numberOfSlowCalls: 느린 호출 수 (int)
  • numberOfSlowFailedCalls: 느리면서 실패한 호출 수 (int)
  • totalDurationInMillis: 총 소요 시간 (long)
  • 약 24 Bytes Payload + Object Header (12~16 Bytes) ≈ 40 Bytes

A. Count-based Sliding Window (횟수 기반)

Count-based Sliding WindowFixedSizeSlidingWindowMetrics 클래스 내부에서 Measurement 객체의 원형 배열(Ring Buffer)을 관리합니다.

slidingWindowSize가 100이라면 정확히 100개의 Measurement 객체를 미리 생성해두고, headIndex를 이동시키며 데이터를 덮어쓰는(Overwrite) 방식으로 동작합니다. 덕분에 객체를 매번 생성하지 않고 재사용(Reset)하여 GC 부담을 최소화했습니다. 메모리 사용량은 WindowSize * 40 Bytes 정도로, 10,000건 설정 시 약 400KB 수준입니다.

B. Time-based Sliding Window (시간 기반)

Time-based Sliding Window 역시 내부적으로 PartialAggregation 객체의 원형 배열을 사용합니다. 차이점은 배열의 각 칸(Slot)에 개별 요청이 아닌 1초 단위의 집계 데이터(Bucket) 가 담긴다는 점입니다.

예를 들어 slidingWindowSize가 10초라면, 1초 동안 100만 건의 요청이 들어와도 해당 초를 담당하는 단 1개의 버킷 객체 안의 카운트 값만 증가합니다. 따라서 트래픽 양(TPS)과 무관하게 메모리 사용량이 일정(WindowSeconds * 40 Bytes)하다는 장점이 있어 대용량 트래픽 처리에 매우 유리합니다.

C. TotalAggregation (실시간 합계 캐시)

TotalAggregationO(1) 성능을 보장하기 위한 Check-sum 객체로, 시간기반과 횟수기반 모두에서 공통적으로 사용됩니다.

매번 100개의 버킷을 루프 돌며 합산하면 O(N) 비용이 발생하므로, 실시간 합계를 별도 객체에 유지합니다. 오래된 데이터가 빠질 때(Evict) 빼고, 새로운 데이터가 들어올 때 더하는 방식으로 효율적으로 관리됩니다.

2-3. [Q&A] 자료구조와 동작 원리

Q. Measurement 객체는 BitSet을 사용하나요? (BitSet vs Object Array)

  • 아니요, 사용하지 않습니다.
    • 이유: BitSet은 0과 1 (성공/실패)만 저장할 수 있습니다. 하지만 Resilience4j는 느린 호출 판단을 위해 실행 시간 정보가 필요합니다.
    • BitSet으로는 "이 요청이 500ms 걸렸다"는 정보를 저장할 수 없기 때문에, long duration 필드를 가진 Measurement 객체를 사용하는 것입니다.

Q. 횟수 기반(Count-based)과 시간 기반(Time-based)의 차이는 무엇인가요?

  • 핵심 차이: "배열의 한 칸(Slot)에 무엇을 담는가?"
    • Count-based: 배열 한 칸 = 단 1건의 호출 정보 (Measurement). (오래된 개별 호출 기억).
    • Time-based: 배열 한 칸 = 1초 동안의 집계(Bucket) (PartialAggregation). (1초 단위로 퉁쳐서 기억).
  • 메모리는 둘 다 설정된 크기만큼만 사용하므로 일정(Constant)합니다.
  • 다만, 대용량 트래픽(TPS) 환경에서의 차이점:
    • Count-based: TPS가 폭증하면 100건 윈도우가 순식간에 회전(Overwrite) 하여, 바로 1초 전의 데이터도 사라질 수 있습니다. (과거 추적 불가).
    • Time-based: TPS가 아무리 높아도 설정된 시간(예: 10초) 동안의 이력을 안정적으로 보존합니다.

Q. Count-based 방식의 Measurement는 어차피 1개의 호출만 담는데, 왜 boolean이 아니라 int를 쓰나요?

  • 코드 재사용과 다형성 때문입니다.
    • AbstractAggregation이라는 상위 클래스로 Time-based(Bucket)와 Count-based(Single)를 모두 처리하기 위함입니다.
    • 또한 int끼리의 연산으로 Subtract-on-Evict(꼬리 빼기) 로직을 단순화할 수 있습니다.

Q. 서킷 브레이커는 Count-based와 Time-based 중 하나만 선택할 수 있나요?

  • 네, 맞습니다. 동시에 쓸 수 없습니다. SlidingWindowType 설정 하나만 가집니다.

2-4. 주요 설정 디폴트값

설정값 디폴트(Default) 설명

slidingWindowTypeCOUNT_BASED윈도우 타입 (횟수 기반 vs 시간 기반)
slidingWindowSize100통계 집계에 사용할 윈도우 크기 (최근 100건)
minimumNumberOfCalls100최소 집계 요청 수. 이 숫자보다 적을 땐 실패율이 높아도 서킷이 열리지 않음.
failureRateThreshold50 (%)실패율 임계치. 50% 이상 실패 시 OPEN.
slowCallRateThreshold100 (%)느린 응답 비율 임계치.
slowCallDurationThreshold60000 (ms)느린 응답으로 간주할 시간 기준 (60초).
waitDurationInOpenState60000 (ms)OPEN 상태 유지 시간 (60초).
permittedNumberOfCallsInHalfOpenState10HALF_OPEN 상태에서 허용할 테스트 요청 수.
automaticTransitionFromOpenToHalfOpenfalse대기 시간 종료 후 자동으로 HALF_OPEN 전환 여부. 기본은 요청이 들어와야 전환됨.

2-5. 동작 예시

상황: slidingWindowSize=10, failureRateThreshold=50%
  1. 초기 상태 (CLOSED): 요청이 계속 들어옵니다.
  1. 집계: 10개의 요청 중 4개가 실패 (40%). 상태 유지.
  1. 임계치 초과: 1개가 더 들어와서 실패 -> 최근 10개 중 5개 실패 (50%).
  1. OPEN 전환: 즉시 차단 시작. CallNotPermittedException 발생.
  1. 대기 (Wait): 60초 대기.
  1. HALF 상태 전환: 60초 후 첫 요청 진입 시 HALF_OPEN.
  1. 테스트: 정확히 10개의 요청만 허용(AtomicInteger로 카운팅). 나머지는 즉시 차단.
  1. 결과 판단: 10개 처리 후 성공률/응답속도에 따라 CLOSED 또는 OPEN 결정.

2-6. [심화] 동시성 엣지 케이스

Q. 윈도우 크기 10(임계치 50%)인데 20명이 동시에 들어오면 어떻게 되나요?

  • CLOSED 상태일 때
    • "입구 컷"은 없습니다. 20명 다 들어갑니다. (Lock-Free 구조상 진입을 막지 않음).
    • 10번째 요청이 끝나는 순간 서킷이 OPEN 되지만, 이미 진입한 20번째 요청까지는 정상 실행됩니다.
  • HALF_OPEN 상태일 때
    • 철저하게 막습니다. permittedNumberOfCallsInHalfOpenState가 10이면, AtomicInteger를 사용해 정확히 10명만 태우고 11번째부터는 CallNotPermittedException을 던집니다.

2-7. [심화] HALF_OPEN 상태와 운영 엣지 케이스

Q. HALF_OPEN에서 10개 허용했는데, 처음 1개가 실패하면 바로 OPEN 되나요? (Fail Fast)

  • 아니요, 10개 다 끝날 때까지 기다립니다.
    • 내부 로직상 minimumNumberOfCalls가 permittedNumberOfCallsInHalfOpenState와 동일하게 설정됩니다.
    • 즉, 10개 결과가 모두 집계되기 전까지는 실패율 계산을 보류(-1 반환)하고 상태를 유지합니다.
    • 10개가 모두 완료된 시점에 실패율을 계산하여 OPEN vs CLOSED를 결정합니다.

Q. 10개 중 5개 성공, 5개 실패면 어떻게 되나요?

  • 실패율(50%) >= 임계치(50%)라면 다시 OPEN 됩니다.
  • 임계치보다 낮아야만 CLOSED로 복구됩니다.

Q. 만약 10개가 모두 5초씩 걸리는 슬로우 쿼리라면? 11번째 요청은 50초를 기다려야 하나요?

  • 아니요, 즉시 차단됩니다.
    • 서킷 브레이커는 진입 시점(Entry)에 AtomicInteger 카운터를 감소시킵니다.
    • 앞선 10개가 아직 실행 중이더라도, 카운터가 0이 되면 11번째 요청은 대기 없이 바로 CallNotPermittedException을 받습니다.

Q. minimumNumberOfCalls를 윈도우 크기보다 크게 잡으면(Min=200, Window=100) 고장 나나요?

  • 아니요, 자동으로 보정됩니다.
    • 생성자에서 Math.min(min, windowSize)를 수행하므로, 실질적으로는 윈도우 크기(100)만큼만 기다립니다.
    • 설정 실수로 인한 오동작을 방지하는 안전장치가 있습니다.

2-8. Fallback 전략 (Graceful Degradation)

A. 기본 사용법

@CircuitBreaker(name = "backendA", fallbackMethod = "fallback")
public String doSomething(String param) {
    return remoteService.call(param);
}

// Fallback 메서드 시그니처는 원본과 같아야 하며, 마지막 파라미터로 Throwable을 받아야 함
private String fallback(String param, CallNotPermittedException e) {
    return "차단됨: 캐시 데이터 반환";
}

private String fallback(String param, Exception e) {
    return "그 외 에러: 기본값 반환";
}

B. [Q&A] Fallback의 함정

Q. Fallback 메서드 안에서 또 예외가 터지면 어떻게 되나요?

  • 그 예외가 그대로 상위로 전파됩니다. Fallback의 Fallback은 지원하지 않습니다.
    • 해결책: Fallback 메서드 안에는 절대 실패하지 않는 로직(Static 리턴, 로컬 캐시 조회)만 넣으세요. DB 조회 같은 거 넣지 마세요.

Q. 캐시된 데이터를 Fallback으로 쓰고 싶어요.

  • Caffeine이나 Ehcache를 사용하여 로컬 메모리에 있는 데이터를 반환하는 것이 가장 이상적인 패턴입니다.

Q. Fallback도 실패하면 2차 Fallback을 하고 싶어요.

  • Resilience4j 자체 기능으로는 불가능하며, try-catch로 감싸거나 Spring @Recover를 혼용해야 하는데 권장하지 않습니다. 구조가 너무 복잡해집니다.

3. Retry

Retry 모듈은 데코레이터 패턴을 사용하여 함수 실행을 감싸고, 예외 발생 시 조건에 따라 재실행하는 단순하면서도 강력한 구조입니다.

3-1. 동작 메커니즘

Allowable 인터페이스를 통해 재시도 가능한 예외인지 판단합니다.

  • 동기 방식: While 루프나 재귀 호출이 아닌, Supplier를 RetryContext로 감싸서 실행합니다.
  • 비동기 방식: Flux.retryWhen()과 유사하게 동작하며 Context 내에서 시도 횟수를 관리합니다.

3-2. [중요] RateLimiter와 함께 사용 시 주의사항 (Critical Warning)

[!WARNING] Retry Storm 방지기본값으로는 RateLimiter의 예외(RequestNotPermitted)도 재시도 대상에 포함됩니다.

만약 RateLimiter와 Retry를 같이 쓰는데 설정을 안 하면 무한 지옥(Retry Storm) 이 발생합니다. (차단됨 -> 재시도 -> 또 차단됨 -> 또 재시도...)

해결책: 반드시 ignoreExceptions에 추가해야 합니다.

RetryConfig config = RetryConfig.custom()
    .ignoreExceptions(RequestNotPermitted.class) // 필수 설정!
    .build();

3-3. [Q&A] Retry 설정

Q. retryExceptions / ignoreExceptions가 비어있으면(기본값) 어떻게 동작하나요?

  • 주의: 기본적으로 "모든 예외(Throwable)"에 대해 재시도합니다.
    • RetryConfig 소스 코드를 보면, 예외 필터가 없을 경우 기본 Predicate가 throwable -> true로 설정되어 있습니다.
    • 따라서 NullPointerException 같은 프로그래밍 오류도 무지성으로 재시도할 수 있으므로, 명시적으로 retryExceptions를 지정하는 것이 좋습니다.

3-4. 주요 설정 디폴트값

설정값 디폴트(Default) 설명

maxAttempts3최대 시도 횟수 (최초 시도 포함).
waitDuration500 (ms)재시도 사이의 대기 시간.
retryExceptionsempty재시도할 예외 클래스 목록. (비어있으면 모든 예외 재시도)
ignoreExceptionsempty재시도하지 않고 즉시 실패 처리할 예외.

4. Rate Limiter

Rate Limiter는 Token Bucket 알고리즘을 기반으로 구현되어 있습니다.

4-1. AtomicRateLimiter

Resilience4j의 구현체인 AtomicRateLimiter는 락을 사용하지 않고 AtomicReference를 사용하여 고성능을 보장합니다.

현재 사이클 번호와 남은 토큰 수 등을 담은 불변 State 객체CAS(Compare-And-Swap) 연산으로 업데이트하여 동시성을 제어합니다. 또한 별도의 스레드가 토큰을 채워주는 것이 아니라, 요청이 들어올 때마다 (현재 시간 - 마지막 갱신 시간) * 리필 속도를 계산하여 토큰을 채우는 Lazy Calculation 방식을 사용합니다.

4-2. [Q&A] Timeout 설정 팁

Q. 운영 환경에서 timeoutDuration은 보통 0s로 하나요?

  • Case By Case지만, 'Fail Fast'가 원칙인 API 서버에서는 0s를 권장합니다.
    • 0s (Zero): 토큰이 없으면 즉시 거절(RequestNotPermitted). 사용자에게 빨리 실패를 알리는 것이 시스템 전체 안정성에 좋습니다.
    • Large Wait: 절대 금지. 스레드 풀 고갈의 원인이 됩니다.

4-3. 고급 활용: 여러 개의 RateLimiter 조합 (RPM + RPD)

"하루 1500회(RPD) 제한과 동시에 1분당 60회(RPM) 제한"을 걸고 싶다면, 데코레이터를 중첩하면 됩니다.

// RPD(하루) 안에 RPM(분당)을 넣어서 이중으로 감쌈
Supplier<String> decorated = RateLimiter.decorateSupplier(rpdLimiter,
    RateLimiter.decorateSupplier(rpmLimiter, service::call));

4-4. 주요 설정 디폴트값

설정값 디폴트(Default) 설명

limitForPeriod50한 주기(Period) 동안 허용할 요청 수(토큰 수).
limitRefreshPeriod500 (ns)[주의] 기본값은 500 나노초(0.0005ms)입니다. limitForPeriod가 50이라면, 사실상 초당 1억 건을 허용하는 "무제한" 설정과 같습니다. 반드시 1s(초) 이상으로 변경해서 쓰세요.
timeoutDuration5000 (ms)토큰이 없을 때 대기할 최대 시간.

5. Bulkhead

시스템 리소스를 격리하여 장애 전파를 막습니다.

5-1. 두 가지 구현체

  1. SemaphoreBulkhead: 세마포어로 동시 실행 스레드 수만 제한. (가볍다. 호출자 스레드 차단 가능성 있음).
  1. ThreadPoolBulkhead: 별도의 스레드 풀과 큐를 생성하여 완벽 격리. (가장 안전하지만 무겁다).

5-2. 주요 설정 디폴트값

SemaphoreBulkhead

설정값 디폴트 설명

maxConcurrentCalls25동시 실행 최대 수.

ThreadPoolBulkhead

설정값 디폴트 설명

maxThreadPoolSizeCPU 코어 수스레드 풀 문 닫을 때 크기.
queueCapacity100대기 큐 크기.

6. Time Limiter

작업의 실행 시간을 제한하며, 주로 비동기 작업(CompletableFuture)과 함께 사용됩니다.

Time Limiter는 작업 실행과 타이머를 경주시시키는 방식으로 동작합니다. 타이머가 먼저 끝나면 TimeoutException을 던지고 작업을 cancel 합니다. 단, 이 과정은 자바의 인터럽트 메커니즘에 의존하므로, 작업 코드가 인터럽트를 무시하거나 블로킹되어 있다면 실제로 멈추지 않을 수 있다는 점에 주의해야 합니다.

디폴트값: timeoutDuration = 1000ms (1초).


7. 심화: 동작 원리와 동시성

7-1. 어노테이션 우선순위

Spring Boot에서 여러 어노테이션을 동시에 붙였을 때 실행 순서(바깥 -> 안쪽)는 다음과 같습니다.

  1. Retry (제일 바깥. 서킷 에러도 잡아서 재시도)
  1. CircuitBreaker
  1. RateLimiter
  1. TimeLimiter
  1. Bulkhead (제일 안쪽. 스레드 점유 직전)

8. 운영 모니터링

8-1. Micrometer & Prometheus 연동 및 주의사항

resilience4j-micrometer 모듈을 사용하면 서킷, 리트라이 등의 상태를 메트릭으로 노출할 수 있습니다.

  • 필수 Grafana 패널
    • Circuit State: resilience4j_circuitbreaker_state (Value 0=CLOSED, 1=OPEN, 2=HALF_OPEN). StateChange 시점에 점 찍히도록 시계열 그래프 추천.
    • Failure Rate: resilience4j_circuitbreaker_failure_rate.
    • Slow Calls: resilience4j_circuitbreaker_slow_calls.
  • [Troubleshooting] Slow Call 메트릭이 안 떠요!
    • slowCallDurationThreshold만 설정하고 slowCallRateThreshold를 설정 안 하면 집계조차 안 될 수 있습니다. (또는 호출 수가 minimumNumberOfCalls 미만일 때).
    • 반드시 두 설정을 세트로 맞춰주세요.

8-2. Spring Boot Actuator Health

management.health.circuitbreakers.enabled=true 설정으로 헬스 체크에 포함시킬 수 있습니다.

  • 주의: 쿠버네티스 Liveness Probe와 연동하지 마세요. 서킷 열렸다고 파드 재시작하면 안 됩니다.

8-3. Event Consumer 활용

상태 변화 시점에 로그를 남기거나 알람을 보내려면 EventConsumer를 등록하세요.

// 예시: 서킷 상태가 변할 때마다 로그 남기기
circuitBreaker.getEventPublisher()
    .onStateTransition(event -> log.warn("CircuitBreaker State Change: {} -> {}",
        event.getStateTransition().getFromState(),
        event.getStateTransition().getToState()));

References: