Skip to content
Published on

서킷 브레이커와 복원력 패턴 실전 가이드 — Resilience4j, Istio, 장애 격리 전략

Authors
  • Name
    Twitter

들어가며

마이크로서비스 아키텍처에서 서비스 간 호출은 본질적으로 불안정하다. 네트워크 지연, 타임아웃, 다운스트림 서비스 장애는 일상적으로 발생하며, 적절한 방어 메커니즘 없이는 단일 서비스 장애가 전체 시스템으로 전파되는 연쇄 장애(Cascading Failure) 를 초래한다. 2024년 블랙프라이데이 기간 한 대형 이커머스 플랫폼에서 상품 추천 서비스의 응답 지연이 상품 목록 페이지 전체를 20초 이상 로딩하게 만들었던 사례가 대표적이다.

이러한 문제를 해결하기 위해 등장한 것이 복원력 패턴(Resilience Patterns) 이다. Michael Nygard가 2007년 Release It! 에서 처음 소개한 서킷 브레이커 패턴을 시작으로, Bulkhead, Retry, Rate Limiter, Timeout, Fallback 등 다양한 패턴이 체계화되었다. Netflix Hystrix가 첫 번째 대중적 구현체였지만, 2018년 유지보수 모드 진입 이후 Resilience4j 가 Java/Spring 생태계의 사실상 표준으로 자리잡았으며, 서비스 메시 환경에서는 Istio 가 인프라 레벨의 서킷 브레이커를 제공한다.

이 글에서는 서킷 브레이커의 동작 원리부터 Resilience4j와 Istio를 활용한 실전 구현, 복합 복원력 패턴 설계, Hystrix 마이그레이션, 운영 모니터링, 장애 사례 분석까지 포괄적으로 다룬다.


1. 서킷 브레이커 패턴의 원리

서킷 브레이커는 전기 회로의 차단기에서 착안한 패턴으로, 원격 서비스 호출 실패를 감지하고 자동으로 호출을 차단하여 시스템 전체의 연쇄 장애를 방지한다.

1.1 세 가지 상태: Closed, Open, Half-Open

              실패율 >= 임계값 (failureRateThreshold)
    ┌─────────────────────────────────────────────┐
    │                                             │
    ▼                                             │
┌──────────┐                                 ┌──────────┐
│          │     시험 호출 성공률 >= 임계값     │          │
OPEN<─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ── │  CLOSED  (차단)  (정상)└──────────┘                                 └──────────┘
     │                                            ▲
     │ waitDurationInOpenState 경과                │
     ▼                                            │
┌──────────────┐    시험 호출 성공               │
HALF-OPEN   │ ────────────────────────────────┘
  (시험 허용)└──────────────┘
     │ 시험 호출 실패
┌──────────┐
OPEN     (다시 차단)
└──────────┘

각 상태의 동작은 다음과 같다.

상태동작전이 조건
CLOSED모든 요청을 정상 통과시키며 결과를 슬라이딩 윈도우에 기록실패율이 임계값 이상이면 OPEN으로 전이
OPEN모든 요청을 즉시 거부하고 CallNotPermittedException 발생waitDuration 경과 후 HALF-OPEN으로 전이
HALF-OPEN제한된 수의 시험 호출만 허용시험 호출 성공률에 따라 CLOSED 또는 OPEN으로 전이

1.2 슬라이딩 윈도우 방식

Resilience4j는 두 가지 슬라이딩 윈도우를 지원한다.

  • COUNT_BASED: 최근 N개 호출 결과를 기준으로 실패율 계산. 트래픽이 일정한 서비스에 적합하다.
  • TIME_BASED: 최근 N초 내의 호출 결과를 기준으로 실패율 계산. 트래픽 변동이 큰 서비스에 적합하다.

2. Resilience4j를 활용한 Java/Spring 서킷 브레이커 구현

2.1 의존성 설정

