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와 조합해야 진정한 회복탄력성을 확보할 수 있다. 설정만 잘 해두면 서비스 장애가 전체 시스템 장애로 번지는 것을 효과적으로 막을 수 있다.