🏗️ 아키텍처

Circuit Breaker

서킷 브레이커 패턴

마이크로서비스 아키텍처에서 장애 전파를 방지하는 핵심 패턴입니다. 연속된 실패가 임계값에 도달하면 요청을 즉시 차단하고, 시스템 복구 후 자동으로 트래픽을 재개합니다.

📖 상세 설명

Circuit Breaker(서킷 브레이커)는 전기 회로의 차단기에서 영감을 받은 분산 시스템 패턴입니다. 과전류가 흐르면 차단기가 회로를 끊어 화재를 방지하듯, 서비스 호출이 반복적으로 실패하면 자동으로 요청을 차단하여 전체 시스템의 연쇄 장애(Cascading Failure)를 방지합니다.

3가지 상태(State)로 동작합니다. Closed(닫힘) 상태에서는 정상적으로 요청을 전달하며 실패율을 모니터링합니다. 실패가 임계값을 초과하면 Open(열림) 상태로 전환되어 모든 요청을 즉시 실패 처리(Fail Fast)합니다. 일정 시간이 지나면 Half-Open(반열림) 상태가 되어 제한된 요청으로 서비스 복구 여부를 확인합니다.

주요 파라미터로는 실패 임계값(Failure Threshold), 타임아웃(Timeout), 재시도 대기 시간(Wait Duration), 반열림 시 허용 요청 수(Permitted Calls in Half-Open) 등이 있습니다. 이 값들은 서비스 특성과 SLA에 맞게 튜닝해야 합니다.

폴백(Fallback) 전략과 함께 사용됩니다. 서킷이 Open 상태일 때 캐시된 데이터 반환, 기본값 제공, 대체 서비스 호출 등의 우아한 성능 저하(Graceful Degradation)를 구현할 수 있습니다. 이를 통해 사용자 경험을 유지하면서 시스템을 보호합니다.

주요 구현체로는 Java의 Resilience4j, Netflix Hystrix(현재 유지보수 모드), Python의 pybreaker, Node.js의 opossum 등이 있습니다. Kubernetes 환경에서는 Istio, Envoy 같은 서비스 메시에서 인프라 수준의 서킷 브레이커를 제공합니다.

💻 코드 예제

# Circuit Breaker 패턴 - 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]     # ValueError는 실패로 카운트 안함
)

# 상태 변경 리스너
class BreakerListener(pybreaker.CircuitBreakerListener):
    def state_change(self, cb, old_state, new_state):
        print(f"🔄 Circuit Breaker 상태 변경: {old_state.name} → {new_state.name}")
        if new_state.name == "open":
            # 알림 발송 (Slack, PagerDuty 등)
            send_alert(f"결제 서비스 서킷 Open! 즉시 확인 필요")

    def failure(self, cb, exc):
        print(f"❌ 실패 감지: {exc}, 현재 실패 횟수: {cb.fail_counter}")

    def success(self, cb):
        print(f"✅ 성공, 실패 카운터 리셋")

payment_breaker.add_listener(BreakerListener())


class PaymentService:
    """결제 서비스 - Circuit Breaker 적용"""

    def __init__(self, base_url: str):
        self.base_url = base_url
        self.cache = {}  # 폴백용 캐시

    @payment_breaker
    def process_payment(self, order_id: str, amount: int) -> dict:
        """
        결제 처리 - 서킷 브레이커로 보호됨
        """
        response = requests.post(
            f"{self.base_url}/payments",
            json={"order_id": order_id, "amount": amount},
            timeout=5
        )
        response.raise_for_status()
        return response.json()

    def process_payment_with_fallback(self, order_id: str, amount: int) -> dict:
        """
        폴백이 있는 결제 처리
        """
        try:
            result = self.process_payment(order_id, amount)
            # 성공 시 캐시 갱신
            self.cache[order_id] = result
            return result

        except pybreaker.CircuitBreakerError:
            # 서킷 Open 상태 - 즉시 폴백
            print(f"⚡ 서킷 Open 상태, 폴백 실행")
            return self._fallback_payment(order_id, amount)

        except requests.exceptions.RequestException as e:
            # 네트워크 에러 - 실패로 카운트됨
            print(f"🌐 네트워크 에러: {e}")
            raise

    def _fallback_payment(self, order_id: str, amount: int) -> dict:
        """
        폴백: 결제를 큐에 저장하고 나중에 처리
        """
        pending_payment = {
            "order_id": order_id,
            "amount": amount,
            "status": "pending",
            "message": "결제 서비스 일시 불가. 잠시 후 자동 처리됩니다."
        }
        # 메시지 큐에 저장 (실제로는 Redis, RabbitMQ 등)
        self._save_to_queue(pending_payment)
        return pending_payment

    def _save_to_queue(self, payment: dict):
        print(f"📥 결제 대기열에 저장: {payment['order_id']}")


