


결과적으로 서비스 가용성이 급격히 낮아짐
아래와 같은 상황을 해결하기 위해 서킷 브레이커를 사용해야함
sequenceDiagram
autonumber
actor C as Client
participant O as Order Service
participant P as Payment Service
participant PG as External PG Server
%% ===== 서버 응답 실패 =====
P--x O: 결제 응답 지연/실패 (타임아웃)
O--x C: 클라이언트 응답 지연/실패
Note over C: 응답을 못 받아서<br/>주문하기 버튼 반복 클릭 (재시도)
%% ===== 반복 요청으로 장애 심화 =====
rect rgba(255, 0, 0, 0.15)
loop 사용자 반복 클릭 / 클라이언트 재시도
C->>O: 주문하기 재요청
O->>P: 결제 재요청
P->>PG: 결제 승인 재요청
PG--x P: No Response / Timeout
end
end
%% ===== 서버 자원 고갈 =====
Note over P: Payment 쓰레드 / 커넥션 풀 고갈
Note over O: Order 서비스 요청 대기 누적
Note over C,O: 장애가 연쇄적으로 확산<br/>(전체 서비스 가용성 급격히 저하)

sequenceDiagram
autonumber
actor C as Client
participant O as Order Service
participant P as Payment Service
participant CB as Circuit Breaker
participant PG as External PG Server
Note over PG: PG 장애 발생(지연/타임아웃)
C->>O: 주문하기 요청
O->>P: 결제 요청
P->>CB: PG 호출 시도
CB->>PG: 결제 승인 요청
PG--x CB: (No Response / Timeout)
CB-->>P: 실패 기록(에러/타임아웃 카운트)
Note over CB: 임계치 초과 시 OPEN 전환
CB-->>P: (OPEN) 즉시 실패 반환
P-->>O: 빠른 실패 응답 + Fallback(예: 결제 실패/대기 안내)
O-->>C: 즉시 응답(빠른 실패/안내)
Note over P,O: 외부 장애 동안에도<br/>내부 쓰레드/커넥션 보호
서킷 브레이커가 닫혀 있음
- 초기 상태이자 정상 동작 상태
- 모든 서비스 요청이 정상적으로 수행됨
- 요청 성공/실패/지연을 지속적으로 기록
- 특정 기간 동안 실패율이 임계값(threshold) 을 초과하면
→ **OPEN 상태로 전환**
waitDurationInOpenState)이 지나면 → HALF_OPEN 상태로 전환서킷 브레이커에서 중요한 개념 중 개념
- 다른 서비스 호출이 실패했을 때, 우리 서비스가 스스로 선택하는 대체 행동
- 서킷 브레이커가 OPEN일 때만 실행되는 게 아니라,
- 호출 실패
- 타임아웃
- 예외 발생
등 **정상 응답을 받지 못한 모든 경우에 실행될 수 있음**
Fallback은 이 호출이 정상 결과를 못 냈을 때 실행되는 대체 로직
즉, 서킷 브레이커 상태와는 직접적인 1:1 관계가 아님.
어느 서비스에 어떤 Fallback를 사용하는지 중요
정합성이 중요한 영역
UX가 중요한 영역
Retry : 요청 실패 시 재시도 처리 기능 제공
RateLimiter : 제한치를 넘어서 요청을 거부하거나 Queue 생성하여 처리하는 기능 제공
TimeLimiter : 실행 시간제한 설정 기능 제공
Bulkhead : 동시 실행 횟수 제한 기능 제공
결제 시스템에서 TossPayments API 의존성이 높아 서킷 브레이커가 필요
dependencies {
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
implementation 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0'
implementation 'io.github.resilience4j:resilience4j-retry:2.2.0'
implementation 'io.github.resilience4j:resilience4j-ratelimiter:2.2.0'
implementation 'io.github.resilience4j:resilience4j-bulkhead:2.2.0'
implementation 'io.github.resilience4j:resilience4j-timelimiter:2.2.0'
}
resilience4j:
circuitbreaker:
instances:
tossPaymentApi: # 서킷브레이커의 이름
failure-rate-threshold: 50 # 실패 비율의 임계치
slow-call-rate-threshold: 80 # 느린 호출의 임계치
slow-call-duration-threshold: 2000ms # 느린 호출로 간주할 시간 값
minimum-number-of-calls: 5 # 총 집계가 유효해 지는 최소한의 요청 수
sliding-window-type: COUNT_BASED # 서킷브레이커의 타입을 지정한다. TIME_BASED, COUNT_BASED 중 택 1
sliding-window-size: 10 # 시간은 단위 초, 카운트는 단위 요청 수
wait-duration-in-open-state: 30s # OPEN에서 HALF_OPEN으로 상태변이가 실행되는 최소 대기 시간
permitted-number-of-calls-in-half-open-state: 3 # HALF_OPEN 상태에서 총 집계가 유효해지는 최소한의 요청 수
automatic-transition-from-open-to-half-open-enabled: true
register-health-indicator: true # 해당 값에 기재한 exception은 모두 실패로 집계
retry:
instances:
tossPaymentApi: # 🎯 TossPayments API 재시도
max-attempts: 2 # 최대 2회 시도 (원본 + 재시도 1회)
wait-duration: 500ms # 재시도 간격 500ms
enable-exponential-backoff: true # 지수 백오프 활성화
exponential-backoff-multiplier: 2 # 500ms → 1000ms
timelimiter:
instances:
tossPaymentApi: A # 🎯 TossPayments PI 타임아웃
timeout-duration: 5s # 외부 API 5초 타임아웃
cancel-running-future: true # 타임아웃 시 Future 취소
resilience4j:
circuitbreaker:
instances:
tossPaymentApi:
failure-rate-threshold: 50
slow-call-rate-threshold: 80
slow-call-duration-threshold: 2000ms
minimum-number-of-calls: 5
sliding-window-type: COUNT_BASED
sliding-window-size: 10
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 3
automatic-transition-from-open-to-half-open-enabled: true
register-health-indicator: true
retry:
instances:
tossPaymentApi:
max-attempts: 2
wait-duration: 500ms
enable-exponential-backoff: true
exponential-backoff-multiplier: 2
timelimiter:
instances:
tossPaymentApi:
timeout-duration: 5s
cancel-running-future: true
// retry -> circuitbreaker -> timelimiter 순서로 작동 //
<aside>
💡
주요 설정값
- failureRateThreshold : 호출 실패율에 대한 임곗값, sliding window 상에서 임곗값보다 실패율이 높아지면 상태가 OPEN
- slow-call-rate-threshold: 느린 호출의 임계치
- slow-call-duration-threshold: 느린 호출로 간주할 시간 값
- slidingWindowType: siliding window의 형태를 COUNT_BASED 로 설정할지, TIME_BASED로 설정할지 결정. 타입에 따라서 slidingWindowSize의 숫자가 의미하는 것이 호출 횟수 혹은 시간이 됨
- slidingWindowSize: sliding window의 크기를 설정. 타입에 따라서 slidingWindowSize의 숫자가 의미하는 것이 호출 횟수 혹은 시간이 됨
- waitDurationInOpenState: OPEN 상태에서 얼마나 기다린 후에, HALF_OPEN으로 전환할지를 설정
</aside>
- CLOSED 상태
- 최소 5번의 요청 후 판단
- 외부 API 50% 실패 시 OPEN 상태로 변환
- 2초 이상 = 느린 호출이 80% 시 OPEN 상태로 변환
- OPEN 상태
- 30초 후 복구 시도
- 복구 시 3번 테스트
### 3. TossPaymentsClient 구현
```java
@Component
@Slf4j
public class TossPaymentsClient {
/**
* 결제 승인 API 호출 (Circuit Breaker 적용)
*/
@CircuitBreaker(name = "tossPaymentApi", fallbackMethod = "confirmFallback")
public TossPaymentsConfirmResponse confirm(TossPaymentsConfirmRequest request) {
log.debug("🎯 Toss Payment API 호출 - 결제 승인: {}", request.getOrderId());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(HttpHeaders.AUTHORIZATION, buildAuthorizationHeader());
HttpEntity<TossPaymentsConfirmRequest> entity = new HttpEntity<>(request, headers);
String url = properties.getBaseUrl() + "/v1/payments/confirm";
return restTemplate.postForObject(url, entity, TossPaymentsConfirmResponse.class);
}
/**
* 결제 취소 API 호출 (Circuit Breaker 적용)
*/
@CircuitBreaker(name = "tossPaymentApi", fallbackMethod = "cancelFallback")
public TossPaymentsCancelResponse cancel(String paymentKey, TossPaymentsCancelRequest request) {
log.debug("🎯 Toss Payment API 호출 - 결제 취소: {}", paymentKey);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(HttpHeaders.AUTHORIZATION, buildAuthorizationHeader());
HttpEntity<TossPaymentsCancelRequest> entity = new HttpEntity<>(request, headers);
String url = properties.getBaseUrl() + "/v1/payments/" + paymentKey + "/cancel";
return restTemplate.postForObject(url, entity, TossPaymentsCancelResponse.class);
}
/**
* 결제 승인 Circuit Breaker Fallback
*
* 🎯 기존 동작 유지: 예외를 그대로 던져서 상위 레이어에서 처리
*/
public TossPaymentsConfirmResponse confirmFallback(TossPaymentsConfirmRequest request,
Exception ex) {
log.error("🚨 Toss Payment API Circuit Breaker 열림 - 결제 승인 실패: orderId={}, error={}",
request.getOrderId(), ex.getMessage());
// 기존 동작 유지: 원본 예외를 그대로 던짐
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new RuntimeException("결제 API 서비스가 불안정합니다. 잠시 후 다시 시도해주세요.", ex);
}
/**
* 결제 취소 Circuit Breaker Fallback
*
* 🎯 기존 동작 유지: 예외를 그대로 던져서 상위 레이어에서 처리
*/
public TossPaymentsCancelResponse cancelFallback(String paymentKey, TossPaymentsCancelRequest request, Exception ex) {
log.error("🚨 Toss Payment API Circuit Breaker 열림 - 결제 취소 실패: paymentKey={}, error={}",
paymentKey, ex.getMessage());
// 기존 동작 유지: 원본 예외를 그대로 던짐
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new RuntimeException("결제 취소 API 서비스가 불안정합니다. 잠시 후 다시 시도해주세요.", ex);
}
}