Spring Resilience4j 서킷브레이커

Resilience4j란?

마이크로서비스 환경에서 외부 서비스 호출은 언제든 실패할 수 있다. Resilience4j는 Netflix Hystrix의 후속으로, 함수형 프로그래밍 스타일의 경량 장애 허용(fault tolerance) 라이브러리다. Spring Boot와의 통합이 매끄럽고, 서킷브레이커·재시도·벌크헤드·타임리미터·레이트리미터 5가지 핵심 패턴을 제공한다.

의존성 설정

Spring Boot 스타터로 간편하게 통합할 수 있다. AOP 기반 어노테이션과 Actuator 모니터링이 자동으로 활성화된다.

// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-aop")
    implementation("io.github.resilience4j:resilience4j-spring-boot3:2.2.0")
    implementation("org.springframework.boot:spring-boot-starter-actuator")
}

서킷브레이커 핵심 원리

서킷브레이커는 전기 회로의 차단기에서 착안한 패턴이다. 연속 실패가 임계치를 넘으면 회로를 열어(OPEN) 더 이상의 호출을 차단하고, 일정 시간 후 반열림(HALF_OPEN) 상태에서 시험 호출을 보내 복구 여부를 판단한다.

상태 동작 전이 조건
CLOSED 정상 호출 허용, 실패율 집계 실패율 ≥ 임계치 → OPEN
OPEN 모든 호출 즉시 차단, fallback 실행 대기 시간 경과 → HALF_OPEN
HALF_OPEN 제한된 시험 호출 허용 성공 → CLOSED / 실패 → OPEN

application.yml 설정 심화

Resilience4j의 강력함은 세밀한 설정에 있다. 슬라이딩 윈도우 방식, 실패율 임계치, 대기 시간 등을 서비스별로 독립 설정할 수 있다.

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

  retry:
    configs:
      default:
        maxAttempts: 3
        waitDuration: 1s
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
        retryExceptions:
          - java.io.IOException
    instances:
      paymentService:
        baseConfig: default
        maxAttempts: 5

  timelimiter:
    configs:
      default:
        timeoutDuration: 3s
        cancelRunningFuture: true
    instances:
      paymentService:
        timeoutDuration: 5s

  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 25
        maxWaitDuration: 500ms
    instances:
      paymentService:
        maxConcurrentCalls: 10

어노테이션 기반 적용

서비스 메서드에 어노테이션을 붙이는 것만으로 서킷브레이커, 재시도, 타임리미터를 조합할 수 있다. 어노테이션 적용 순서가 중요하다: Retry → CircuitBreaker → RateLimiter → TimeLimiter → Bulkhead 순으로 감싸진다.

@Service
@Slf4j
public class PaymentService {

    private final PaymentClient paymentClient;

    @CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
    @Retry(name = "paymentService")
    @TimeLimiter(name = "paymentService")
    @Bulkhead(name = "paymentService")
    public CompletableFuture<PaymentResponse> processPayment(PaymentRequest request) {
        return CompletableFuture.supplyAsync(() ->
            paymentClient.charge(request)
        );
    }

    // fallback: 동일 파라미터 + Throwable 추가
    private CompletableFuture<PaymentResponse> paymentFallback(
            PaymentRequest request, Throwable t) {
        log.warn("Payment circuit open, fallback triggered: {}", t.getMessage());
        return CompletableFuture.completedFuture(
            PaymentResponse.pending(request.getOrderId(),
                "결제 서비스 일시 장애. 잠시 후 재시도됩니다.")
        );
    }
}

프로그래밍 방식 서킷브레이커

어노테이션 대신 코드로 직접 서킷브레이커를 구성하면 동적 제어가 가능하다. 런타임에 서킷 상태를 조회하거나 강제 전이할 수 있어 운영 시 유용하다.

@Configuration
public class ResilienceConfig {

    @Bean
    public CircuitBreaker inventoryCircuitBreaker(CircuitBreakerRegistry registry) {
        CircuitBreaker cb = registry.circuitBreaker("inventoryService");

        cb.getEventPublisher()
            .onStateTransition(event ->
                log.info("CircuitBreaker '{}' 상태 전이: {} → {}",
                    event.getCircuitBreakerName(),
                    event.getStateTransition().getFromState(),
                    event.getStateTransition().getToState()))
            .onFailureRateExceeded(event ->
                log.error("실패율 임계치 초과: {}%",
                    event.getFailureRate()))
            .onSlowCallRateExceeded(event ->
                log.warn("느린 호출 비율 초과: {}%",
                    event.getSlowCallRate()));

        return cb;
    }
}

// 서비스에서 사용
@Service
public class InventoryService {

    private final CircuitBreaker circuitBreaker;
    private final InventoryClient client;

    public InventoryResponse checkStock(String sku) {
        return circuitBreaker.executeSupplier(() ->
            client.getStock(sku)
        );
    }

    // 운영 API: 강제 서킷 제어
    public void forceOpen() {
        circuitBreaker.transitionToForcedOpenState();
    }

