Resilience4j Circuit Breaker, 운영에서 이렇게 적용했다
이번엔 Circuit Breaker를 이론 말고, 실제 적용한 방식 기준으로 정리해본다.
상황은 단순했다.
- 외부 API가 간헐적으로 느려지거나 timeout
- 우리 API 스레드가 기다리다가 묶임
- 지연이 누적되면서 5xx가 같이 증가
그래서 Resilience4j를 넣었고, 운영에서 급할 때는 Circuit을 강제로 열고/닫을 수 있게
Spring Cloud 기반 제어를 같이 붙였다.
1) 왜 Circuit Breaker가 필요했는지
Circuit 없을 때는 장애가 아래처럼 번졌다.
- 다운스트림 timeout 증가
- 업스트림 대기 스레드 누적
- 전체 응답 지연 + 에러율 상승
핵심은 “느린 외부 의존성 하나”가 전체 서비스 품질을 같이 끌어내린다는 점이었다.
2) 적용 스택
spring-cloud-starter-circuitbreaker-resilience4jspring-boot-starter-actuatorspring-cloud-starter-config(운영 제어용)
이 조합으로,
- 코드 호출은 Spring Cloud CircuitBreaker API로 감싸고
- 실제 상태 관리는 Resilience4j가 담당하고
- 강제 OPEN/CLOSE는 Spring Cloud Config 값으로 제어했다.
여기서 많이 헷갈리는 포인트가 하나 있다.
- Spring Cloud는 Circuit/Config를 위한 추상화 + 연동 레이어
- 운영 화면(UI)은 기본 제공이 아니라 보통 Grafana/Spring Boot Admin/사내 도구로 구성
즉 Spring Cloud를 붙였다고 Circuit 전용 웹 콘솔이 자동으로 생기진 않는다.
3) 기본 설정값 (실무에서 많이 쓰는 시작점)
resilience4j:
circuitbreaker:
instances:
orderApi:
registerHealthIndicator: true
slidingWindowType: COUNT_BASED
slidingWindowSize: 50
minimumNumberOfCalls: 20
failureRateThreshold: 50
slowCallRateThreshold: 60
slowCallDurationThreshold: 2s
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 10
automaticTransitionFromOpenToHalfOpenEnabled: true
management:
endpoints:
web:
exposure:
include: health,info,prometheus,refresh,busrefresh
이 값은 정답이 아니라 시작점이다.
중요한 건 운영 지표 보면서 계속 튜닝하는 거다.
4) 실제 호출 코드 예시
@Service
@RequiredArgsConstructor
public class OrderGateway {
private final CircuitBreakerFactory<?, ?> circuitBreakerFactory;
private final RestClient orderClient;
public OrderResponse getOrder(String orderId) {
CircuitBreaker cb = circuitBreakerFactory.create("orderApi");
return cb.run(
() -> orderClient.get()
.uri("/external/orders/{id}", orderId)
.retrieve()
.body(OrderResponse.class),
throwable -> OrderResponse.fallback(orderId)
);
}
}
여기서 포인트는 fallback을 “무조건 성공처럼” 만들지 않는 거다.
프론트/클라이언트가 fallback 응답임을 구분할 수 있게 설계해야 운영에서 덜 꼬인다.
fallbackMethod 방식도 같이 쓴다
팀/코드베이스에 따라 annotation 방식이 더 읽기 쉬울 때가 있다.
@Service
public class OrderQueryService {
private final ExternalOrderClient externalOrderClient;
public OrderQueryService(ExternalOrderClient externalOrderClient) {
this.externalOrderClient = externalOrderClient;
}
@io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker(
name = "orderApi",
fallbackMethod = "getOrderFallback"
)
public OrderResponse getOrder(String orderId) {
return externalOrderClient.getOrder(orderId);
}
private OrderResponse getOrderFallback(String orderId, Throwable t) {
return OrderResponse.fallback(orderId);
}
}
정리하면 둘 다 가능하다.
- Spring Cloud CircuitBreaker
cb.run(..., fallback)함수형 방식 - Resilience4j annotation +
fallbackMethod방식
5) Circuit 상태 이해 (운영 기준)
CLOSED: 정상 호출 + 통계 수집OPEN: fail-fast, 다운스트림 호출 차단HALF_OPEN: 제한된 probe 호출로 복구 여부 확인
그리고 운영 제어용으로 FORCED_OPEN, FORCED_CLOSED를 사용했다.
6) 강제 OPEN/CLOSE 제어를 Spring Cloud로 붙인 방식
요구사항이 이거였다.
- 장애 대응 중 운영자가 circuit을 강제로 열거나 닫을 수 있어야 함
- 재배포 없이 반영돼야 함
그래서 아래 흐름으로 구성했다.
- Spring Cloud Config 저장소에 모드 값 관리
- 운영자가 값 변경 (
AUTO,FORCED_OPEN,FORCED_CLOSED) /actuator/refresh(단일 인스턴스) 또는/actuator/busrefresh(다중 인스턴스) 호출- 앱이 갱신된 모드를 읽고 Circuit 상태 전환
여기서 /actuator는 Prometheus 전용 경로가 아니다.
Actuator 관리 엔드포인트 공통 베이스 경로이고, prometheus, health, refresh 등이 그 아래에 붙는다.
예시:
# single instance refresh
curl -X POST http://127.0.0.1:18081/actuator/refresh
# multi instance refresh (Spring Cloud Bus)
curl -X POST http://127.0.0.1:18081/actuator/busrefresh
운영에서는 이 엔드포인트를 외부 공개하지 않고, 내부망 + 인증으로 제한하는 걸 전제로 사용했다.
운영 모드 프로퍼티
ops:
circuit:
order-api:
mode: AUTO # AUTO | FORCED_OPEN | FORCED_CLOSED
RefreshScope + 상태 동기화 코드
@Getter
@Setter
@RefreshScope
@Component
@ConfigurationProperties(prefix = "ops.circuit.order-api")
public class CircuitControlProperties {
private Mode mode = Mode.AUTO;
public enum Mode {
AUTO,
FORCED_OPEN,
FORCED_CLOSED
}
}
@Service
@RequiredArgsConstructor
public class CircuitStateSynchronizer {
private final CircuitBreakerRegistry registry;
private final CircuitControlProperties properties;
@Scheduled(fixedDelayString = "${ops.circuit.sync-delay-ms:3000}")
public void syncState() {
io.github.resilience4j.circuitbreaker.CircuitBreaker cb =
registry.circuitBreaker("orderApi");
switch (properties.getMode()) {
case FORCED_OPEN -> cb.transitionToForcedOpenState();
case FORCED_CLOSED -> cb.transitionToClosedState();
case AUTO -> cb.reset();
}
}
}
FORCED_CLOSED는 보호막을 우회하는 동작이라 진짜 짧게만 써야 한다.
장애 대응에선 보통 FORCED_OPEN이 더 자주 필요했다.
7) 운영에서 실제로 본 지표
- circuit state 전환 빈도
- fallback 비율
- 실패/느린 호출 비율
이 지표를 안 보면 “설정 넣었으니 끝”이 되는데,
실제로는 상태 전환 패턴을 보고 threshold를 계속 조정해야 한다.
8) 롤아웃할 때 체크한 순서
- 관측부터 붙이고(상태/실패/느린 호출)
- fallback 응답 품질 검증하고
- 온콜 runbook 정리하고
- 강제 제어 모드(OPEN/CLOSED)까지 운영 절차에 포함
정리하면 Circuit Breaker는 라이브러리 하나 추가하는 작업이 아니라,
- 장애 전파를 끊는 런타임 장치
- 운영자가 제어할 수 있는 대응 장치
이 두 개를 같이 만드는 작업이었다.
특히 Spring Cloud Config + refresh로 강제 상태 제어를 붙여두면,
긴급 대응 속도가 확실히 빨라진다.
References
- Spring Cloud Circuit Breaker Reference: https://docs.spring.io/spring-cloud-circuitbreaker/reference/
- Resilience4j CircuitBreaker Guide: https://resilience4j.readme.io/docs/circuitbreaker
- Spring Cloud Config Reference: https://docs.spring.io/spring-cloud-config/docs/current/reference/html/
- Spring Cloud Commons Refresh Scope: https://docs.spring.io/spring-cloud-commons/reference/spring-cloud-commons/application-context-services.html#refresh-scope
- Spring Boot Actuator Endpoints: https://docs.spring.io/spring-boot/reference/actuator/endpoints.html