결제 시스템을 구현할 때 토스 같은 PG사를 쓰면 참 좋다.
근데 문제가 하나 있다. 만약 토스가 죽으면?
이 문제를 해결해보자
PG사에서 결제를 하고, 그 결과를 우리의 앱이 받는다.
이때 서킷 브레이커는 실패율 임계치에 넘게 실패하면 요청을 못 보내게 닫아 버린다.
예를 들면 다음과 같다.
고객 1,2,3,4,5,6 번이 결제를 요청하고 있고,
minimumNumberOfCalls이 3,
실패율 임계치가 50%라고 하자.
이때 1번은 성공하고, 2,3번이 실패하면 실패율을 약 66%다.
1. OPEN
그러면 서킷 브레이커가 이를 감지해서 열린다.
이로인해 뒤에 오는 4,5,6번은 PG사(토스)로의 외부 호출을 차단해서 요청 없이 실패했다는 즉시 대체 응답(fallback)을 받을 수 있다.
예를 들면
“현재 결제 시스템이 불안정합니다. 잠시 후 다시 시도해 주세요.”
와 같은 응답을 사용자에게 제공할 수 있는거다.
2. Half-Open
그리고 일정 시간(waitDurationInOpenState)이 지나면 서킷 브레이커는 Half-Open 상태가 되어
일부 요청만 PG사로 보내 정상 동작 여부를 확인한다.
이 요청이 성공하면 다시 닫힌 상태(Closed)로 돌아가고,
실패하면 다시 열린 상태(Open)를 유지한다.
이렇게 하면 PG 장애가 우리 서버 장애로 전이되는 것을 막을 수 있다.
만약 서킷 브레이커가 없다면
느린 결제 응답으로 인해 스레드 풀이 고갈되고,
결제와 무관한 기능까지 함께 장애가 발생할 수 있으니 꼭 있는게 좋다.
Closed
↓ (실패율 초과)
Open ── waitDurationInOpenState ──▶ Half-Open
↑
└────────── (시험 호출 실패) ◀───────────┘
그럼 이제 구현 코드를 보자.
Resilience4j를 써서 할 수 있다.
당연하지만 설치부터 해야 한다.
build.gradle.kts
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'io.github.resilience4j:resilience4j-spring-boot3'
}
application.yml
resilience4j:
circuitbreaker:
instances:
tossPayment:
slidingWindowType: COUNT_BASED
slidingWindowSize: 10
minimumNumberOfCalls: 3
failureRateThreshold: 50
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 2
recordExceptions:
- java.net.SocketTimeoutException
- org.springframework.web.client.HttpServerErrorException
여기서 설정은 다음과 같다.
slidingWindowType : 슬라이딩 윈도우 타입
slidingWindowSize : 최근 10번 기준
minimumNumberOfCalls : 최소 3번부터 판단
failureRateThreshold : 실패율 50% 초과 시 Open
waitDurationInOpenState : Open 상태 10초 유지
permittedNumberOfCallsInHalfOpenState : Half-Open에서 2번만 시험 호출
참고로 slidingWindowType에서 COUNT_BASED가 아니라 TIME_BASED로 하면 최근 slidingWindowSize초간 발생한 호출 결과로 작동한다.
PaymentService
@Service
public class PaymentService {
private final TossPaymentClient tossPaymentClient;
public PaymentService(TossPaymentClient tossPaymentClient) {
this.tossPaymentClient = tossPaymentClient;
}
@CircuitBreaker(
name = "tossPayment",
fallbackMethod = "paymentFallback"
)
public PaymentResult pay(PaymentRequest request) {
return tossPaymentClient.requestPayment(request);
}
/**
* 서킷 브레이커 Open 상태이거나 예외 발생 시 호출
*/
public PaymentResult paymentFallback(
PaymentRequest request,
Exception e
) {
return PaymentResult.failed(
"PAYMENT_TEMPORARILY_UNAVAILABLE",
"현재 결제 시스템이 불안정합니다. 잠시 후 다시 시도해 주세요."
);
}
}
어노테이션 하나로 편리하게!
나 이런거 좋아해
엥 근데 TossPaymentClient 이거 뭐죠 싶을 수 있으니까 이것도 추가
@Component
public class TossPaymentClient {
private final RestTemplate restTemplate;
public TossPaymentClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public PaymentResult requestPayment(PaymentRequest request) {
return restTemplate.postForObject(
"https://api.tosspayments.com/v1/payments",
request,
PaymentResult.class
);
}
}