    public void reset() {
        circuitBreaker.reset();
    }
}

Actuator 모니터링 통합

Resilience4j는 Spring Boot Actuator와 자동 통합되어 서킷브레이커 상태, 메트릭, 이벤트를 실시간으로 노출한다. Prometheus + Grafana와 연동하면 장애 패턴을 시각화할 수 있다.

# Actuator 엔드포인트 활성화
management:
  endpoints:
    web:
      exposure:
        include: health,circuitbreakers,circuitbreakerevents,metrics
  health:
    circuitbreakers:
      enabled: true

# 요청 예시
# GET /actuator/circuitbreakers
# → {"circuitBreakers":{"paymentService":{"state":"CLOSED","failureRate":-1.0}}}

# GET /actuator/circuitbreakerevents/paymentService
# → 최근 이벤트 목록 (성공/실패/상태전이)

# Prometheus 메트릭
# resilience4j_circuitbreaker_state{name="paymentService"} 0
# resilience4j_circuitbreaker_failure_rate{name="paymentService"} 12.5
# resilience4j_circuitbreaker_calls_seconds_count{name="paymentService",kind="successful"} 847

Spring Observation API 심화 글에서 다룬 Micrometer 기반 메트릭 체계와 결합하면, 서킷브레이커 상태 변화를 트레이스·로그와 함께 통합 관측할 수 있다.

Retry + CircuitBreaker 조합 전략

재시도와 서킷브레이커를 함께 쓸 때는 실행 순서를 이해해야 한다. Retry가 CircuitBreaker 안쪽에서 동작하므로, 3번 재시도 후 최종 실패가 서킷브레이커에 1건의 실패로 기록된다.

// 실행 흐름: Bulkhead → TimeLimiter → CircuitBreaker → Retry → 실제 호출
//
// 호출 시나리오:
// 1차 시도: IOException → Retry가 잡음
// 2차 시도: IOException → Retry가 잡음
// 3차 시도: 성공 → CircuitBreaker에 "성공 1건" 기록
//
// 만약 3차도 실패하면:
// → Retry 소진 → CircuitBreaker에 "실패 1건" 기록
// → 실패 누적 시 서킷 OPEN

// 커스텀 순서가 필요하면 aspect-order 조정
resilience4j:
  circuitbreaker:
    circuitBreakerAspectOrder: 1
  retry:
    retryAspectOrder: 2  # 숫자가 클수록 안쪽

WebClient 통합: 리액티브 서킷브레이커

WebClient와 함께 사용할 때는 리액티브 데코레이터를 적용한다. Spring R2DBC 리액티브 DB와 결합하면 전 구간 논블로킹 장애 허용 파이프라인을 구축할 수 있다.

@Service
public class ExternalApiService {

    private final WebClient webClient;
    private final CircuitBreakerRegistry registry;

    public Mono<ApiResponse> callExternalApi(String endpoint) {
        CircuitBreaker cb = registry.circuitBreaker("externalApi");

        return webClient.get()
            .uri(endpoint)
            .retrieve()
            .bodyToMono(ApiResponse.class)
            .timeout(Duration.ofSeconds(3))
            .transformDeferred(CircuitBreakerOperator.of(cb))
            .onErrorResume(CallNotPermittedException.class,
                e -> Mono.just(ApiResponse.cached()))
            .onErrorResume(TimeoutException.class,
                e -> Mono.just(ApiResponse.timeout()));
    }
}

테스트 전략

서킷브레이커 동작을 단위 테스트로 검증하는 것이 중요하다. 실패를 주입하고 상태 전이를 확인하는 패턴이다.

@SpringBootTest
class PaymentServiceTest {

    @Autowired
    private CircuitBreakerRegistry registry;

    @MockBean
    private PaymentClient paymentClient;

    @Autowired
    private PaymentService paymentService;

    @Test
    void 실패율_초과시_서킷이_열린다() {
        CircuitBreaker cb = registry.circuitBreaker("paymentService");

        // 연속 실패 주입
        when(paymentClient.charge(any()))
            .thenThrow(new HttpServerErrorException(HttpStatus.SERVICE_UNAVAILABLE));

        // minimumNumberOfCalls만큼 호출
        for (int i = 0; i < 10; i++) {
            try {
                paymentService.processPayment(new PaymentRequest()).get();
            } catch (Exception ignored) {}
        }

        assertThat(cb.getState()).isEqualTo(CircuitBreaker.State.OPEN);

        // OPEN 상태에서는 즉시 fallback
        PaymentResponse response = paymentService
            .processPayment(new PaymentRequest()).get();
        assertThat(response.getStatus()).isEqualTo("PENDING");
    }
}

마무리

Resilience4j는 마이크로서비스의 장애 전파를 차단하는 핵심 도구다. 서킷브레이커 단독이 아니라 Retry·Bulkhead·TimeLimiter와 조합해야 진정한 회복탄력성을 확보할 수 있다. 설정만 잘 해두면 서비스 장애가 전체 시스템 장애로 번지는 것을 효과적으로 막을 수 있다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux