티스토리 뷰
내가 처음 써킷브레이커 개념에 대해 접했던건 Netflix OSS 의 hystrix 를 통해서였다. 그때만 해도 생소했었는데 이제 써킷브레이커는 굉장히 친숙한 기술이 됐다. 그 사이 hystrix 는 저물고 resilience4j 가 자바 진영의 표준 써킷브레이커가 되었다.
써킷브레이커의 개념은 신입 개발자를 채용할때도 어렵지 않게 대답을 들을 수 있다. 특정 루틴이 일정량 실패하면 해당 루틴을 실행하지 않고 즉시 실패했다가(OPEN), 이후 루틴을 다시 실행해보고(HALF-OPEN) 성공한다면 계속 실행하고(CLOSE) 실패한다면 계속 즉시실패(OPEN)하는 형태로 의존 루틴의 장애가 전파되지 않도록 차단하는 기술이다.
꼭 외부 시스템에 한해서 사용하는 기술은 아니지만 보통은 외부 시스템을 호출하는 부분에서 사용하고, 해당 시스템의 장애 상황시 우리 시스템을 보호하는 역할을 하는데 보통 써킷브레이커가 열릴때의 로그패턴은 이렇다.
의존시스템의 에러응답 로그
의존시스템의 에러응답 로그
의존시스템의 에러응답 로그
... 설정량만큼 에러응답 발생
써킷브레이커가 열렸다는 로그
써킷브레이커가 열렸다는 로그
... 이후 복구
다만 최근 에러로그 없이 써킷브레이커가 열리는 이슈가 있었고, 이를 정리해보려한다.
... 사전 에러로그 없음
써킷브레이커가 열렸다는 로그
써킷브레이커가 열렸다는 로그
... 이후 복구
# 일반적인 써킷브레이커
// 써킷브레이커 설정
public CircuitBreakerConfig createCircuitBreakerConfig() {
return CircuitBreakerConfig.custom()
// 써킷브레이커 상태변이를 계산하기 위한 최소 실행 횟수
.minimumNumberOfCalls(5)
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
// 써킷브레이커 조건을 계산하기 위해 결과를 저장하는 윈도우 크기
.slidingWindowSize(10)
// 써킷브레이커가 열리기 위한 실패율
.failureRateThreshold(50)
// 슬로우콜로 인지할 시간
.slowCallDurationThreshold(Duration.ofMillis(200))
// 써킷브레이커가 열리기 위한 슬로우콜 비율
.slowCallRateThreshold(50)
.build();
}
// 써킷브레이커 실행, 모든 호출은 실패하며 10번 반복
public void simulate(CircuitBreaker circuitBreaker) {
Runnable runnable = circuitBreaker.decorateRunnable(() -> {
// 보통 외부 API 호출
if (true) {
throw new RuntimeException("Failed!");
}
System.out.println("Succeed");
});
for(int i = 0; i < 10; i++) {
try {
runnable.run();
} catch (Exception e) {
System.out.println("Call failed: " + e.getMessage());
}
}
}
// 실행코드
CircuitBreakerSimulator simulator = new CircuitBreakerSimulator();
CircuitBreakerConfig config = simulator.createCircuitBreakerConfig();
CircuitBreaker circuitBreaker = CircuitBreaker.of("circuitBreaker", config);
simulator.simulate(circuitBreaker);
위 코드는 최소 5번 이상의 호출에서 실패율이 50% 이상이라면 써킷브레이커가 열리는 코드이다. 그리고 현재는 10번 호출해서 10번 모두 실패하게끔 되어있다. 실행결과는 이렇다.
Call failed: Failed!
Call failed: Failed!
Call failed: Failed!
Call failed: Failed!
Call failed: Failed!
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
10번 모두 실패하지만 최소 5번까지는 써킷브레이커가 열리지 않기 때문에 5번까지는 내부 RuntimeException 메시지가 출력된다. 그리고 6번째부터는 써킷브레이커가 열리게 된다. 내부 RuntimeException 으로 표현한 로직이 외부 API 호출이라면 해당 API 의 에러가 발생하게 될 것이다. 보통은 써킷브레이커가 열리는 시점엔 이런 유형의 로그가 남게되고, 써킷브레이크가 열리기 이전 에러로그를 보면서 써킷브레이커가 열리게된 원인을 파악하게 된다.
# 에러로그 없는 써킷브레이커
Succeed
Succeed
Succeed
Succeed
Succeed
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
Call failed: CircuitBreaker 'circuitBreaker' is OPEN and does not permit further calls
하지만 이런 로그가 발생한다면? 분명히 다섯번째까지 잘 성공해놓고, 여섯번째부터 써킷브레이커가 열리고있다. 처음엔 어디선가 에러로그는 삼키고있는게 아닌가 하는 생각에 삽질을 좀 했었는데 이를 재현하기 위한 코드는 이렇다.
Runnable runnable = circuitBreaker.decorateRunnable(() -> {
// 보통 외부 API 호출
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Succeed");
});
첫번째 코드에서는 강제로 예외를 일으키는 코드였지만 두번째는 강제로 슬로우콜을 일으키는 코드로 변경한 것이다. 위 코드는 슬로우콜로 인해 써킷브레이커의 슬로우콜 카운트는 증가시키지만 별도로 예외를 일으키지는 않아 써킷브레이커가 열리기 전에는 슬로우콜을 파악하기 어렵다. 슬로우콜로 인한 동작은 이런 순서로 동작한다.
- 루틴 실행
- 해당 루틴이 슬로우콜 설정 이상의 시간을 소요함
- resilience4j 는 슬로우콜을 중단시키거나 에러로그를 남기지 않음
- 슬로우콜은 계속 실행하면서 슬로우콜 카운트만 증가시킴
- 슬로우콜 카운트가 설정에 도달하면 조용히 써킷브레이커 상태를 OPEN 으로 변경
지금과 같은 예제코드에서는 코드 변경이 확 눈에 띄어 문제를 찾기가 쉽지만 현실의 코드에서는 외부 API 를 호출하는 로직이 들어있는 경우가 많다. 예를들어 API 호출시 read timeout 은 10초를 걸어놓고, resilience4j 의 slowCallDurationThreshold 는 5초를 잡아놓은 상태에서 API 가 7초가 걸려 응답한다면 API 는 모두 성공하지만 어느순간 써킷브레이커가 열리게되고 원인을 찾아내기가 쉽지 않다. 참고로 slowCallDurationThreshold 속성의 기본값은 60000ms(1분)이다.
# 하나 더
써킷브레이커가 열리는 조건을 설정할때 주로 보는 프로퍼티는 slidingWindowSize, minimumNumberOfCalls 가 있다. slidingWindowSize 는 써킷브레이커의 실행 결과를 저장하는 갯수의 크기이다. 100으로 지정하면 최근 100개의 결과를 저장하게 된다.
minimumNumberOfCalls 는 써킷브레이커의 상태변이를 위한 최소 실행 갯수이다. 해당 설정이 100으로 되어있으면 99번째 호출까지는 100% 실패하더라도 써킷브레이커가 열리지 않는다.
여기서 주의해야할 부분은 두 설정의 상관관계이다. minimumNumberOfCalls 가 slidingWindowSize 보다 클 경우 minimumNumberOfCalls 는 slidingWindowSize 와 동일하게 동작한다.
- minimumNumberOfCalls 100 slidingWindowSize 10
위 설정일 경우 100번 호출되기 전까진 써킷브레이커가 열리지 않겠지 라고 생각하면 큰 오산이라는 의미다. 10번째 호출부터 써킷브레이커가 열릴 수 있다. 이런 설명이 문서에는 상당히 빈약하기 때문에 slidingWindowSize 는 작게 가져가고 minimumNumberOfCalls 를 크게 가져간다면 그 설정은 아무런 의미가 없게된다. 참고로 slidingWindowSize 는 기본값이 100 이다.
위 내용은 claude opus 4.8 모델도 잘못대답하고 있었고 정정 프롬프트를 넣기 전까지는 틀린 내용을 답해주고 있었기 때문에 AI 만 믿고 사용하다가도 큰코다칠 수 있으니 유의하자.
참고로 위 두 설정의 상관관계는 slidingWindowType 이 COUNT_BASED 일때 동작한다. TIME_BASED 일때는 상관없으므로 TIME_BASED 일땐 무시해도 된다.
# 참고자료
https://resilience4j.readme.io/docs/circuitbreaker
CircuitBreaker
Getting started with resilience4j-circuitbreaker
resilience4j.readme.io
'Java' 카테고리의 다른 글
| apache httpcomponents client 의 timeout 동작 테스트 (0) | 2025.08.23 |
|---|---|
| apache httpcomponents client 사용시 상황별 timeout과 connection pool 동작 (0) | 2025.08.15 |
| synchronized 구문에서 virtual thread 동작과 JEP 491 (0) | 2025.08.10 |
| JSR354 JavaMoney (1) | 2025.04.25 |
| 예외처리는 어떻게 하는게 좋을까 (3) | 2025.01.05 |
- Total
- Today
- Yesterday
- db
- toby
- OOP
- EffectiveJava
- spring cloud
- Spring
- JPA
- TEST
- go-core
- Kotlin
- Jackson
- code
- javascript
- JavaScript Core
- frontcode
- java8
- Git
- backend개발환경
- MySQL
- http
- servlet
- java
- mariadb
- frontend개발환경
- DesignPattern
- 정규표현식
- Design Pattern
- programming
- clean code
- generics
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
