"결제 서비스가 응답하지 않아서 주문 서비스 스레드가 다 묶여버렸고, 결국 전체 시스템이 다운되었습니다." (연쇄 장애, Cascading Failure)
우리 집 두꺼비집(누전 차단기)은 전력 과부하가 걸리면 딱! 하고 내려가서 가전제품을 보호합니다. 소프트웨어에서도 특정 서비스의 에러율이 치솟으면, 즉시 연결을 차단(Open)하고 '잠시 후에 다시 시도해봐'라고 빠르게 실패(Fail Fast) 처리하는 것이 서킷 브레이커의 핵심입니다.
중요한 건 "도대체 언제 차단기를 내릴 것인가?" 입니다.
1. 기본 개념: 3가지 상태
서킷 브레이커는 신호등처럼 3가지 상태를 오가며 시스템을 보호합니다.
- Closed (닫힘/정상): 전기가 통하듯 정상적으로 요청을 보냅니다.
- Open (열림/차단): 에러율이 임계치를 넘으면 차단기가 내려갑니다. 요청을 보내지 않고 바로 에러(혹은 Fallback)를 반환합니다.
- Half-Open (반쯤 열림): 일정 시간이 지난 후, "이제 살았나?" 하고 간보기 위해 요청을 몇 개만 흘려보냅니다. 성공하면 Closed, 실패하면 다시 Open 됩니다.
2. 방식 1: 횟수 기반 슬라이딩 윈도우 (Count-based Sliding Window)
가장 직관적인 방식입니다. **"최근 N번의 요청 중 M번 실패하면 차단한다"**는 논리입니다.
특징
- 배열(Array) 관리: 최근 100개의 요청 결과를 원형 배열(Circular Array)에 저장합니다.
- 트래픽 무관: 요청이 드문드문 와도, 최근 100개를 채울 때까지의 결과를 봅니다.
설정 예시 (Resilience4j / YAML)
YAML
resilience4j:
circuitbreaker:
instances:
paymentService:
slidingWindowType: COUNT_BASED # 횟수 기반
slidingWindowSize: 10 # 최근 10개 요청만 기억
failureRateThreshold: 50 # 50% (5개) 이상 실패하면 차단
장단점
- 장점: 이해하기 쉽고, 트래픽이 적은 서비스에서도 "확실하게 N번 실패하면" 동작하므로 예측 가능합니다.
- 단점: 트래픽이 갑자기 폭주할 때, 통계의 표본이 너무 빨리 물갈이되어 정확한 에러율 판단이 어려울 수 있습니다.
3. 방식 2: 시간 기반 슬라이딩 윈도우 (Time-based Sliding Window)
현대적인 MSA 환경(특히 Resilience4j)에서 선호하는 방식입니다. **"최근 N초 동안 들어온 요청 중 M%가 실패하면 차단한다"**는 논리입니다.
특징
- 버킷(Bucket) 관리: 시간을 N개의 버킷으로 쪼개서(예: 1초 단위) 성공/실패 횟수를 집계합니다.
- 오래된 데이터 만료: 설정한 시간이 지나면 과거 데이터는 자연스럽게 통계에서 빠집니다.
설정 예시 (Resilience4j / YAML)
YAML
resilience4j:
circuitbreaker:
instances:
paymentService:
slidingWindowType: TIME_BASED # 시간 기반
slidingWindowSize: 10 # 최근 10초 동안의 데이터를 봄
minimumNumberOfCalls: 5 # 최소 5번은 요청이 와야 판단 시작
failureRateThreshold: 50 # 에러율 50% 넘으면 차단
장단점
- 장점: 트래픽이 들쑥날쑥해도 **"현재 시점의 장애 상황"**을 더 정확하게 반영합니다. 대용량 트래픽 처리에 유리합니다.
- 단점: 내부적으로 스레드 안전하게 시간을 관리해야 하므로 구현 복잡도가 약간 더 높습니다(라이브러리가 해주니 걱정 뚝).
4. 실무 예시: Spring Boot와 적용
과거에는 Netflix Hystrix를 썼지만, 현재는 유지보수가 중단되었습니다. 지금은 Resilience4j가 표준입니다.
Service 코드 적용 (@CircuitBreaker)
Java
@Service
@RequiredArgsConstructor
public class OrderService {
private final PaymentClient paymentClient;
// "paymentService"라는 이름의 서킷 설정을 적용
// 에러 나면 fallbackMethod 실행
@CircuitBreaker(name = "paymentService", fallbackMethod = "payFallback")
public void order(Order order) {
paymentClient.pay(order); // 외부 호출
}
// 대체 로직 (Fallback)
public void payFallback(Order order, Throwable t) {
log.error("결제 서비스 장애 발생: {}", t.getMessage());
// "잠시 후에 다시 시도해주세요" 메시지 반환 or 임시 저장
}
}
요약: 무엇을 선택해야 할까?
| 구분 | Count-based (횟수 기반) | Time-based (시간 기반) |
| 기준 | 최근 N개의 요청 | 최근 N초 동안의 요청 |
| 민감도 | 요청 횟수가 찰 때까지 기다림 | 시간이 지나면 데이터가 갱신됨 |
| 메모리 | 고정 크기 배열 사용 (적음) | 시간 버킷 관리 (약간 더 큼) |
| 추천 상황 | 호출 빈도가 낮은 배치(Batch) 작업이나 내부 툴 | 트래픽이 많은 대고객 서비스 API |
결론
호출 빈도가 드문드문 있는 API라면 횟수(Count) 기반이 낫습니다. 1시간에 1번 호출되는데 시간 기반(최근 10초)으로 설정하면 영원히 차단되지 않을 수 있으니까요.
하지만 초당 수십~수백 건의 요청이 들어오는 메인 비즈니스 로직이라면 시간(Time) 기반 슬라이딩 윈도우를 사용하여, **"지금 당장 장애가 났는지"**를 민감하게 감지하는 것이 시스템 전체의 안전을 위해 유리합니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| API 게이트웨이 패턴(API Gateway) 완벽 정리 (0) | 2025.12.07 |
|---|---|
| 캐싱 패턴(Caching Pattern) 완벽 정리 (0) | 2025.12.07 |
| ORM 패턴(ORM Pattern) 완벽 정리 (0) | 2025.12.07 |
| 사가 패턴(Saga Pattern) 완벽 정리: 코레오그래피(Choreography) vs 오케스트레이션(Orchestration) (0) | 2025.12.07 |
| 페이지네이션 패턴(Pagination Pattern) 완벽 정리 (0) | 2025.12.07 |