// build.gradle (Spring Boot 3.x)
dependencies {
    implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
    implementation 'io.github.resilience4j:resilience4j-micrometer:2.2.0'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

2.2 application.yml 설정

resilience4j:
  circuitbreaker:
    instances:
      paymentService:
        registerHealthIndicator: true
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 10
        minimumNumberOfCalls: 5
        failureRateThreshold: 50
        waitDurationInOpenState: 10s
        permittedNumberOfCallsInHalfOpenState: 3
        slowCallDurationThreshold: 2s
        slowCallRateThreshold: 80
        recordExceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException
          - org.springframework.web.client.HttpServerErrorException
        ignoreExceptions:
          - com.example.BusinessException

  retry:
    instances:
      paymentService:
        maxAttempts: 3
        waitDuration: 500ms
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
        retryExceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException

  bulkhead:
    instances:
      paymentService:
        maxConcurrentCalls: 20
        maxWaitDuration: 500ms

  timelimiter:
    instances:
      paymentService:
        timeoutDuration: 3s
        cancelRunningFuture: true

  ratelimiter:
    instances:
      paymentService:
        limitRefreshPeriod: 1s
        limitForPeriod: 50
        timeoutDuration: 0s

2.3 어노테이션 기반 구현

@Service
@Slf4j
public class PaymentService {

    private final PaymentGatewayClient paymentGatewayClient;
    private final PaymentCacheService paymentCacheService;

    public PaymentService(PaymentGatewayClient paymentGatewayClient,
                          PaymentCacheService paymentCacheService) {
        this.paymentGatewayClient = paymentGatewayClient;
        this.paymentCacheService = paymentCacheService;
    }

    @CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
    @Bulkhead(name = "paymentService")
    @Retry(name = "paymentService")
    @TimeLimiter(name = "paymentService")
    public CompletableFuture<PaymentResponse> processPayment(PaymentRequest request) {
        return CompletableFuture.supplyAsync(() -> {
            log.info("결제 요청 처리 중: orderId={}", request.getOrderId());
            return paymentGatewayClient.charge(request);
        });
    }

    // 폴백 메서드: 서킷이 OPEN이거나 예외 발생 시 호출
    private CompletableFuture<PaymentResponse> paymentFallback(
            PaymentRequest request, Throwable throwable) {
        log.warn("결제 서비스 폴백 실행: orderId={}, reason={}",
                request.getOrderId(), throwable.getMessage());

        if (throwable instanceof CallNotPermittedException) {
            // 서킷이 열린 상태 - 큐에 저장 후 비동기 처리
            return CompletableFuture.completedFuture(
                PaymentResponse.queued(request.getOrderId(),
                    "결제 서비스 일시 장애. 주문이 대기열에 등록되었습니다.")
            );
        }

        // 기타 예외 - 캐시된 결과 반환 시도
        return CompletableFuture.completedFuture(
            paymentCacheService.getCachedResponse(request.getOrderId())
                .orElse(PaymentResponse.error(request.getOrderId(),
                    "결제 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."))
        );
    }
}

Aspect 실행 순서: Resilience4j의 어노테이션은 다음 순서로 중첩 적용된다.

Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( Function ) ) ) ) )

가장 바깥의 Retry가 마지막에 적용되므로, CircuitBreaker가 예외를 던지면 Retry가 재시도를 수행한다. 이 순서는 각 모듈의 *AspectOrder 속성으로 커스터마이징할 수 있다.


3. Istio 서비스 메시 레벨 서킷 브레이커

Istio는 애플리케이션 코드 변경 없이 인프라 레벨에서 서킷 브레이커를 적용할 수 있다. Envoy 프록시의 Outlier Detection 기능을 활용하여 비정상 인스턴스를 로드 밸런싱 풀에서 자동 제거한다.

3.1 DestinationRule 설정

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: payment-service-circuit-breaker
  namespace: production
