TL;DR
'slidingWindowSize'는 최근 어디까지 볼지를 정하는 값이고, 'minimumNumberOfCalls'는 언제부터 그 숫자를 믿고 판단할지를 정하는 값이다.
TIME_BASED에서는 특히 호출 수가 적은 구간이 생길 수 있어서, 'window'와 'minimum call'을 따로 봐야 한다.
개발하다 보면 이름은 비슷한데, 실제로는 다른 문제를 푸는 설정을 만나게 된다. 이번에 가장 헷갈렸던 값이 'slidingWindowSize'와 'minimumNumberOfCalls'였다.
처음엔 둘 다 실패율 계산에 관여하니 거의 같은 축의 설정처럼 보였다. 여기서 의문이 들었다.
"window가 있는데 minimum call이 왜 또 필요하지?"
이번 글에서는 그 차이를 개념과 검증 흐름으로 정리해본다.
문제 상황
- 'slidingWindowSize'와 'minimumNumberOfCalls'가 둘 다 실패율 계산에 관여해서 역할이 겹쳐 보였다.
- 특히 TIME_BASED에서는 최근 N초를 본다는데, minimum call이 왜 또 필요한지 직관이 잘 오지 않았다.
- 이 둘을 같은 값처럼 읽으면 저트래픽 구간에서 회로가 너무 빨리 열리거나, 반대로 지나치게 늦게 열릴 수 있다.
결국 이번 글의 질문은 하나였다. 'slidingWindowSize'와 'minimumNumberOfCalls'는 정확히 무엇이 다를까?
분석
실패율만 보면 왜 헷갈릴까
실패율이라는 숫자는 직관을 쉽게 속인다. 실패율 100%라는 결과만 보면 둘 다 똑같이 위험해 보인다.
- 1번 호출했는데 1번 실패한 경우
- 10번 호출했는데 10번 실패한 경우
숫자만 보면 둘 다 100%다. 하지만 운영 관점에서 둘은 전혀 같은 상황이 아니다. 첫 번째는 우연이나 일시적 장애일 수 있고, 두 번째는 패턴으로 봐야 할 가능성이 훨씬 높다.
즉 서킷브레이커는 단순히 '최근 실패율이 높다'만 보면 안 됐다. 적어도 아래 두 질문에는 답할 수 있어야 했다.
- 최근 어디까지를 보고 판단할 것인가
- 최소 몇 건은 모여야 그 판단을 믿을 것인가
아래 화면은 Claude의 'show me'를 이용해 시뮬레이터 상태를 시각적으로 확인한 캡처다. 실패율은 이미 75%지만, 아직 'minimumNumberOfCalls = 5'에 도달하지 않았기 때문에 평가가 보류되는 장면이다.

이 장면 하나만 봐도 실패율과 표본 수를 같은 문제로 보면 안 된다는 점이 드러난다.
두 값이 답하는 질문은 달랐다
결론부터 말하면, 두 값은 같은 숫자를 조절하는 설정이 아니었다.
- 'slidingWindowSize'는 최근 어디까지를 현재 상태로 볼지 정한다.
- 'minimumNumberOfCalls'는 몇 건은 쌓여야 실패율 판단을 시작할지 정한다.
- 'failureRateThreshold'는 그 실패율이 어느 수준을 넘으면 OPEN으로 볼지 정한다.
예를 들어 'slidingWindowSize = 10', 'minimumNumberOfCalls = 5'라면 최근 10건을 기억하되, 그 안에서 최소 5건이 쌓이기 전까지는 실패율이 높아도 회로를 열지 않는다. 즉 'window가 가득 차야 평가한다'가 아니라, '판단 가능한 최소 표본이 모인 순간부터 현재 window 기준으로 평가한다'가 더 정확한 해석이었다.
원인
원인은 실패율과 표본 수를 같은 문제로 생각한 데 있었다. 실패율은 비율이고, 'minimumNumberOfCalls'는 그 비율을 판단해도 되는 최소 표본 수다. 둘은 함께 움직이지만, 역할 자체는 달랐다.
이 오해가 특히 TIME_BASED에서 커지는 이유도 여기 있었다. COUNT_BASED는 최근 N건을 보니 표본 수가 눈에 보인다. 반면 TIME_BASED는 최근 N초를 보는 방식이라, window 안에 실제로 몇 건이 들어있는지가 트래픽 양에 따라 계속 달라진다.
그래서 TIME_BASED를 읽을 때는 아래 두 문장을 같이 봐야 했다.
- 'slidingWindowSize'는 최근 몇 초를 볼지 정한다.
- 'minimumNumberOfCalls'는 그 몇 초 안에 몇 건은 쌓여야 판단할지 정한다.
즉 TIME_BASED에서 'window'는 시간 축이고, 'minimumNumberOfCalls'는 표본 수 축이었다.
아래 화면도 Claude의 'show me'로 시각적으로 확인한 캡처다. 실패 3건을 만든 뒤 시간을 흘려보내고 마지막에 성공 1건을 넣으면, 오래된 실패 버킷이 window 밖으로 밀려나면서 현재 window 안에는 최신 호출만 남는다.

이 차이 때문에 TIME_BASED는 특히 저트래픽 구간에서 과민해질 수 있다. 시간 window는 유지되지만, 그 안의 호출 수는 충분하지 않을 수 있기 때문이다.
해결
그래서 설정을 읽는 기준 자체를 바꿨다.
이제는 이렇게 읽는다
- 'slidingWindowSize'는 관찰 범위다.
- 'minimumNumberOfCalls'는 판단 시작선이다.
- 'failureRateThreshold'는 OPEN 전환 민감도다.
이렇게 나눠서 읽으면 왜 값이 분리돼 있는지 자연스럽다.
예를 들어 같은 'window size'를 쓰더라도 최소 표본 수는 다르게 잡을 수 있다.
- 최근 60초를 보되, 최소 20건은 쌓여야 판단한다.
- 최근 60초를 보되, 최소 5건만 쌓여도 판단한다.
두 설정은 같은 시간 범위를 본다. 하지만 OPEN이 열리는 민감도는 전혀 다르다.
반대로 최근 10건을 보되 최소 5건부터 판단할 수도 있고, 최근 100건을 보되 최소 20건부터 판단할 수도 있다. 이번에는 판단 시작선뿐 아니라, 얼마나 긴 히스토리를 최근 상태로 볼지도 달라진다.
결국 운영에서 따로 조절하고 싶은 것은 아래 두 가지였다.
- 최근 상태를 얼마나 넓게 볼지
- 그 최근 상태가 판단할 만큼 충분히 쌓였는지
이 둘을 한 값으로 묶어버리면, 민감도 조절과 관찰 범위 조절을 동시에 건드리게 된다. 그렇게 되면 특정 트래픽 패턴에서는 너무 빨리 열리고, 다른 패턴에서는 지나치게 둔해질 수 있다.
검증
비교 축은 'TIME_BASED / COUNT_BASED'였다. 트래픽 유형은 '단건 호출 + 시간 경과'였다.
위 해석이 실제로도 맞는지는 'pgQueryCircuitBreaker' 설정값을 작게 줄인 뒤, 성공과 실패 호출을 의도적으로 넣고 각 단계마다 'state'와 'metrics'를 확인하는 방식으로 검증했다.
테스트 설정은 아래 두 가지였다.
- TIME_BASED: 'slidingWindowSize = 2초', 'minimumNumberOfCalls = 10'
- COUNT_BASED: 'slidingWindowSize = 10건', 'minimumNumberOfCalls = 10'
확인한 결과는 세 가지였다.
- 'minimumNumberOfCalls = 10'일 때는 실패율이 100%여도 9건까지는 CLOSED였고, 10번째 실패가 들어온 뒤에야 OPEN이 됐다.
- TIME_BASED는 2.5초가 지난 뒤 다음 호출이 들어오자 이전 실패가 window에서 빠졌고, COUNT_BASED는 시간이 아니라 새 호출이 쌓여야 오래된 실패가 밀려났다.
- 둘 다 HALF_OPEN에서 복구에 성공해 다시 CLOSED가 되면 이전 window를 이어받지 않고 새 표본부터 다시 시작했다.
실제로는 아래처럼 시나리오를 하나씩 실행하면서 확인했다.
// 1) minimumNumberOfCalls 확인
repeat(9) { failQuery(); }
assertState(CLOSED);
failQuery();
assertState(OPEN);
// 2) TIME_BASED 확인
repeat(3) { failQuery(); }
sleep(2500);
successQuery();
assertMetrics(bufferedCalls = 1, failedCalls = 0);
// 3) COUNT_BASED 확인
repeat(3) { failQuery(); }
sleep(2500);
successQuery();
assertMetrics(bufferedCalls = 4, failedCalls = 3);
// 4) HALF_OPEN -> CLOSED 복구 확인
repeat(10) { failQuery(); }
sleep(1000);
successQuery();
successQuery();
assertState(CLOSED);
assertMetrics(bufferedCalls = 0, failedCalls = 0);
결국 이 글에서 중요했던 건 '어느 테스트 클래스를 실행했는가'보다, '어떤 호출 순서를 만들고 그때 metrics가 어떻게 바뀌는지를 확인했는가'였다.
정리
결국 헷갈렸던 이유는 실패율과 표본 수를 같은 문제로 본 데 있었다. 실제로는 'slidingWindowSize'가 관찰 범위였고, 'minimumNumberOfCalls'는 판단 시작선이었다.
- 'slidingWindowSize'는 최근 어디까지를 현재 상태로 볼지 정한다.
- 'minimumNumberOfCalls'는 언제부터 그 숫자를 믿고 판단할지 정한다.
- TIME_BASED는 시간 축과 표본 수 축을 같이 봐야 덜 헷갈린다.
결국 'window size는 관찰 범위, minimum call은 판단 시작선'. 이게 핵심이다.