성능 최적화 23편 - Resilience4j 서킷 브레이커로 장애 전파 막기
21편과 22편에서 Read Replica와 샤딩으로 시스템을 여러 대로 나누는 확장 전략을 다뤘습니다. 시스템이 여러 컴포넌트로 나뉘면 나뉠수록, 하나가 장애를 겪었을 때 그 여파가 다른 곳까지 번지지 않도록 막는 것이 중요해집니다. 이번 글에서는 그 방어 장치인 서킷 브레이커(Circuit Breaker) 패턴을 정리합니다.
1. 장애가 전파되는 상황
11편에서 다룬 결제 API 연동을 다시 가져와보겠습니다. 결제 서버에 장애가 생겨 응답이 극도로 느려졌다고 가정합니다.
public OrderDetailDto getOrderDetail(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
PaymentInfo paymentInfo = paymentClient.getPaymentInfo(order.getPaymentId()); // 응답이 30초씩 걸림
return OrderDetailDto.of(order, paymentInfo);
}
결제 서버가 응답을 안 주면, 이 API를 호출한 스레드는 타임아웃이 날 때까지 계속 대기 상태로 묶입니다. 문제는 이런 요청이 계속 쌓이면, 우리 서버의 스레드 풀 자체가 고갈된다는 점입니다.
결제 서버 장애 발생
-> 결제 정보 조회 요청들이 전부 응답 대기 상태로 쌓임
-> 스레드 풀(7편에서 다룬 톰캣/HikariCP 스레드 포함) 고갈
-> 결제와 무관한 다른 API(상품 조회 등)까지 처리 못 하게 됨
-> 서버 전체 장애로 확산
결제 서버 하나의 장애가, 아무 관련 없는 상품 조회 기능까지 마비시키는 장애 전파(Cascading Failure)가 발생하는 것입니다.
2. 서킷 브레이커의 개념
서킷 브레이커는 전기 회로의 차단기에서 이름을 따왔습니다. 특정 서비스 호출이 반복적으로 실패하면, 회로를 아예 열어(Open) 더 이상 그 서비스로 요청을 보내지 않고 즉시 실패 처리해버립니다.
CLOSED (정상)
│ 실패율이 임계치를 넘음
▼
OPEN (차단)
│ 일정 시간 경과
▼
HALF_OPEN (일부 요청만 시도)
│ 성공하면 CLOSED로, 실패하면 다시 OPEN으로
▼
CLOSED 또는 OPEN
- CLOSED : 평소 상태, 모든 요청이 정상적으로 전달됩니다.
- OPEN : 실패율이 임계치를 넘으면 회로가 열리고, 이후 요청은 실제 호출 없이 즉시 실패(또는 대체 응답) 처리됩니다.
- HALF_OPEN : 일정 시간이 지나면 일부 요청만 실제로 보내보고, 정상 회복 여부를 확인합니다.
핵심은, 이미 응답이 느려진 서버에 계속 요청을 쌓아두는 대신 빠르게 실패시켜서 우리 시스템의 자원을 보호하는 것입니다.
3. Resilience4j 적용하기
implementation 'io.github.resilience4j:resilience4j-spring-boot3'
resilience4j:
circuitbreaker:
instances:
paymentService:
sliding-window-size: 20 # 최근 20개 요청을 기준으로 판단
failure-rate-threshold: 50 # 실패율 50% 넘으면 OPEN
wait-duration-in-open-state: 10s # OPEN 상태 유지 시간
permitted-number-of-calls-in-half-open-state: 5
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentClient paymentClient;
@CircuitBreaker(name = "paymentService", fallbackMethod = "getPaymentInfoFallback")
public PaymentInfo getPaymentInfo(Long paymentId) {
return paymentClient.getPaymentInfo(paymentId);
}
public PaymentInfo getPaymentInfoFallback(Long paymentId, Throwable t) {
log.warn("결제 서비스 장애로 대체 응답 반환, paymentId={}", paymentId, t);
return PaymentInfo.unavailable(); // 기본값 또는 안내 메시지로 대체
}
}
최근 20개 요청 중 50% 이상이 실패하면 서킷이 열리고, 이후 요청들은 결제 서버를 호출하지 않고 즉시 getPaymentInfoFallback이 실행됩니다. 사용자에게는 "결제 정보를 일시적으로 불러올 수 없습니다" 같은 대체 응답을 보여주면서도, 서버 자체는 계속 정상 동작합니다.
4. Timeout, Retry와 함께 조합하기
서킷 브레이커 단독으로는 부족하고, Timeout과 Retry를 함께 조합해야 실효성이 생깁니다.
resilience4j:
timelimiter:
instances:
paymentService:
timeout-duration: 3s # 3초 넘으면 타임아웃 처리
retry:
instances:
paymentService:
max-attempts: 2
wait-duration: 500ms
@CircuitBreaker(name = "paymentService", fallbackMethod = "getPaymentInfoFallback")
@Retry(name = "paymentService")
@TimeLimiter(name = "paymentService")
public CompletableFuture<PaymentInfo> getPaymentInfoAsync(Long paymentId) {
return CompletableFuture.supplyAsync(() -> paymentClient.getPaymentInfo(paymentId));
}
이렇게 조합하면 다음과 같은 순서로 방어가 이루어집니다.
1. 3초 넘게 응답 없으면 Timeout으로 처리 (무한 대기 방지)
2. 실패 시 짧은 간격으로 최대 2번 재시도
3. 재시도까지 실패한 비율이 누적되면 서킷이 OPEN
4. OPEN 상태에서는 재시도조차 하지 않고 즉시 fallback 응답
5. 서킷 브레이커 상태 모니터링하기
management:
endpoints:
web:
exposure:
include: health, circuitbreakers
health:
circuitbreakers:
enabled: true
Actuator를 함께 연동하면, 각 서킷 브레이커의 현재 상태(CLOSED/OPEN/HALF_OPEN)와 실패율을 지표로 노출할 수 있습니다. 8편에서 다룬 APM이나 대시보드와 연결해서, 서킷이 열리는 순간 알림을 받도록 구성하면 장애를 빠르게 인지할 수 있습니다.
6. Fallback 설계 시 주의할 점
- Fallback은 단순히 예외를 삼키는 것이 아니라, 사용자에게 의미 있는 대체 경험을 제공해야 합니다. 결제 정보 대신 "확인 중" 상태를 보여주거나, 캐시(3편)에 남아있는 이전 값을 대신 보여주는 방식이 좋은 예입니다.
- Fallback 로직 자체가 무거우면 안 됩니다. 이미 장애 상황에서 실행되는 코드이므로, 최대한 가볍고 안전하게 작성해야 합니다.
7. 정리
- 외부 시스템의 장애는 방치하면 우리 시스템의 스레드 풀을 고갈시켜 전체 장애로 번질 수 있다
- 서킷 브레이커는 실패율이 임계치를 넘으면 회로를 열어, 더 이상의 요청을 차단하고 즉시 실패 처리함으로써 자원을 보호한다
- Timeout, Retry, Circuit Breaker를 함께 조합해야 실질적인 장애 방어 효과가 생긴다
- Fallback은 단순 실패 처리가 아니라, 사용자에게 최소한의 의미 있는 응답을 제공하도록 설계해야 한다
21~22편에서 시스템을 확장하는 방법을 다뤘다면, 이번 편은 그렇게 확장된 여러 컴포넌트 사이의 연결 지점을 안전하게 지키는 방법입니다. 시스템이 복잡해질수록, 무언가를 더 빠르게 만드는 것만큼이나 "무엇이 실패해도 전체가 무너지지 않게 만드는 것"이 중요해집니다.
'성능 최적화' 카테고리의 다른 글
| 성능 최적화 STEP 25 - 분산 트레이싱(Distributed Tracing) (0) | 2026.07.23 |
|---|---|
| 성능 최적화 STEP 24 - Rate Limiting (0) | 2026.07.21 |
| 성능 최적화 STEP 22 - 샤딩(Sharding) (1) | 2026.07.20 |
| 성능 최적화 STEP 21 - Read Replica (1) | 2026.07.20 |
| 성능 최적화 STEP 20 - 네트워크 비용 줄이기 (0) | 2026.07.15 |