이 글에서 얻는 것

  • Circuit Breaker 패턴의 동작 원리를 이해합니다.
  • Resilience4j로 Circuit Breaker를 구현할 수 있습니다.
  • 장애 전파를 차단하고 시스템을 보호할 수 있습니다.
  • 재시도, 타임아웃, 폴백 전략을 조합할 수 있습니다.

0) Circuit Breaker는 “전기 차단기"다

문제 상황

마이크로서비스 A → B → C

C 서비스 장애 발생!
↓
B는 C에 계속 요청 (타임아웃 대기)
↓
A도 B를 기다림
↓
전체 시스템 다운!

Circuit Breaker 적용:

C 서비스 장애 감지
↓
Circuit Open (차단)
↓
C로의 요청 즉시 차단
↓
Fallback 응답 반환
↓
시스템 전체는 정상 동작

1) Circuit Breaker 상태

1-1) 3가지 상태

CLOSED (정상):
- 모든 요청이 정상적으로 전달됨
- 실패율 모니터링
- 실패율이 임계값 초과 → OPEN

OPEN (차단):
- 모든 요청이 즉시 실패 (빠른 실패)
- Fallback 응답 반환
- 일정 시간 후 → HALF_OPEN

HALF_OPEN (반개방):
- 일부 요청만 허용 (테스트)
- 성공하면 → CLOSED
- 실패하면 → OPEN

1-2) 상태 전이 다이어그램

     [실패율 < 임계값]
    ┌─────────────────┐
    │                 ↓
┌─────────┐       ┌─────────┐
│ CLOSED  │───────│  OPEN   │
└─────────┘       └─────────┘
    ↑                 │
    │   [대기 시간 경과]
    │                 ↓
    │          ┌──────────────┐
    └──────────│  HALF_OPEN   │
  [성공]       └──────────────┘
                      │
                [실패] ↓
              다시 OPEN으로

2) Resilience4j 기본

2-1) 의존성

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.1.0'
    implementation 'io.github.resilience4j:resilience4j-reactor:2.1.0'
}

2-2) 설정

resilience4j.circuitbreaker:
  configs:
    default:
      # 실패율 임계값 (50%)
      failureRateThreshold: 50
      
      # 느린 호출 임계값 (느린 호출 비율이 80% 넘으면 Open)
      slowCallRateThreshold: 80
      slowCallDurationThreshold: 2s
      
      # 최소 호출 수 (이 수만큼 호출되어야 통계 계산)
      minimumNumberOfCalls: 10
      
      # Sliding Window 크기
      slidingWindowType: COUNT_BASED
      slidingWindowSize: 100
      
      # Open 상태 유지 시간
      waitDurationInOpenState: 10s
      
      # Half-Open 상태에서 허용할 호출 수
      permittedNumberOfCallsInHalfOpenState: 5
      
      # 자동으로 CLOSED → OPEN 전환 허용
      automaticTransitionFromOpenToHalfOpenEnabled: true
      
  instances:
    paymentService:
      baseConfig: default
      failureRateThreshold: 60
      
    externalApi:
      baseConfig: default
      waitDurationInOpenState: 30s

3) @CircuitBreaker 사용

3-1) 기본 사용

@Service
public class PaymentService {

    @Autowired
    private RestTemplate restTemplate;

    @CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
    public PaymentResponse processPayment(PaymentRequest request) {
        // 외부 결제 API 호출
        return restTemplate.postForObject(
            "https://payment-api.com/process",
            request,
            PaymentResponse.class
        );
    }

    // Fallback 메서드 (Circuit Open 시 호출)
    private PaymentResponse paymentFallback(PaymentRequest request, Exception e) {
        log.error("Payment service is unavailable", e);
        
        return PaymentResponse.builder()
            .status("PENDING")
            .message("결제 서비스를 일시적으로 사용할 수 없습니다. 잠시 후 다시 시도해주세요.")
            .build();
    }
}

3-2) 다양한 Fallback

@Service
public class UserService {

    @CircuitBreaker(name = "userService", fallbackMethod = "getUserFromCacheFallback")
    public User getUser(Long id) {
        return restTemplate.getForObject(
            "https://user-api.com/users/" + id,
            User.class
        );
    }

    // Fallback 1: 캐시에서 조회
    private User getUserFromCacheFallback(Long id, Exception e) {
        log.warn("User service unavailable, trying cache");
        return cacheManager.getUser(id)
            .orElseGet(() -> getUserDefaultFallback(id, e));
    }