spec:
  host: payment-service.production.svc.cluster.local
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100 # 최대 TCP 연결 수
        connectTimeout: 3s # TCP 연결 타임아웃
      http:
        h2UpgradePolicy: DEFAULT
        http1MaxPendingRequests: 50 # 대기 중인 HTTP 요청 최대 수
        http2MaxRequests: 100 # 활성 HTTP/2 요청 최대 수
        maxRequestsPerConnection: 10 # 연결당 최대 요청 수
        maxRetries: 3 # 최대 재시도 횟수
    outlierDetection:
      consecutive5xxErrors: 5 # 연속 5xx 에러 5회 시 제거
      interval: 10s # 분석 주기
      baseEjectionTime: 30s # 최소 제거 시간
      maxEjectionPercent: 50 # 최대 제거 비율 (50%)
      minHealthPercent: 30 # 최소 정상 인스턴스 비율

3.2 Istio vs 애플리케이션 레벨 서킷 브레이커

구분Istio (인프라 레벨)Resilience4j (앱 레벨)
적용 방식코드 변경 없이 YAML 설정어노테이션 또는 프로그래매틱 API
격리 단위인스턴스(Pod) 단위 제거메서드/서비스 단위 차단
폴백지원하지 않음 (503 반환)커스텀 폴백 메서드 지원
언어 독립성모든 언어/프레임워크 지원Java/Kotlin 전용
세밀한 제어제한적매우 세밀함
모니터링Kiali, Grafana 연동Micrometer, Actuator 연동
권장 사례다중 언어 환경, 기본 보호비즈니스 로직 연동 필요 시

실무에서는 두 가지를 함께 사용하는 것이 권장된다. Istio가 인프라 레벨에서 비정상 인스턴스를 격리하고, Resilience4j가 애플리케이션 레벨에서 세밀한 폴백과 재시도를 처리하는 구조이다.


4. Bulkhead 패턴: 장애 격리 전략

Bulkhead(격벽) 패턴은 선박의 격벽에서 유래한 개념으로, 하나의 구획에 침수가 발생해도 다른 구획은 영향을 받지 않도록 격리하는 전략이다.

4.1 Semaphore Bulkhead vs ThreadPool Bulkhead

구분Semaphore BulkheadThreadPool Bulkhead
격리 방식세마포어로 동시 호출 수 제한별도 스레드 풀에서 실행
호출 스레드요청 스레드에서 직접 실행별도 스레드에서 비동기 실행
반환 타입동기/비동기 모두 지원CompletableFuture만 지원
오버헤드낮음스레드 풀 관리 비용 존재
권장 사례일반적인 동시성 제한완전한 스레드 격리 필요 시
# ThreadPool Bulkhead 설정
resilience4j:
  thread-pool-bulkhead:
    instances:
      inventoryService:
        maxThreadPoolSize: 10
        coreThreadPoolSize: 5
        queueCapacity: 20
        keepAliveDuration: 100ms
        writableStackTraceEnabled: true

4.2 서비스별 Bulkhead 격리 예시

@Service
public class OrderOrchestrator {

    @Bulkhead(name = "paymentService", type = Bulkhead.Type.SEMAPHORE)
    public PaymentResult processPayment(Order order) {
        return paymentClient.charge(order.getPaymentInfo());
    }

    @Bulkhead(name = "inventoryService", type = Bulkhead.Type.THREADPOOL)
    public CompletableFuture<InventoryResult> reserveInventory(Order order) {
        return CompletableFuture.supplyAsync(() ->
            inventoryClient.reserve(order.getItems()));
    }

    @Bulkhead(name = "notificationService", type = Bulkhead.Type.SEMAPHORE)
    public void sendNotification(Order order) {
        notificationClient.send(order.getUserId(), "주문이 접수되었습니다.");
    }
}

이렇게 서비스별로 Bulkhead를 분리하면, 재고 서비스가 느려져도 결제 서비스의 동시 호출 용량은 영향을 받지 않는다.


5. Retry + Timeout + Rate Limiter 조합 패턴

