서킷 브레이커(Circuit Breaker)

김뉴오·2026년 1월 21일

키워드

목록 보기
16/16

1. 두꺼비 집과 서킷 브레이커 관계

  • 집에 있는 두꺼비집은 과도한 전류가 흐를 경우 회로와 기기를 보호하기 위해 전류를 차단하는 장치
  • 두꺼비 집의 작동 방식은
    • Closed 상태 : 전류가 흐름
    • Open 상태 : 전류가 차단
  • 두꺼비집은 전기 회로 차단기로, 영어로 Circuit Breaker
  • 소프트웨어의 서킷 브레이커 패턴은 이러한 전기 회로 차단기의 작동 원리에서 영감을 받아 만들어짐

2. 서킷 브레이커 패턴 소개

  • 시스템의 회복 탄력성과 내결합성을 높이기 위한 패턴
    • 회복 탄력성: 장애 발생 후 정상 상태로 빠르게 복구하는 능력
    • 내결함성: 일부 컴포넌트에 장애가 발생해도 전체 시스템이 중단되지 않도록 버티는 능력
  • 즉, 다른 서비스에서 장애가 발생하더라도 문제가 전체 시스템으로 전파되지 않도록 차단하는 것이 서킷 브레이커의 핵심 목적

3. MSA에서 발생할 수 있는 상황

문제 상황

  • 결제 서비스가 외부 PG 서버에 의존
  • PG 서버에 장애 발생
  • 클라이언트가 요청을 보냈지만 응답을 받지 못함
  • 따라서 클라이언트는 응답을 받지 못해 주문하기 버튼을 반복 클릭
  • 장애가 난 PG 서버로 요청이 계속 누적
  • 이를 호출한 주문 서비스·결제 서비스의 쓰레드 / 커넥션 풀이 고갈
  • 결과적으로 결제 서비스 → 주문 서비스 → 전체 서비스로 장애 확산

결과적으로 서비스 가용성이 급격히 낮아짐

아래와 같은 상황을 해결하기 위해 서킷 브레이커를 사용해야함

sequenceDiagram
    autonumber
    actor C as Client
    participant O as Order Service
    participant P as Payment Service
    participant PG as External PG Server

    
  

    %% ===== 서버 응답 실패 =====
  
        P--x O: 결제 응답 지연/실패 (타임아웃)
        O--x C: 클라이언트 응답 지연/실패

    Note over C: 응답을 못 받아서<br/>주문하기 버튼 반복 클릭 (재시도)

    %% ===== 반복 요청으로 장애 심화 =====
    rect rgba(255, 0, 0, 0.15)
        loop 사용자 반복 클릭 / 클라이언트 재시도
            C->>O: 주문하기 재요청
            O->>P: 결제 재요청
            P->>PG: 결제 승인 재요청
            PG--x P: No Response / Timeout
        end
    end

    %% ===== 서버 자원 고갈 =====
   
        Note over P: Payment 쓰레드 / 커넥션 풀 고갈
        Note over O: Order 서비스 요청 대기 누적
        Note over C,O: 장애가 연쇄적으로 확산<br/>(전체 서비스 가용성 급격히 저하)
   
  

서킷 브레이커 적용 시

  • PG 서버 장애와 같은 장애 서비스로의 요청 즉시 차단
  • 호출한 서비스 불필요한 자원 소모 방지
  • 클라이언트에게 Fallback 제공
sequenceDiagram
    autonumber
    actor C as Client
    participant O as Order Service
    participant P as Payment Service
    participant CB as Circuit Breaker
    participant PG as External PG Server

    Note over PG: PG 장애 발생(지연/타임아웃)

    C->>O: 주문하기 요청
    O->>P: 결제 요청
    P->>CB: PG 호출 시도
    CB->>PG: 결제 승인 요청

    PG--x CB: (No Response / Timeout)
    CB-->>P: 실패 기록(에러/타임아웃 카운트)

    Note over CB: 임계치 초과 시 OPEN 전환
    CB-->>P: (OPEN) 즉시 실패 반환
    P-->>O: 빠른 실패 응답 + Fallback(예: 결제 실패/대기 안내)
    O-->>C: 즉시 응답(빠른 실패/안내)

    Note over P,O: 외부 장애 동안에도<br/>내부 쓰레드/커넥션 보호

  

4. 서킷 브레이커 작동 방식

CLOSED 상태

서킷 브레이커가 닫혀 있음

  • 초기 상태이자 정상 동작 상태
  • 모든 서비스 요청이 정상적으로 수행됨
  • 요청 성공/실패/지연을 지속적으로 기록
  • 특정 기간 동안 실패율이 임계값(threshold) 을 초과하면
→ **OPEN 상태로 전환**

OPEN 상태

  • 실패율이 임계값을 초과한 상태
  • 장애 서비스로의 모든 요청을 즉시 차단
  • 실제 호출 없이 바로 예외 반환 → Fail Fast
  • 설정된 대기 시간(waitDurationInOpenState)이 지나면 → HALF_OPEN 상태로 전환

HALF_OPEN 상태

  • 장애 서비스가 복구되었는지 확인하기 위한 탐색 상태
  • 제한된 횟수의 테스트 요청만 허용
    • 테스트 요청 결과에 따라 상태 결정
    • 실패율이 다시 임계치 이상 → OPEN
    • 정상 응답 비율이 높음 → CLOSED

5. FallBack

서킷 브레이커에서 중요한 개념 중 개념

  • 다른 서비스 호출이 실패했을 때, 우리 서비스가 스스로 선택하는 대체 행동
  • 서킷 브레이커가 OPEN일 때만 실행되는 게 아니라,
    • 호출 실패
    • 타임아웃
    • 예외 발생
    등 **정상 응답을 받지 못한 모든 경우에 실행될 수 있음**
    

Fallback은 이 호출이 정상 결과를 못 냈을 때 실행되는 대체 로직

즉, 서킷 브레이커 상태와는 직접적인 1:1 관계가 아님.

FallBack 종류

  1. Fail - Fast
  • 대체 결과가 의미 없거나 위험할 때는 명확하고 빠르게 즉시 실패
  • 장단점 :
    • 장 : 정합성·안전성 확보
    • 단 : 사용자 이탈 가능성
  1. 비동기 처리로 전환
  • 동기 호출 대신 요청을 접수만 하고 나중에 처리
  • 이벤트 발행 후 재시도 / 보상 트랜잭션으로 처리
  • 장단점 :
    • 장 : 시스템 마비 방지, 비즈니스 연속성
    • 단 : 상태 관리 복잡성 증가
  1. 캐시/스냅샷 데이터 변환
  • 최근 성공 결과를 Redis 또는 로컬 캐시에 저장해두고 실패 시 캐시 저장 결과를 반환
  • 장단점 :
    • 장 : 트래픽 방어, 사용자 경험 좋음
    • 정합성 이슈

어느 서비스에 어떤 Fallback를 사용하는지 중요

Fallback 사용 예시

  1. Order → Payment 결제 요청에서의 Fallback
  • 문제 상황
    • PG 서버 장애
    • Payment Service 응답 지연
    • Order Service의 쓰레드/커넥션 점유
  • Fail Fast Fallback
    • Payment 호출 차단 → 즉시 실패 응답 → Order 생성 안 함
    • 응답 예 : “현재 결제 서비스가 불안정합니다. 잠시 후 다시 시도해주세요”
    • 장 : 결제/정합성 매우 안전
    • 단 : 사용자 이탈 가능
  • 비동기 전환 Fallback
    • Order 생성 (PAYMENT_PENDING) → 결제 요청 이벤트 Outbox에 저장 → Kafka로 비동기 결제 시도
  • 캐시/스냅샷 Fallback
    • 결제 도메인에서는 사용 불가 (정합성 문제)

Fallback 선택 기준

정합성이 중요한 영역

  • 결제, 재고, 좌석 확정
  • 잘못된 정보/중복 처리 리스크가 커서
  • Fail Fast 또는 비동기 전환이 안전

UX가 중요한 영역

  • 추천, 통계, 부가 정보
  • → 캐시 / 기본값 Fallback 효과적

Fallback 설계할 때 꼭 지켜야 하는 것

  • fallback 자체는 가볍고 빨라야 함
    • “실패했는데 fallback에서도 DB 풀스캔” 하면 더 망함
  • 무한 재시도 금지
    • fallback 안에서 다시 같은 외부 API를 호출하면 의미 없음
  • 관측 가능하게(로그/메트릭)
    • “fallback 얼마나 발생했는지”가 운영에 핵심 지표

6. Resilience4j

  • 서킷 브레이커를 제공하는 Java 라이브러리
  • Circuit Breaker 외에도 서비스 내결함성을 향상시키기 위한 패턴으로 Retry, Bulkhead, RateLimiter, TimeLimiter, Cache 모듈을 제공한다.
💡

Retry : 요청 실패 시 재시도 처리 기능 제공
RateLimiter : 제한치를 넘어서 요청을 거부하거나 Queue 생성하여 처리하는 기능 제공
TimeLimiter : 실행 시간제한 설정 기능 제공
Bulkhead : 동시 실행 횟수 제한 기능 제공