# 수동 서킷 제어 (점검 모드 등)
def maintenance_mode():
    """점검 모드 - 수동으로 서킷 열기"""
    payment_breaker.open()
    print("🔧 점검 모드: 서킷을 수동으로 열었습니다")

def resume_service():
    """서비스 재개 - 서킷 닫기"""
    payment_breaker.close()
    print("✅ 서비스 재개: 서킷을 닫았습니다")


# 사용 예시
if __name__ == "__main__":
    service = PaymentService("http://payment-api.internal")

    # 정상 결제 처리
    result = service.process_payment_with_fallback("ORDER-001", 50000)
    print(f"결제 결과: {result}")

    # 서킷 상태 확인
    print(f"현재 상태: {payment_breaker.current_state}")
    print(f"실패 횟수: {payment_breaker.fail_counter}")
// Circuit Breaker 패턴 - Node.js + opossum
const CircuitBreaker = require('opossum');
const axios = require('axios');

// 외부 서비스 호출 함수
async function callPaymentService(orderId, amount) {
    const response = await axios.post('http://payment-api/payments', {
        orderId,
        amount
    }, { timeout: 5000 });
    return response.data;
}

// Circuit Breaker 옵션 설정
const circuitOptions = {
    timeout: 5000,           // 5초 타임아웃
    errorThresholdPercentage: 50,  // 50% 실패 시 Open
    resetTimeout: 30000,     // 30초 후 Half-Open
    volumeThreshold: 10,     // 최소 10개 요청 후 판단
    rollingCountTimeout: 10000,    // 10초 윈도우
    rollingCountBuckets: 10        // 통계 버킷 수
};

// Circuit Breaker 생성
const paymentBreaker = new CircuitBreaker(callPaymentService, circuitOptions);

// 폴백 함수 등록
paymentBreaker.fallback((orderId, amount) => {
    console.log(`⚡ 폴백 실행: 결제 ${orderId}를 대기열에 저장`);
    return {
        orderId,
        amount,
        status: 'pending',
        message: '결제 서비스 일시 불가. 잠시 후 자동 처리됩니다.'
    };
});

// 이벤트 리스너
paymentBreaker.on('open', () => {
    console.log('🔴 Circuit OPEN - 결제 서비스 차단');
    // 알림 발송
    sendSlackAlert('결제 서비스 Circuit Breaker OPEN!');
});

paymentBreaker.on('halfOpen', () => {
    console.log('🟡 Circuit HALF-OPEN - 복구 테스트 중');
});

paymentBreaker.on('close', () => {
    console.log('🟢 Circuit CLOSED - 정상 운영');
});

paymentBreaker.on('fallback', (result) => {
    console.log('📥 폴백 결과:', result);
});

paymentBreaker.on('timeout', () => {
    console.log('⏰ 타임아웃 발생');
});

// 통계 모니터링
paymentBreaker.on('snapshot', (stats) => {
    console.log('📊 통계:', {
        failures: stats.failures,
        successes: stats.successes,
        fallbacks: stats.fallbacks,
        latencyMean: stats.latencyMean
    });
});


// 결제 서비스 클래스
class PaymentService {
    /**
     * 결제 처리 (Circuit Breaker 적용)
     */
    async processPayment(orderId, amount) {
        try {
            const result = await paymentBreaker.fire(orderId, amount);
            console.log(`✅ 결제 성공: ${orderId}`);
            return result;
        } catch (error) {
            console.error(`❌ 결제 실패: ${error.message}`);
            throw error;
        }
    }