복원력 패턴은 단독으로 사용하기보다 조합하여 사용할 때 가장 효과적이다. 하지만 잘못된 조합은 오히려 장애를 악화시킬 수 있으므로 주의가 필요하다.

5.1 패턴 조합 시 주의사항

  • Retry + CircuitBreaker: Retry만 단독 사용하면 장애 서비스에 부하를 가중시킨다. 반드시 CircuitBreaker와 함께 사용하여 일정 실패율 이상일 때 재시도 자체를 차단해야 한다.
  • Timeout + Retry: 총 소요 시간 = timeout * maxAttempts. 타임아웃 3초에 재시도 3회라면 최악의 경우 9초가 소요된다. 사용자 응답 시간 SLA를 고려하여 설계해야 한다.
  • Rate Limiter + CircuitBreaker: 외부 API의 호출 제한(Rate Limit)을 초과하지 않도록 Rate Limiter를 적용하고, API 자체 장애에는 CircuitBreaker가 대응하는 이중 방어 구조이다.

5.2 프로그래매틱 API를 활용한 조합

@Configuration
public class ResilienceConfig {

    @Bean
    public Supplier<String> resilientSupplier(
            CircuitBreakerRegistry circuitBreakerRegistry,
            RetryRegistry retryRegistry,
            BulkheadRegistry bulkheadRegistry,
            RateLimiterRegistry rateLimiterRegistry) {

        CircuitBreaker circuitBreaker = circuitBreakerRegistry
                .circuitBreaker("externalApi");
        Retry retry = retryRegistry.retry("externalApi");
        Bulkhead bulkhead = bulkheadRegistry.bulkhead("externalApi");
        RateLimiter rateLimiter = rateLimiterRegistry
                .rateLimiter("externalApi");

        // 데코레이터 체이닝: 안쪽부터 바깥쪽으로 적용
        Supplier<String> decoratedSupplier = Decorators
                .ofSupplier(() -> externalApiClient.call())
                .withBulkhead(bulkhead)           // 1. 동시 호출 제한
                .withRateLimiter(rateLimiter)      // 2. 초당 호출 제한
                .withCircuitBreaker(circuitBreaker) // 3. 실패 감지/차단
                .withRetry(retry)                   // 4. 재시도
                .withFallback(Arrays.asList(
                    CallNotPermittedException.class,
                    BulkheadFullException.class,
                    RequestNotPermitted.class),
                    throwable -> "Fallback Response")
                .decorate();

        return decoratedSupplier;
    }
}

6. 폴백(Fallback) 전략 설계

폴백은 원래 서비스가 실패했을 때 제공하는 대안적 응답이다. 단순히 에러 메시지를 반환하는 것이 아니라, 사용자 경험을 최대한 유지하면서 graceful degradation을 구현하는 것이 핵심이다.

6.1 폴백 전략 유형

전략설명적용 예시
캐시 폴백마지막 성공 응답을 캐시하여 반환상품 추천, 환율 정보, 날씨 데이터
기본값 폴백사전 정의된 기본값 반환설정 서비스, 기능 플래그
대기열 폴백요청을 큐에 저장하고 나중에 처리결제 처리, 주문 접수
대체 서비스 폴백보조 서비스로 라우팅CDN 이중화, 멀티 리전
빈 응답 폴백빈 결과 반환 (에러 대신)검색 자동완성, 추천 위젯
수동 전환 폴백운영자가 수동으로 대체 로직 활성화중요 비즈니스 로직

6.2 다단계 폴백 구현

@Service
@Slf4j
public class ProductRecommendationService {

    private final RecommendationEngine primaryEngine;
    private final RecommendationEngine secondaryEngine;
    private final RedisTemplate<String, List<Product>> cache;

    @CircuitBreaker(name = "recommendation",
                    fallbackMethod = "secondaryRecommendation")
    public List<Product> getRecommendations(String userId) {
        return primaryEngine.recommend(userId);
    }