Netflix Hystrix

  • 현재는 새롭게 개발을 진행하지 않아서 Resilience4j 사용을 권장

7. Popcorn 프로젝트 Resilience4j 적용

결제 시스템에서 TossPayments API 의존성이 높아 서킷 브레이커가 필요

1. 먼저, Resilience4j 라이브러리 의존성 주입

dependencies {
	implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
	implementation 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0'
	implementation 'io.github.resilience4j:resilience4j-retry:2.2.0'
	implementation 'io.github.resilience4j:resilience4j-ratelimiter:2.2.0'
	implementation 'io.github.resilience4j:resilience4j-bulkhead:2.2.0'
	implementation 'io.github.resilience4j:resilience4j-timelimiter:2.2.0'
}
  • 서킷 브레이커 외에 추가적으로 retry, ratelimiter, bulkhead, timelimiter 사용

2. application.yml 설정

resilience4j:
  circuitbreaker:
    instances:
      tossPaymentApi:                        # 서킷브레이커의 이름
        failure-rate-threshold: 50           # 실패 비율의 임계치
        slow-call-rate-threshold: 80         # 느린 호출의 임계치
        slow-call-duration-threshold: 2000ms # 느린 호출로 간주할 시간 값
        minimum-number-of-calls: 5           # 총 집계가 유효해 지는 최소한의 요청 수
        sliding-window-type: COUNT_BASED     # 서킷브레이커의 타입을 지정한다. TIME_BASED, COUNT_BASED 중 택 1
        sliding-window-size: 10                      # 시간은 단위 초, 카운트는 단위 요청 수
        wait-duration-in-open-state: 30s     # OPEN에서 HALF_OPEN으로 상태변이가 실행되는 최소 대기 시간
        permitted-number-of-calls-in-half-open-state: 3  # HALF_OPEN 상태에서 총 집계가 유효해지는 최소한의 요청 수
        automatic-transition-from-open-to-half-open-enabled: true
        register-health-indicator: true      # 해당 값에 기재한 exception은 모두 실패로 집계

  retry:
    instances:
      tossPaymentApi:                        # 🎯 TossPayments API 재시도
        max-attempts: 2                      # 최대 2회 시도 (원본 + 재시도 1회)
        wait-duration: 500ms                 # 재시도 간격 500ms
        enable-exponential-backoff: true     # 지수 백오프 활성화
        exponential-backoff-multiplier: 2    # 500ms → 1000ms

  timelimiter:
    instances:
      tossPaymentApi:                      A  # 🎯 TossPayments PI 타임아웃
        timeout-duration: 5s                 # 외부 API 5초 타임아웃
        cancel-running-future: true          # 타임아웃 시 Future 취소

resilience4j:
  circuitbreaker:
    instances:
      tossPaymentApi:                       
        failure-rate-threshold: 50         
        slow-call-rate-threshold: 80         
        slow-call-duration-threshold: 2000ms 
        minimum-number-of-calls: 5           
        sliding-window-type: COUNT_BASED     
        sliding-window-size: 10              
        wait-duration-in-open-state: 30s     
        permitted-number-of-calls-in-half-open-state: 3  
        automatic-transition-from-open-to-half-open-enabled: true
        register-health-indicator: true     

  retry:
    instances:
      tossPaymentApi:                        
        max-attempts: 2                     
        wait-duration: 500ms                 
        enable-exponential-backoff: true    
        exponential-backoff-multiplier: 2    

  timelimiter:
    instances:
      tossPaymentApi:                      
        timeout-duration: 5s                
        cancel-running-future: true          

// retry -> circuitbreaker -> timelimiter 순서로 작동 //


<aside>
💡

주요 설정값

- failureRateThreshold : 호출 실패율에 대한 임곗값, sliding window 상에서 임곗값보다 실패율이 높아지면 상태가 OPEN
- slow-call-rate-threshold:  느린 호출의 임계치
- slow-call-duration-threshold: 느린 호출로 간주할 시간 값
- slidingWindowType: siliding window의 형태를 COUNT_BASED 로 설정할지, TIME_BASED로 설정할지 결정. 타입에 따라서 slidingWindowSize의 숫자가 의미하는 것이 호출 횟수 혹은 시간이 됨
- slidingWindowSize: sliding window의 크기를 설정.  타입에 따라서 slidingWindowSize의 숫자가 의미하는 것이 호출 횟수 혹은 시간이 됨
- waitDurationInOpenState: OPEN 상태에서 얼마나 기다린 후에, HALF_OPEN으로 전환할지를 설정
</aside>

