Resilience4j — 서킷 브레이커, 리트라이, 벌크헤드로 장애를 격리하는 방법
외부 결제 API가 응답을 안 하면, 우리 서비스도 같이 멈춰야 하는 걸까요?
마이크로서비스 환경에서 하나의 서비스 장애가 연쇄적으로 전체 시스템을 마비시키는 것을 "장애 전파(Cascading Failure)"라고 합니다. Resilience4j는 서킷 브레이커, 리트라이, 벌크헤드 등의 패턴으로 이런 장애를 격리하고 시스템의 탄력성을 높여줍니다.
Resilience4j란
Resilience4j는 Java 애플리케이션을 위한 경량 장애 허용 라이브러리입니다. Netflix Hystrix의 후속으로, Java 8+ 함수형 프로그래밍을 기반으로 설계되었습니다.
제공하는 핵심 모듈:
- CircuitBreaker: 장애 감지 후 요청 차단
- Retry: 실패 시 자동 재시도
- RateLimiter: 요청 속도 제한
- Bulkhead: 동시 요청 수 제한
- TimeLimiter: 타임아웃 설정
의존성
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
Circuit Breaker — 서킷 브레이커
상태 머신
실패율 ≥ 임계값
CLOSED ──────────────→ OPEN
↑ │
│ 성공률 ≥ 임계값 │ 대기 시간 경과
│ ↓
└──────────────── HALF_OPEN
실패율 ≥ 임계값 → OPEN
- CLOSED: 정상 상태. 모든 요청 통과. 실패율 모니터링 중
- OPEN: 차단 상태. 모든 요청 즉시 실패. 백엔드 보호
- HALF_OPEN: 탐색 상태. 제한된 요청만 통과시켜 복구 여부 확인
설정
resilience4j:
circuitbreaker:
instances:
paymentService:
slidingWindowType: COUNT_BASED
slidingWindowSize: 10 # 최근 10개 요청 기준
failureRateThreshold: 50 # 실패율 50% 이상이면 OPEN
waitDurationInOpenState: 30s # OPEN 상태 유지 시간
permittedNumberOfCallsInHalfOpenState: 5 # HALF_OPEN에서 허용할 요청 수
minimumNumberOfCalls: 5 # 최소 5번은 호출해야 판단
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
ignoreExceptions:
- com.example.BusinessException # 비즈니스 예외는 실패로 카운트하지 않음
어노테이션 사용
@Service
public class PaymentService {
private final PaymentClient paymentClient;
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResult processPayment(PaymentRequest request) {
return paymentClient.pay(request);
}
// Fallback 메서드 — 파라미터 + Throwable
private PaymentResult paymentFallback(PaymentRequest request, Throwable t) {
log.warn("결제 서비스 장애로 Fallback 실행: {}", t.getMessage());
return PaymentResult.pending(request.getOrderId(),
"결제 서비스가 일시적으로 이용 불가합니다. 잠시 후 자동으로 재시도됩니다.");
}
}
Fallback 메서드는 원본 메서드와 ** 같은 파라미터 + Throwable**을 받아야 합니다.
Retry — 자동 재시도
일시적인 장애(네트워크 글리치 등)에 대해 자동으로 재시도합니다.
resilience4j:
retry:
instances:
paymentService:
maxAttempts: 3 # 최대 3번 시도
waitDuration: 1s # 재시도 간격
exponentialBackoffMultiplier: 2 # 지수 백오프 (1s → 2s → 4s)
retryExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
ignoreExceptions:
- com.example.BusinessException
@Retry(name = "paymentService", fallbackMethod = "retryFallback")
public PaymentResult processPayment(PaymentRequest request) {
return paymentClient.pay(request);
}
CircuitBreaker + Retry 조합
Retry를 CircuitBreaker 안에서 사용하면 "재시도해도 안 되면 서킷을 열어" 패턴을 구현할 수 있습니다.
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")
@Retry(name = "paymentService")
public PaymentResult processPayment(PaymentRequest request) {
return paymentClient.pay(request);
}
실행 순서: Retry → CircuitBreaker. Retry가 모든 재시도를 소진한 후에도 실패하면 CircuitBreaker에 실패로 기록됩니다.
RateLimiter — 속도 제한
resilience4j:
ratelimiter:
instances:
externalApi:
limitForPeriod: 100 # 주기당 허용 요청 수
limitRefreshPeriod: 1s # 갱신 주기
timeoutDuration: 500ms # 허용 대기 시간
@RateLimiter(name = "externalApi")
public ExternalData callExternalApi() {
return externalClient.getData();
}
외부 API의 Rate Limit이 있을 때, 우리 쪽에서 미리 속도를 제한하여 429 에러를 방지합니다.
Bulkhead — 격벽
동시 호출 수를 제한하여 특정 서비스 호출이 전체 스레드 풀을 점유하는 것을 방지합니다.
세마포어 방식
resilience4j:
bulkhead:
instances:
paymentService:
maxConcurrentCalls: 20 # 최대 동시 호출 20개
maxWaitDuration: 500ms # 대기 시간
@Bulkhead(name = "paymentService")
public PaymentResult processPayment(PaymentRequest request) {
return paymentClient.pay(request);
}
스레드 풀 방식
resilience4j:
thread-pool-bulkhead:
instances:
paymentService:
maxThreadPoolSize: 10
coreThreadPoolSize: 5
queueCapacity: 20
| 방식 | 격리 수준 | 오버헤드 | 비동기 지원 |
|---|---|---|---|
| 세마포어 | 동시 호출 수 제한 | 낮음 | 제한적 |
| 스레드 풀 | 별도 스레드 풀 격리 | 높음 | 우수 |
TimeLimiter — 타임아웃
비동기 호출에 타임아웃을 설정합니다.
resilience4j:
timelimiter:
instances:
paymentService:
timeoutDuration: 3s # 3초 타임아웃
cancelRunningFuture: true # 타임아웃 시 실행 중인 Future 취소
@TimeLimiter(name = "paymentService")
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")
public CompletableFuture<PaymentResult> processPaymentAsync(PaymentRequest request) {
return CompletableFuture.supplyAsync(() -> paymentClient.pay(request));
}
모니터링
Resilience4j는 Actuator, Micrometer와 통합되어 상태를 모니터링할 수 있습니다.
management:
endpoints:
web:
exposure:
include: health, circuitbreakers, retries
health:
circuitbreakers:
enabled: true
GET /actuator/circuitbreakers
GET /actuator/circuitbreakerevents
Prometheus/Grafana와 연동하면 서킷 상태, 실패율, 요청 수 등을 시각화할 수 있습니다.
실무 패턴 조합
@Service
public class OrderService {
// 결제: 재시도 → 서킷 브레이커 → 타임아웃
@TimeLimiter(name = "payment")
@CircuitBreaker(name = "payment", fallbackMethod = "paymentFallback")
@Retry(name = "payment")
@Bulkhead(name = "payment")
public CompletableFuture<PaymentResult> processPayment(PaymentRequest request) {
return CompletableFuture.supplyAsync(() -> paymentClient.pay(request));
}
// Fallback
private CompletableFuture<PaymentResult> paymentFallback(
PaymentRequest request, Throwable t) {
log.warn("결제 Fallback: {}", t.getMessage());
return CompletableFuture.completedFuture(
PaymentResult.pending(request.getOrderId()));
}
}
** 실행 순서 **: Retry → CircuitBreaker → RateLimiter → TimeLimiter → Bulkhead → 실제 호출
실무 팁
- slidingWindowSize 와 failureRateThreshold 는 트래픽 패턴에 맞게 조절하세요
- Fallback에서 **대안 로직 **(캐시 반환, 큐에 적재 등)을 구현하세요
- ignoreExceptions 로 비즈니스 예외(잘못된 입력 등)는 실패로 카운트하지 마세요
- Retry에 지수 백오프 를 반드시 설정하여 장애 서비스에 부하를 주지 마세요
주의할 점
1. Retry와 CircuitBreaker를 조합할 때 어노테이션 순서에 따라 동작이 달라진다
Resilience4j에서 어노테이션 순서에 따라 실행 순서가 결정됩니다. Retry가 CircuitBreaker 바깥에 있으면 서킷이 열린 상태에서도 재시도가 발생하여 불필요한 CallNotPermittedException이 반복됩니다. 일반적으로 Retry → CircuitBreaker 순서(Retry가 안쪽)가 적절합니다.
2. Fallback 메서드의 시그니처가 원본과 다르면 런타임에 예외가 발생한다
Fallback 메서드는 원본 메서드와 동일한 파라미터 + Throwable(또는 특정 예외 타입)을 받아야 합니다. 파라미터 타입이나 순서가 다르면 NoSuchMethodException이 발생하는데, 컴파일 시점에는 잡히지 않아 운영 중에 장애가 발생할 수 있습니다.
3. ignoreExceptions를 설정하지 않으면 비즈니스 예외도 실패로 카운트된다
잘못된 입력(400 Bad Request)이나 인증 실패(401) 같은 비즈니스 예외가 서킷 브레이커의 실패율에 포함되면, 정상적인 요청 거부가 많아질 때 서킷이 열려서 모든 요청이 차단됩니다. 비즈니스 예외는 반드시 ignoreExceptions에 등록하여 실패 카운트에서 제외하세요.
정리
- CircuitBreaker 는 CLOSED → OPEN → HALF_OPEN 상태 전환으로 장애를 감지하고 격리합니다
- Retry 는 일시적 장애에 자동 재시도하며, 지수 백오프로 부하를 분산합니다
- Bulkhead 는 동시 호출 수를 제한하여 장애 전파를 방지합니다
- 실무에서는 여러 패턴을 조합 하여 사용하며, Fallback에서 대안 로직을 제공합니다