    /**
     * 서킷 상태 확인
     */
    getStatus() {
        return {
            state: paymentBreaker.opened ? 'OPEN' :
                   paymentBreaker.halfOpen ? 'HALF-OPEN' : 'CLOSED',
            stats: paymentBreaker.stats
        };
    }

    /**
     * 헬스체크 엔드포인트용
     */
    isHealthy() {
        return !paymentBreaker.opened;
    }
}

// Express 라우터 예시
const express = require('express');
const router = express.Router();
const paymentService = new PaymentService();

router.post('/payments', async (req, res) => {
    const { orderId, amount } = req.body;

    try {
        const result = await paymentService.processPayment(orderId, amount);
        res.json(result);
    } catch (error) {
        res.status(503).json({
            error: 'Service temporarily unavailable',
            message: error.message
        });
    }
});

// 서킷 상태 모니터링 엔드포인트
router.get('/health/circuit', (req, res) => {
    res.json(paymentService.getStatus());
});

module.exports = { PaymentService, paymentBreaker };
// Circuit Breaker 패턴 - Java + Resilience4j
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.circuitbreaker.event.CircuitBreakerOnStateTransitionEvent;
import io.vavr.control.Try;

import java.time.Duration;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;

public class PaymentServiceWithCircuitBreaker {

    private final CircuitBreaker circuitBreaker;
    private final PaymentClient paymentClient;

    public PaymentServiceWithCircuitBreaker() {
        // Circuit Breaker 설정
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)           // 50% 실패 시 Open
            .slowCallRateThreshold(50)          // 50% 느린 호출 시 Open
            .slowCallDurationThreshold(Duration.ofSeconds(2))  // 2초 이상 = 느린 호출
            .waitDurationInOpenState(Duration.ofSeconds(30))   // 30초 후 Half-Open
            .permittedNumberOfCallsInHalfOpenState(5)          // Half-Open에서 5개 테스트
            .minimumNumberOfCalls(10)           // 최소 10개 호출 후 판단
            .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
            .slidingWindowSize(20)              // 최근 20개 요청 기준
            .recordExceptions(IOException.class, TimeoutException.class)  // 실패로 기록할 예외
            .ignoreExceptions(BusinessException.class)  // 무시할 예외
            .build();

        // Registry에서 Circuit Breaker 생성
        CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
        this.circuitBreaker = registry.circuitBreaker("paymentService");
        this.paymentClient = new PaymentClient();