- CLOSED 상태
    - 최소 5번의 요청 후 판단
    - 외부 API 50% 실패 시 OPEN 상태로 변환
    - 2초 이상 = 느린 호출이 80% 시  OPEN 상태로 변환
- OPEN 상태
    - 30초 후 복구 시도
    - 복구 시 3번 테스트

### 3.  TossPaymentsClient 구현

```java
@Component
@Slf4j
public class TossPaymentsClient {

    /**
     * 결제 승인 API 호출 (Circuit Breaker 적용)
     */
    @CircuitBreaker(name = "tossPaymentApi", fallbackMethod = "confirmFallback")
    public TossPaymentsConfirmResponse confirm(TossPaymentsConfirmRequest request) {
        log.debug("🎯 Toss Payment API 호출 - 결제 승인: {}", request.getOrderId());

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set(HttpHeaders.AUTHORIZATION, buildAuthorizationHeader());
        HttpEntity<TossPaymentsConfirmRequest> entity = new HttpEntity<>(request, headers);

        String url = properties.getBaseUrl() + "/v1/payments/confirm";
        return restTemplate.postForObject(url, entity, TossPaymentsConfirmResponse.class);
    }

    /**
     * 결제 취소 API 호출 (Circuit Breaker 적용)
     */
    @CircuitBreaker(name = "tossPaymentApi", fallbackMethod = "cancelFallback")
    public TossPaymentsCancelResponse cancel(String paymentKey, TossPaymentsCancelRequest request) {
        log.debug("🎯 Toss Payment API 호출 - 결제 취소: {}", paymentKey);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set(HttpHeaders.AUTHORIZATION, buildAuthorizationHeader());
        HttpEntity<TossPaymentsCancelRequest> entity = new HttpEntity<>(request, headers);

        String url = properties.getBaseUrl() + "/v1/payments/" + paymentKey + "/cancel";
        return restTemplate.postForObject(url, entity, TossPaymentsCancelResponse.class);
    }

    /**
     * 결제 승인 Circuit Breaker Fallback
     *
     * 🎯 기존 동작 유지: 예외를 그대로 던져서 상위 레이어에서 처리
     */
    public TossPaymentsConfirmResponse confirmFallback(TossPaymentsConfirmRequest request,
     Exception ex) {
        log.error("🚨 Toss Payment API Circuit Breaker 열림 - 결제 승인 실패: orderId={}, error={}",
            request.getOrderId(), ex.getMessage());
        
        // 기존 동작 유지: 원본 예외를 그대로 던짐                                            
        if (ex instanceof RuntimeException) {
            throw (RuntimeException) ex;
        }
        throw new RuntimeException("결제 API 서비스가 불안정합니다. 잠시 후 다시 시도해주세요.", ex);
    }

    /**
     * 결제 취소 Circuit Breaker Fallback
     *
     * 🎯 기존 동작 유지: 예외를 그대로 던져서 상위 레이어에서 처리
     */
    public TossPaymentsCancelResponse cancelFallback(String paymentKey, TossPaymentsCancelRequest request, Exception ex) {
        log.error("🚨 Toss Payment API Circuit Breaker 열림 - 결제 취소 실패: paymentKey={}, error={}",
            paymentKey, ex.getMessage());

        // 기존 동작 유지: 원본 예외를 그대로 던짐
        if (ex instanceof RuntimeException) {
            throw (RuntimeException) ex;
        }
        throw new RuntimeException("결제 취소 API 서비스가 불안정합니다. 잠시 후 다시 시도해주세요.", ex);
    }

}
  • 기존 동작은 완전히 그대로 유지하면서 Circuit Breaker 어노테이션만 추가
  • 아래 Fallback 동작 구현
    • Fail - Fast 방식을 사용
    • 즉각 적으로 실패 응답을 반환하여 안정성을 높임

8. 서킷 브레이커 도입 시 고려 사항

전체 서비스 vs 특정 서비스만 호출?

  • 장애의 원인은 항상 호출 단위에서 발생
  • 특정 호출 하나가 문제를 일으킴
  • 하지만 전체 서비스에 서킷 브레이크를 적용하면 과잉 차단 (Over-blocking)
  • 호출이 느려지거나 실패했을 때 우리 서비스가 위험을 고려하여 정해야함

서킷 브레이커 설정 모두 통일?

  • 서킷 브레이커 사용 해야하는 서비스 특징에 따라 다르게 설정해야함
  • API Gateway
    • 장애 전파 위험 큼
    • 민감한 임계값, 짧은 복구 시간
  • 결제 서비스
    • 외부 서비스 의존
    • 보수적인 임계값, 긴 복구 시간 설정 가능
profile
Bello! NewOld velog~

0개의 댓글