    // 1차 폴백: 보조 추천 엔진 사용
    private List<Product> secondaryRecommendation(
            String userId, Throwable t) {
        log.warn("1차 추천 엔진 장애, 보조 엔진으로 전환: {}", t.getMessage());
        try {
            return secondaryEngine.recommend(userId);
        } catch (Exception e) {
            return cachedRecommendation(userId, e);
        }
    }

    // 2차 폴백: 캐시된 추천 결과 반환
    private List<Product> cachedRecommendation(
            String userId, Throwable t) {
        log.warn("보조 추천 엔진도 장애, 캐시 조회: {}", t.getMessage());
        List<Product> cached = cache.opsForValue()
                .get("recommendation:" + userId);
        if (cached != null && !cached.isEmpty()) {
            return cached;
        }
        return defaultRecommendation(userId, t);
    }

    // 3차 폴백: 인기 상품 기본 목록 반환
    private List<Product> defaultRecommendation(
            String userId, Throwable t) {
        log.warn("캐시도 없음, 기본 인기 상품 반환");
        return List.of(
            Product.popular("BEST-001", "베스트셀러 상품 A"),
            Product.popular("BEST-002", "베스트셀러 상품 B"),
            Product.popular("BEST-003", "베스트셀러 상품 C")
        );
    }
}

7. Netflix Hystrix에서 Resilience4j로의 마이그레이션

Netflix Hystrix는 2018년 유지보수 모드에 진입했으며, Spring Cloud 2020.0.0부터 공식 지원이 중단되었다. 기존 Hystrix 사용 프로젝트는 Resilience4j로 마이그레이션이 필요하다.

7.1 Resilience4j vs Hystrix vs Istio 비교

항목HystrixResilience4jIstio
유지보수 상태유지보수 모드 (2018~)활발히 유지보수 중활발히 유지보수 중
설계 철학OOP (HystrixCommand 상속)함수형 프로그래밍 (데코레이터)인프라 기반 (사이드카 프록시)
모듈 구성올인원필요한 모듈만 선택전체 서비스 메시
Spring Boot 통합Spring Cloud Netflix네이티브 Spring Boot 스타터Kubernetes 환경 필요
격리 방식Thread Pool / SemaphoreSemaphore / Thread Pool커넥션 풀 / Outlier Detection
설정 방식Java Config / PropertiesYAML / Java Config / 어노테이션Kubernetes CRD (YAML)
리액티브 지원제한적 (RxJava 1)완전 지원 (Reactor, RxJava 2/3)해당 없음
메트릭Hystrix DashboardMicrometer / PrometheusPrometheus / Kiali
폴백HystrixCommand.getFallback()fallbackMethod 어노테이션미지원 (503 반환)
러닝 커브보통낮음높음 (서비스 메시 이해 필요)

7.2 마이그레이션 핵심 체크리스트

1단계: 의존성 교체

// 제거
// implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix'

// 추가
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
implementation 'io.github.resilience4j:resilience4j-micrometer:2.2.0'

2단계: 코드 변환 패턴

HystrixResilience4j
@HystrixCommand(fallbackMethod = "fallback")@CircuitBreaker(name = "svc", fallbackMethod = "fallback")
HystrixCommand extends HystrixCommandDecorators.ofSupplier(() -> ...).withCircuitBreaker(cb)
@HystrixProperty(name = "...")application.yml 설정
HystrixDashboardMicrometer + Grafana

3단계: 설정 마이그레이션

Hystrix의 circuitBreaker.requestVolumeThreshold는 Resilience4j의 minimumNumberOfCalls에 대응하고, circuitBreaker.errorThresholdPercentagefailureRateThreshold에 대응한다. circuitBreaker.sleepWindowInMillisecondswaitDurationInOpenState로 변환된다.

4단계: 점진적 전환

한 번에 전체를 교체하지 말고, 서비스별로 점진적으로 마이그레이션한다. Resilience4j와 Hystrix는 동일 프로젝트에서 공존 가능하므로, 새 서비스부터 Resilience4j를 적용하고 기존 서비스를 순차적으로 전환하는 것이 안전하다.


8. 실패 사례 분석과 복구 절차

8.1 사례 1: Retry Storm (재시도 폭풍)

상황: 결제 게이트웨이 장애 시 모든 클라이언트가 동시에 재시도를 수행하여 게이트웨이 복구를 지연시킴.

원인: CircuitBreaker 없이 Retry만 적용. 재시도 간격에 jitter(무작위 지연)가 없어 동기화된 재시도 발생.

해결:

  • CircuitBreaker를 Retry와 함께 적용하여 일정 실패율 이상에서 재시도 자체를 차단
  • Exponential backoff에 jitter 추가
resilience4j:
  retry:
    instances:
      paymentGateway:
        maxAttempts: 3
        waitDuration: 1s
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
        enableRandomizedWait: true # jitter 활성화
        randomizedWaitFactor: 0.5 # 50% 범위 내 무작위화

8.2 사례 2: Bulkhead 미적용으로 인한 스레드 풀 고갈

상황: 재고 확인 API가 느려지면서 Tomcat 스레드 풀 전체를 점유. 결제, 주문 조회 등 무관한 API까지 타임아웃 발생.

원인: 모든 외부 서비스 호출이 동일한 스레드 풀에서 실행.

해결:

  • 서비스별 ThreadPool Bulkhead 적용으로 스레드 격리
  • 느린 서비스가 전체 스레드 풀을 점유하지 못하도록 제한

8.3 사례 3: 서킷 브레이커 임계값 설정 오류

상황: minimumNumberOfCalls: 1, failureRateThreshold: 50으로 설정. 단 한 번의 실패로 서킷이 열려 정상 서비스도 차단됨.

원인: 통계적으로 유의미하지 않은 소수의 호출로 상태 전이가 발생.

해결:

  • minimumNumberOfCalls를 최소 5~10으로 설정
  • slidingWindowSize를 충분히 크게 설정 (최소 10 이상)
  • 운영 환경에서 실제 트래픽 패턴을 분석한 후 임계값 조정

8.4 복구 절차 표준화

#!/bin/bash
# circuit-breaker-recovery.sh
# 서킷 브레이커 장애 복구 절차 스크립트

echo "===== 서킷 브레이커 상태 확인 ====="
# Actuator 엔드포인트로 서킷 브레이커 상태 확인
curl -s http://localhost:8080/actuator/circuitbreakers | jq '.circuitBreakers'

echo ""
echo "===== 다운스트림 서비스 헬스 체크 ====="
curl -s http://payment-service:8080/actuator/health | jq '.status'
curl -s http://inventory-service:8080/actuator/health | jq '.status'

echo ""
echo "===== 서킷 브레이커 강제 닫기 (다운스트림 복구 확인 후) ====="
# 주의: 다운스트림 서비스가 완전히 복구된 후에만 실행
# curl -X POST http://localhost:8080/actuator/circuitbreakers/paymentService/close

echo ""
echo "===== 현재 메트릭 확인 ====="
curl -s http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.state | jq '.'
curl -s http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.failure.rate | jq '.'

echo ""
echo "===== Istio Outlier Detection 상태 확인 ====="
kubectl get destinationrules -n production
kubectl describe destinationrule payment-service-circuit-breaker -n production

9. 운영 모니터링 및 메트릭

9.1 핵심 모니터링 메트릭

서킷 브레이커 운영에서 반드시 모니터링해야 할 메트릭은 다음과 같다.

메트릭설명경고 임계값
resilience4j.circuitbreaker.state현재 서킷 상태 (0=CLOSED, 1=OPEN, 2=HALF_OPEN)state == 1 (OPEN)
resilience4j.circuitbreaker.failure.rate현재 실패율 (%)40% 이상
resilience4j.circuitbreaker.calls성공/실패/무시/차단된 호출 수차단 호출 급증 시
resilience4j.circuitbreaker.slow.call.rate느린 호출 비율 (%)60% 이상
resilience4j.bulkhead.available.concurrent.calls사용 가능한 동시 호출 수0에 가까울 때
resilience4j.retry.calls재시도 횟수급증 시
resilience4j.ratelimiter.available.permissions사용 가능한 허용 수0에 가까울 때

9.2 Prometheus + Grafana 대시보드 설정

Resilience4j는 Micrometer를 통해 Prometheus 형식의 메트릭을 자동 노출한다.

# Prometheus scrape 설정
scrape_configs:
  - job_name: 'spring-boot-resilience4j'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 5s
    static_configs:
      - targets: ['payment-service:8080']
        labels:
          application: 'payment-service'

Grafana 알림 규칙 예시: 서킷이 OPEN 상태로 전이되면 Slack 알림을 발송하도록 구성한다.

# Grafana Alert Rule (provisioning)
groups:
  - name: circuit-breaker-alerts
    rules:
      - alert: CircuitBreakerOpen
        expr: resilience4j_circuitbreaker_state{state="open"} == 1
        for: 10s
        labels:
          severity: critical
        annotations:
          summary: '서킷 브레이커 OPEN - {{ $labels.name }}'
          description: >
            {{ $labels.application }}의 {{ $labels.name }}
            서킷 브레이커가 OPEN 상태입니다.
            즉시 다운스트림 서비스 상태를 확인하세요.

      - alert: HighFailureRate
        expr: resilience4j_circuitbreaker_failure_rate > 40
        for: 30s
        labels:
          severity: warning
        annotations:
          summary: '높은 실패율 감지 - {{ $labels.name }}'
          description: >
            {{ $labels.name }}의 실패율이 {{ $value }}%입니다.
            서킷이 열리기 전에 원인을 파악하세요.

9.3 Istio 모니터링 (Kiali + Grafana)

# Istio 메시 내 서비스 상태 확인
istioctl proxy-config cluster <pod-name> -n production | grep outlier

# Envoy 통계 확인
kubectl exec -it <pod-name> -n production -c istio-proxy -- \
  curl localhost:15000/stats | grep outlier_detection

# Kiali 대시보드 접근
istioctl dashboard kiali

10. 트러블슈팅

서킷 브레이커가 열리지 않는 경우

  • minimumNumberOfCalls 값을 확인한다. 이 값보다 적은 호출이 발생했다면 실패율이 100%여도 서킷은 열리지 않는다.
  • recordExceptions에 실제 발생하는 예외 타입이 포함되어 있는지 확인한다. 등록되지 않은 예외는 실패로 카운트되지 않는다.
  • ignoreExceptions에 의도치 않게 장애 예외가 포함되어 있는지 점검한다.

서킷이 HALF-OPEN에서 빠르게 다시 OPEN으로 전이되는 경우

  • permittedNumberOfCallsInHalfOpenState 값이 너무 작으면 통계적으로 유의미한 판단이 어렵다. 최소 3~5로 설정한다.
  • 다운스트림 서비스가 부분적으로만 복구된 경우 발생할 수 있다. 다운스트림의 완전한 복구를 확인한다.

Bulkhead 관련 오류

  • BulkheadFullException이 빈번하게 발생하면 maxConcurrentCalls 값을 늘리거나, 다운스트림 서비스의 응답 시간을 개선한다.
  • ThreadPool Bulkhead 사용 시 queueCapacity가 0이면 스레드 풀이 가득 찼을 때 즉시 거부된다.

Istio Outlier Detection이 작동하지 않는 경우

  • Pod에 Istio 사이드카 프록시가 주입되어 있는지 확인한다: kubectl get pod <name> -o jsonpath='{.spec.containers[*].name}'
  • DestinationRule의 host 필드가 정확한 서비스 FQDN인지 확인한다.
  • maxEjectionPercent가 너무 낮으면 일부 비정상 인스턴스가 제거되지 않을 수 있다.