        // 상태 변경 이벤트 리스너
        circuitBreaker.getEventPublisher()
            .onStateTransition(this::handleStateTransition)
            .onError(event -> log.error("Circuit Breaker 에러: {}", event.getThrowable().getMessage()))
            .onSuccess(event -> log.debug("호출 성공, 응답시간: {}ms", event.getElapsedDuration().toMillis()));
    }

    private void handleStateTransition(CircuitBreakerOnStateTransitionEvent event) {
        log.warn("🔄 Circuit Breaker 상태 변경: {} → {}",
            event.getStateTransition().getFromState(),
            event.getStateTransition().getToState());

        if (event.getStateTransition().getToState() == CircuitBreaker.State.OPEN) {
            // 알림 발송
            alertService.sendAlert("결제 서비스 Circuit Breaker OPEN!");
        }
    }

    /**
     * 결제 처리 - Circuit Breaker + 폴백
     */
    public PaymentResult processPayment(String orderId, BigDecimal amount) {
        // Circuit Breaker로 감싼 Supplier
        Supplier decoratedSupplier = CircuitBreaker
            .decorateSupplier(circuitBreaker,
                () -> paymentClient.processPayment(orderId, amount));

        // 실행 + 폴백
        return Try.ofSupplier(decoratedSupplier)
            .recover(CallNotPermittedException.class,
                ex -> fallbackPayment(orderId, amount, "Circuit Open"))
            .recover(TimeoutException.class,
                ex -> fallbackPayment(orderId, amount, "Timeout"))
            .recover(IOException.class,
                ex -> fallbackPayment(orderId, amount, "Network Error"))
            .get();
    }

    /**
     * 폴백: 결제를 큐에 저장
     */
    private PaymentResult fallbackPayment(String orderId, BigDecimal amount, String reason) {
        log.info("⚡ 폴백 실행 ({}): 결제 {}를 대기열에 저장", reason, orderId);

        // 메시지 큐에 저장
        PendingPayment pending = new PendingPayment(orderId, amount);
        messageQueue.send("pending-payments", pending);

        return PaymentResult.pending(orderId,
            "결제가 대기열에 저장되었습니다. 잠시 후 자동 처리됩니다.");
    }

    /**
     * 서킷 상태 확인
     */
    public CircuitBreakerStatus getStatus() {
        CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics();
        return new CircuitBreakerStatus(
            circuitBreaker.getState().name(),
            metrics.getFailureRate(),
            metrics.getNumberOfFailedCalls(),
            metrics.getNumberOfSuccessfulCalls(),
            metrics.getSlowCallRate()
        );
    }

    /**
     * 수동 서킷 제어 (점검 모드)
     */
    public void enableMaintenanceMode() {
        circuitBreaker.transitionToOpenState();
        log.info("🔧 점검 모드: 서킷을 수동으로 열었습니다");
    }

    public void disableMaintenanceMode() {
        circuitBreaker.transitionToClosedState();
        log.info("✅ 서비스 재개: 서킷을 닫았습니다");
    }
}

// Spring Boot 설정 (application.yml)
/*
resilience4j:
  circuitbreaker:
    instances:
      paymentService:
        register-health-indicator: true
        sliding-window-size: 20
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 5
        automatic-transition-from-open-to-half-open-enabled: true
*/

🗣️ 실무에서 이렇게 말하세요

💬 장애 대응 회의에서
"결제 서비스 지연이 발생했는데 주문 서비스까지 영향을 받았습니다. Circuit Breaker를 적용하면 결제 서비스가 느려질 때 즉시 폴백으로 전환되어 주문 서비스의 연쇄 장애를 막을 수 있습니다. 실패율 50% 이상이면 30초간 서킷을 열도록 설정하죠."
💬 아키텍처 리뷰에서
"외부 API 연동 부분에 서킷 브레이커가 없네요. 써드파티 서비스가 불안정해지면 우리 서비스 전체가 느려질 수 있어요. Resilience4j 적용하고, 서킷 Open 시 캐시된 데이터를 반환하는 폴백을 추가합시다."
💬 모니터링 논의에서
"서킷 브레이커 상태를 Grafana 대시보드에 추가했습니다. Open 상태로 전환되면 즉시 Slack 알림이 오고, Half-Open에서 성공률도 추적됩니다. 이 메트릭으로 외부 서비스의 안정성 SLA를 측정할 수 있어요."

⚠️ 주의사항 & 베스트 프랙티스

적절한 임계값 설정 필수

임계값이 너무 낮으면 일시적 에러에도 서킷이 열리고, 너무 높으면 장애 전파를 막지 못합니다. 서비스 특성에 맞게 실패율, 타임아웃, 슬라이딩 윈도우를 튜닝하세요.

폴백 없는 서킷 브레이커 금지

서킷이 열렸을 때 단순 에러만 반환하면 사용자 경험이 나빠집니다. 캐시 데이터, 기본값, 대체 서비스 등 의미 있는 폴백 전략을 반드시 구현하세요.

모니터링/알림 연동 필수

서킷 상태 변화를 모니터링하지 않으면 장애 원인 파악이 어렵습니다. Open/Close 상태 변경, 실패율, 응답 시간을 대시보드와 알림 시스템에 연동하세요.

서킷 브레이커 베스트 프랙티스

서비스별 독립 서킷 설정, 타임아웃과 함께 사용, 점검 모드용 수동 제어 API 제공, Bulkhead 패턴과 조합하여 리소스 격리, 주기적인 임계값 검토 및 조정.

🔗 관련 용어

📚 더 배우기