    // Fallback 2: 기본값 반환
    private User getUserDefaultFallback(Long id, Exception e) {
        log.error("All fallbacks failed", e);
        return User.builder()
            .id(id)
            .name("Unknown User")
            .build();
    }
}

4) 프로그래밍 방식

4-1) CircuitBreakerRegistry 사용

@Service
public class ExternalApiService {

    private final CircuitBreakerRegistry circuitBreakerRegistry;
    private final RestTemplate restTemplate;

    public ExternalApiService(CircuitBreakerRegistry circuitBreakerRegistry,
                              RestTemplate restTemplate) {
        this.circuitBreakerRegistry = circuitBreakerRegistry;
        this.restTemplate = restTemplate;
    }

    public ApiResponse callExternalApi(String endpoint) {
        CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("externalApi");

        return circuitBreaker.executeSupplier(() -> {
            return restTemplate.getForObject(endpoint, ApiResponse.class);
        });
    }
}

4-2) 이벤트 리스너

@Configuration
public class CircuitBreakerEventListener {

    @Bean
    public CircuitBreakerEventListener circuitBreakerEventListener(
            CircuitBreakerRegistry circuitBreakerRegistry) {

        circuitBreakerRegistry.circuitBreaker("paymentService")
            .getEventPublisher()
            .onStateTransition(event -> {
                log.warn("Circuit Breaker State Change: {} -> {}",
                    event.getStateTransition().getFromState(),
                    event.getStateTransition().getToState());
                
                // Slack 알림 등
                if (event.getStateTransition().getToState() == CircuitBreaker.State.OPEN) {
                    slackNotifier.send("Payment service circuit opened!");
                }
            })
            .onError(event -> {
                log.error("Circuit Breaker Error: {}", event.getThrowable().getMessage());
            });

        return new CircuitBreakerEventListener();
    }
}

5) 재시도 + Circuit Breaker 조합

5-1) 재시도 설정

resilience4j.retry:
  configs:
    default:
      maxAttempts: 3
      waitDuration: 1s
      retryExceptions:
        - java.net.ConnectException
        - java.net.SocketTimeoutException
        
  instances:
    paymentService:
      baseConfig: default

5-2) 재시도 + Circuit Breaker

@Service
public class PaymentService {

    @Retry(name = "paymentService", fallbackMethod = "paymentFallback")
    @CircuitBreaker(name = "paymentService")
    public PaymentResponse processPayment(PaymentRequest request) {
        // 1. 재시도 (3번)
        // 2. 재시도 모두 실패 시 Circuit Breaker에서 감지
        // 3. 실패율이 임계값 초과 시 Circuit Open
        return restTemplate.postForObject(...);
    }

    private PaymentResponse paymentFallback(PaymentRequest request, Exception e) {
        return PaymentResponse.pending("서비스 일시 중단");
    }
}

6) 타임아웃 + Circuit Breaker

6-1) 타임아웃 설정

resilience4j.timelimiter:
  configs:
    default:
      timeoutDuration: 3s
      
  instances:
    paymentService:
      baseConfig: default

6-2) 조합 사용

@Service
public class PaymentService {

    @TimeLimiter(name = "paymentService")
    @CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
    public CompletableFuture<PaymentResponse> processPaymentAsync(PaymentRequest request) {
        return CompletableFuture.supplyAsync(() -> {
            // 1. 타임아웃 (3초)
            // 2. 타임아웃 초과 시 Circuit Breaker에서 감지
            return restTemplate.postForObject(...);
        });
    }
}

7) 모니터링

7-1) Actuator Endpoint

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,circuitbreakers,circuitbreakerevents
  health:
    circuitbreakers:
      enabled: true
  metrics:
    tags:
      application: ${spring.application.name}

확인:

# Circuit Breaker 상태 확인
curl http://localhost:8080/actuator/circuitbreakers

# 응답:
{
  "circuitBreakers": {
    "paymentService": {
      "state": "CLOSED",
      "failureRate": "12.5%",
      "slowCallRate": "0%",
      "bufferedCalls": 16,
      "failedCalls": 2
    }
  }
}

# 이벤트 확인
curl http://localhost:8080/actuator/circuitbreakerevents/paymentService

7-2) Prometheus 메트릭

management:
  metrics:
    export:
      prometheus:
        enabled: true

메트릭:

resilience4j_circuitbreaker_state{name="paymentService",state="closed"} 1
resilience4j_circuitbreaker_failure_rate{name="paymentService"} 0.125
resilience4j_circuitbreaker_slow_call_rate{name="paymentService"} 0.0
resilience4j_circuitbreaker_buffered_calls{name="paymentService",kind="failed"} 2

8) 실전 패턴

8-1) 여러 Circuit Breaker 조합

@Service
public class OrderService {

    // 결제 서비스
    @CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
    public PaymentResponse processPayment(Order order) {
        return paymentClient.process(order.getPayment());
    }

    // 재고 서비스
    @CircuitBreaker(name = "inventoryService", fallbackMethod = "inventoryFallback")
    public void reserveInventory(Order order) {
        inventoryClient.reserve(order.getItems());
    }

    // 이메일 서비스 (중요도 낮음, Circuit Breaker 불필요)
    @Async
    public void sendConfirmationEmail(Order order) {
        try {
            emailClient.send(order.getEmail(), "주문 확인");
        } catch (Exception e) {
            log.error("Email sending failed", e);
            // 실패해도 무시
        }
    }

    private PaymentResponse paymentFallback(Order order, Exception e) {
        // 결제 실패 → 주문 취소
        throw new PaymentUnavailableException("결제 서비스 일시 중단");
    }

    private void inventoryFallback(Order order, Exception e) {
        // 재고 확인 실패 → 재고 없음으로 간주
        throw new InventoryUnavailableException("재고 서비스 일시 중단");
    }
}

8-2) 우아한 성능 저하 (Graceful Degradation)

@Service
public class RecommendationService {

    @CircuitBreaker(name = "recommendationService", fallbackMethod = "getPopularItemsFallback")
    public List<Product> getRecommendations(Long userId) {
        // ML 기반 추천 (외부 서비스)
        return mlService.getRecommendations(userId);
    }

    // Fallback 1: 인기 상품 반환
    private List<Product> getPopularItemsFallback(Long userId, Exception e) {
        log.warn("Recommendation service unavailable, returning popular items");
        return productRepository.findPopularProducts(PageRequest.of(0, 10));
    }
}

9) 주의사항

⚠️ 1. 적절한 임계값 설정

# ❌ 너무 민감
failureRateThreshold: 10  # 10%만 실패해도 Open

# ✅ 적절한 설정
failureRateThreshold: 50  # 50% 실패 시 Open
minimumNumberOfCalls: 10  # 최소 10번은 호출되어야 판단

⚠️ 2. Fallback 메서드 시그니처

// ❌ 나쁜 예: 시그니처 불일치
@CircuitBreaker(name = "userService", fallbackMethod = "fallback")
public User getUser(Long id) { ... }

private User fallback() {  // Exception 파라미터 없음!
    return User.unknown();
}

// ✅ 좋은 예
private User fallback(Long id, Exception e) {  // 원본 파라미터 + Exception
    log.error("Fallback triggered", e);
    return User.unknown();
}

⚠️ 3. Circuit Breaker 남용

// ❌ 나쁜 예: 내부 메서드에 Circuit Breaker
@CircuitBreaker(name = "local")
private void internalMethod() {
    // 내부 메서드는 불필요!
}

// ✅ 좋은 예: 외부 의존성에만 적용
@CircuitBreaker(name = "externalApi")
public ApiResponse callExternalApi() {
    // 외부 API 호출
}

연습 (추천)

  1. Circuit Breaker 구현

    • Resilience4j 설정
    • @CircuitBreaker 적용
    • Fallback 메서드 작성
  2. 상태 전이 테스트

    • 장애 발생 시켜 OPEN 상태 확인
    • 복구 후 CLOSED 상태 전환 확인
  3. 모니터링

    • Actuator로 상태 확인
    • Prometheus 메트릭 수집

요약

  • Circuit Breaker는 장애 전파를 차단
  • CLOSED → OPEN → HALF_OPEN 상태 전이
  • Resilience4j로 쉽게 구현 가능
  • Fallback으로 우아한 성능 저하
  • 재시도, 타임아웃과 함께 조합

다음 단계

  • 분산 추적: /learning/deep-dive/deep-dive-distributed-tracing/
  • API Gateway: /learning/deep-dive/deep-dive-api-gateway/
  • 마이크로서비스 패턴: /learning/deep-dive/deep-dive-microservices-patterns/