11. 실전 체크리스트

설계 단계

  • 각 다운스트림 서비스의 SLA(응답 시간, 가용성)를 확인했는가
  • 서비스별 장애 영향도를 분류했는가 (Critical / High / Medium / Low)
  • 장애 시 폴백 전략을 정의했는가 (캐시, 기본값, 대기열, 대체 서비스)
  • Retry 적용 대상이 멱등성(Idempotency)을 보장하는지 확인했는가
  • Retry + CircuitBreaker 조합 사용 여부를 결정했는가 (Retry 단독 사용 금지)
  • 총 타임아웃 = timeout * maxAttempts 값이 사용자 SLA 이내인지 확인했는가

구현 단계

  • CircuitBreaker의 slidingWindowSizeminimumNumberOfCalls를 충분히 크게 설정했는가 (최소 5~10)
  • recordExceptions에 네트워크/타임아웃 관련 예외를 등록했는가
  • ignoreExceptions에 비즈니스 예외(400 Bad Request 등)를 등록했는가
  • 서비스별 Bulkhead를 분리 적용했는가
  • 외부 API 호출에 Rate Limiter를 적용했는가
  • Fallback 메서드의 파라미터가 원래 메서드와 일치하는지 확인했는가 (+ Throwable 추가)

운영 단계

  • Actuator 엔드포인트(/actuator/circuitbreakers, /actuator/health)를 노출했는가
  • Prometheus 메트릭 수집을 설정했는가
  • 서킷 OPEN 상태 전이 시 알림(Slack, PagerDuty 등)을 설정했는가
  • 실패율 경고 임계값 알림을 설정했는가
  • 서킷 브레이커 장애 복구 절차(Runbook)를 문서화했는가
  • 주기적인 Chaos Engineering 테스트(서비스 장애 주입)를 수행하고 있는가
  • Istio 환경이라면 DestinationRule과 Outlier Detection을 설정했는가

테스트 단계

  • 서킷 상태 전이(CLOSED -> OPEN -> HALF-OPEN -> CLOSED) 시나리오를 테스트했는가
  • Fallback 메서드가 정상 동작하는지 테스트했는가
  • Bulkhead 가득 참 상황을 시뮬레이션했는가
  • 다운스트림 서비스 완전 다운 시나리오를 테스트했는가
  • 느린 응답(Slow Call) 시나리오를 테스트했는가

마치며

복원력 패턴은 마이크로서비스 아키텍처에서 선택이 아닌 필수이다. 서킷 브레이커, Bulkhead, Retry, Rate Limiter, Timeout을 적절히 조합하면 단일 서비스 장애가 전체 시스템으로 전파되는 것을 효과적으로 차단할 수 있다.

핵심 원칙을 정리하면 다음과 같다.

  1. Retry 단독 사용은 금지: 반드시 CircuitBreaker와 함께 사용하여 재시도 폭풍을 방지한다.
  2. 서비스별 격리: Bulkhead로 각 다운스트림 서비스의 리소스 사용을 격리한다.
  3. 다단계 폴백: 단일 폴백이 아닌, 대체 서비스 -> 캐시 -> 기본값의 다단계 구조를 설계한다.
  4. 인프라 + 앱 레벨 이중 방어: Istio의 Outlier Detection과 Resilience4j를 함께 사용한다.
  5. 모니터링 필수: 서킷 상태, 실패율, 느린 호출 비율을 실시간 모니터링하고 알림을 설정한다.

Hystrix에서 Resilience4j로의 마이그레이션은 점진적으로 수행하되, 신규 서비스부터 Resilience4j를 도입하는 것이 현실적이다. 무엇보다 중요한 것은 정기적인 Chaos Engineering 테스트를 통해 설정한 복원력 패턴이 실제 장애 상황에서 기대대로 동작하는지 검증하는 것이다.


참고 자료