프로그래밍/디자인패턴

서킷 브레이커 패턴(Circuit Breaker Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 19:47

"결제 서비스가 응답하지 않아서 주문 서비스 스레드가 다 묶여버렸고, 결국 전체 시스템이 다운되었습니다." (연쇄 장애, Cascading Failure)

우리 집 두꺼비집(누전 차단기)은 전력 과부하가 걸리면 딱! 하고 내려가서 가전제품을 보호합니다. 소프트웨어에서도 특정 서비스의 에러율이 치솟으면, 즉시 연결을 차단(Open)하고 '잠시 후에 다시 시도해봐'라고 빠르게 실패(Fail Fast) 처리하는 것이 서킷 브레이커의 핵심입니다.

중요한 건 "도대체 언제 차단기를 내릴 것인가?" 입니다.


1. 기본 개념: 3가지 상태

서킷 브레이커는 신호등처럼 3가지 상태를 오가며 시스템을 보호합니다.

  1. Closed (닫힘/정상): 전기가 통하듯 정상적으로 요청을 보냅니다.
  2. Open (열림/차단): 에러율이 임계치를 넘으면 차단기가 내려갑니다. 요청을 보내지 않고 바로 에러(혹은 Fallback)를 반환합니다.
  3. 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) 기반 슬라이딩 윈도우를 사용하여, **"지금 당장 장애가 났는지"**를 민감하게 감지하는 것이 시스템 전체의 안전을 위해 유리합니다.