- 들어가며
- 연쇄 장애(Cascading Failure) 이해
- 서킷 브레이커 상태 전이
- Resilience4j 구현
- Python 구현 (pybreaker)
- 폴백 전략 패턴
- Resilience 패턴 조합
- Prometheus 메트릭
- 테스트
- 퀴즈
- 마무리
- 참고 자료

들어가며
마이크로서비스 아키텍처에서 서비스 간 호출은 필연적입니다. 하지만 하나의 서비스가 느려지거나 응답을 하지 않으면, 호출하는 서비스까지 연쇄적으로 장애가 전파됩니다. 서킷 브레이커(Circuit Breaker) 패턴은 전기의 차단기처럼, 장애가 전파되기 전에 회로를 끊어 시스템 전체의 안정성을 보호합니다.
연쇄 장애(Cascading Failure) 이해
정상 상태:
[Client] → [API Gateway] → [Order Service] → [Payment Service] → [Bank API]
↓
[Inventory Service]
Payment Service 장애 시 (서킷 브레이커 없음):
[Client] ← 타임아웃 ← [API Gateway] ← 타임아웃 ← [Order Service] ← 타임아웃 ← [Payment Service] ✗
스레드 고갈! 스레드 고갈!
결과: Payment 장애가 전체 시스템을 마비시킴
서킷 브레이커 상태 전이
실패율 < 임계값 실패율 >= 임계값
┌────────────────────┐ ┌────────────────────┐
│ │ │ │
▼ │ │ ▼
┌────────┐ ┌────────────┐ ┌──────────┐
│ CLOSED │ ──────> │ HALF-OPEN │ <────── │ OPEN │
│ (정상) │ │ (시험 허용) │ │ (차단) │
└────────┘ └────────────┘ └──────────┘
▲ │ │
│ │ │
└────────────────────┘ │
시험 호출 성공 대기 시간 경과
(waitDurationInOpenState)
- CLOSED: 정상 상태, 모든 요청 허용
- OPEN: 장애 감지, 모든 요청 즉시 실패 (빠른 실패)
- HALF-OPEN: 제한된 수의 시험 호출 허용, 성공하면 CLOSED로 복귀
Resilience4j 구현
의존성 추가
<!-- Maven -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
// Gradle
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
설정 (application.yml)
resilience4j:
circuitbreaker:
instances:
paymentService:
registerHealthIndicator: true
slidingWindowType: COUNT_BASED
slidingWindowSize: 10 # 최근 10개 요청 기준
minimumNumberOfCalls: 5 # 최소 5번 호출 후 평가
failureRateThreshold: 50 # 실패율 50% 이상 → OPEN
slowCallRateThreshold: 80 # 느린 호출 80% 이상 → OPEN
slowCallDurationThreshold: 2s # 2초 이상 → 느린 호출
waitDurationInOpenState: 30s # OPEN → HALF-OPEN 대기
permittedNumberOfCallsInHalfOpenState: 3 # HALF-OPEN 시험 호출 수
automaticTransitionFromOpenToHalfOpenEnabled: true
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- org.springframework.web.client.HttpServerErrorException
ignoreExceptions:
- com.example.BusinessException
retry:
instances:
paymentService:
maxAttempts: 3
waitDuration: 1s
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- java.io.IOException
bulkhead:
instances:
paymentService:
maxConcurrentCalls: 20
maxWaitDuration: 500ms
timelimiter:
instances:
paymentService:
timeoutDuration: 3s
cancelRunningFuture: true
서비스 구현
@Service
@Slf4j
public class PaymentService {
private final RestTemplate restTemplate;
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
@Retry(name = "paymentService")
@Bulkhead(name = "paymentService")
@TimeLimiter(name = "paymentService")
public CompletableFuture<PaymentResponse> processPayment(PaymentRequest request) {
log.info("Processing payment for order: {}", request.getOrderId());
PaymentResponse response = restTemplate.postForObject(
"http://payment-service/api/v1/payments",
request,
PaymentResponse.class
);
return CompletableFuture.completedFuture(response);
}
// 폴백 메서드 — 서킷 OPEN 시 호출
private CompletableFuture<PaymentResponse> paymentFallback(
PaymentRequest request, Exception ex) {
log.warn("Payment circuit breaker activated for order: {}. Reason: {}",
request.getOrderId(), ex.getMessage());
// 전략 1: 큐에 넣고 나중에 재시도
paymentRetryQueue.add(request);
// 전략 2: 기본 응답 반환
return CompletableFuture.completedFuture(
PaymentResponse.builder()
.orderId(request.getOrderId())
.status(PaymentStatus.PENDING)
.message("Payment is being processed. You will receive confirmation shortly.")
.build()
);
}
}
이벤트 모니터링
@Component
public class CircuitBreakerEventListener {
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
@PostConstruct
public void registerEventListeners() {
CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("paymentService");
cb.getEventPublisher()
.onStateTransition(event -> {
log.warn("Circuit Breaker '{}' state transition: {} → {}",
event.getCircuitBreakerName(),
event.getStateTransition().getFromState(),
event.getStateTransition().getToState());
// Slack/PagerDuty 알림
if (event.getStateTransition().getToState() ==
CircuitBreaker.State.OPEN) {
alertService.sendAlert(
"CRITICAL: Payment circuit breaker OPEN!");
}
})
.onError(event ->
log.error("Circuit Breaker error: {} (duration: {}ms)",
event.getThrowable().getMessage(),
event.getElapsedDuration().toMillis())
)
.onSuccess(event ->
log.debug("Circuit Breaker success (duration: {}ms)",
event.getElapsedDuration().toMillis())
);
}
}
Python 구현 (pybreaker)
import pybreaker
import requests
from functools import wraps
# 서킷 브레이커 생성
payment_breaker = pybreaker.CircuitBreaker(
fail_max=5, # 5번 실패 시 OPEN
reset_timeout=30, # 30초 후 HALF-OPEN
exclude=[ValueError], # 비즈니스 예외 제외
)
# 리스너 등록
class CircuitBreakerListener(pybreaker.CircuitBreakerListener):
def state_change(self, cb, old_state, new_state):
print(f"Circuit '{cb.name}': {old_state.name} → {new_state.name}")
if new_state == pybreaker.STATE_OPEN:
send_slack_alert(f"⚠️ {cb.name} circuit OPEN!")
def failure(self, cb, exc):
print(f"Circuit '{cb.name}' failure: {exc}")
payment_breaker.add_listener(CircuitBreakerListener())
# 데코레이터로 사용
@payment_breaker
def process_payment(order_id: str, amount: float) -> dict:
response = requests.post(
"http://payment-service/api/v1/payments",
json={"order_id": order_id, "amount": amount},
timeout=3
)
response.raise_for_status()
return response.json()
# 폴백 포함 래퍼
def process_payment_safe(order_id: str, amount: float) -> dict:
try:
return process_payment(order_id, amount)
except pybreaker.CircuitBreakerError:
# 서킷 OPEN 상태 → 즉시 폴백
return {
"order_id": order_id,
"status": "PENDING",
"message": "Payment queued for retry"
}
except requests.RequestException as e:
# 네트워크 에러 → 서킷에 실패 기록됨
return {
"order_id": order_id,
"status": "FAILED",
"error": str(e)
}
폴백 전략 패턴
1. 캐시 폴백
private PaymentResponse paymentCacheFallback(PaymentRequest req, Exception ex) {
// 마지막으로 성공한 응답을 캐시에서 반환
return cache.getIfPresent("payment:" + req.getOrderId());
}
2. 기본값 반환
private List<Product> productFallback(String category, Exception ex) {
// 기본 추천 상품 반환
return defaultProducts.getByCategory(category);
}
3. 대체 서비스 호출
private PaymentResponse paymentBackupFallback(PaymentRequest req, Exception ex) {
// 백업 결제 서비스 호출
return backupPaymentService.process(req);
}
4. 큐잉 후 비동기 처리
private PaymentResponse paymentQueueFallback(PaymentRequest req, Exception ex) {
// 메시지 큐에 넣고 나중에 처리
kafkaTemplate.send("payment-retry", req);
return PaymentResponse.pending(req.getOrderId());
}
Resilience 패턴 조합
// 적용 순서 (외부 → 내부):
// Retry → CircuitBreaker → RateLimiter → TimeLimiter → Bulkhead
@Retry(name = "service") // 3. 실패 시 재시도
@CircuitBreaker(name = "service") // 2. 실패율 모니터링
@Bulkhead(name = "service") // 1. 동시성 제한
public Response callExternalService() {
// ...
}
요청 흐름:
[요청] → Bulkhead (동시 20개 제한)
→ CircuitBreaker (실패율 모니터링)
→ Retry (실패 시 최대 3회 재시도)
→ TimeLimiter (3초 타임아웃)
→ [외부 서비스 호출]
Prometheus 메트릭
# application.yml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
distribution:
percentiles-histogram:
resilience4j.circuitbreaker.calls: true
# 서킷 브레이커 상태
resilience4j_circuitbreaker_state{name="paymentService"}
# 0=CLOSED, 1=OPEN, 2=HALF_OPEN
# 실패율
resilience4j_circuitbreaker_failure_rate{name="paymentService"}
# 호출 통계
rate(resilience4j_circuitbreaker_calls_total{name="paymentService",kind="successful"}[5m])
rate(resilience4j_circuitbreaker_calls_total{name="paymentService",kind="failed"}[5m])
# 느린 호출 비율
resilience4j_circuitbreaker_slow_call_rate{name="paymentService"}
테스트
@SpringBootTest
class CircuitBreakerTest {
@Autowired
private CircuitBreakerRegistry registry;
@Test
void shouldOpenCircuitAfterFailures() {
CircuitBreaker cb = registry.circuitBreaker("paymentService");
// CLOSED 상태 확인
assertThat(cb.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
// 5번 실패 시뮬레이션
for (int i = 0; i < 5; i++) {
cb.onError(0, TimeUnit.MILLISECONDS, new IOException("timeout"));
}
// OPEN 상태로 전환 확인
assertThat(cb.getState()).isEqualTo(CircuitBreaker.State.OPEN);
// OPEN 상태에서 즉시 실패
assertThatThrownBy(() ->
cb.decorateSupplier(() -> "test").get()
).isInstanceOf(CallNotPermittedException.class);
}
}
퀴즈
Q1. 서킷 브레이커의 세 가지 상태는?
CLOSED(정상, 모든 호출 허용), OPEN(차단, 즉시 실패), HALF-OPEN(제한적 시험 호출 허용)입니다.
Q2. failureRateThreshold: 50의 의미는?
슬라이딩 윈도우 내에서 실패율이 50% 이상이면 서킷이 OPEN 상태로 전환됩니다.
Q3. Resilience4j의 어노테이션 적용 순서는?
외부에서 내부로: Retry → CircuitBreaker → RateLimiter → TimeLimiter → Bulkhead 순서로 적용됩니다.
Q4. 서킷 브레이커 없이 마이크로서비스에서 발생하는 문제는?
연쇄 장애(Cascading Failure)입니다. 하나의 느린 서비스가 호출 서비스의 스레드를 고갈시켜 전체 시스템이 마비됩니다.
Q5. HALF-OPEN 상태에서 시험 호출이 모두 성공하면?
서킷이 CLOSED 상태로 복귀하여 정상적으로 모든 호출을 허용합니다.
Q6. Bulkhead 패턴의 역할은?
동시 호출 수를 제한하여, 하나의 서비스 호출이 모든 스레드를 소진하는 것을 방지합니다. 선박의 격벽(Bulkhead)처럼 장애를 격리합니다.
Q7. 폴백 전략 4가지를 나열하세요.
캐시 폴백 (마지막 성공 응답), 기본값 반환, 대체 서비스 호출, 큐잉 후 비동기 처리의 4가지입니다.
마무리
서킷 브레이커 패턴은 마이크로서비스 아키텍처에서 필수적인 장애 격리 메커니즘입니다. Resilience4j는 가볍고 모듈러한 구현을 제공하며, Retry, Bulkhead, TimeLimiter 등 다른 레질리언스 패턴과 조합하여 견고한 분산 시스템을 구축할 수 있습니다.
참고 자료
현재 단락 (1/247)
마이크로서비스 아키텍처에서 서비스 간 호출은 필연적입니다. 하지만 하나의 서비스가 느려지거나 응답을 하지 않으면, 호출하는 서비스까지 연쇄적으로 장애가 전파됩니다. **서킷 브레이...