반응형
이커머스 개발을 하며 외부 PG 서버의 지연과 오류가 내 서비스 전체 응답 속도를 악화시키는 문제가 발생하였다.
나는 이를 해결하기 위해 Resilience4j 서킷 브레이커(Circuit Breaker)을 사용해 외부 장애가 내부 서비스까지 확산되는 것을 차단하였다.
아키텍처 개요
- 호출 흐름: commerce-api → PG(Feign) → 콜백(PG → commerce-api)
- 보상 흐름: 콜백 실패/지연 시, PENDING 건을 일정 주기로 동기화(폴링)하여 상태 확정
서킷 브레이커를 사용한 이유?
- Retry 회복 전략은 잘못 사용시 서버에 부하를 주거나 DoS 공격처럼 동작할 수 있다.
- 서킷 브레이커는 “실패율이 임계치를 넘으면 빠르게 실패”시켜 전체 시스템을 보호한다 (누전 차단기의 역할)
- 서킷 브레이커는 half open을 통해 몇 개의 요청만 통과 시키고 다시 회로를 닫거나 유지하는 방식을 채택할 수 있다.
resilience4j:
circuitbreaker:
configs:
default:
sliding-window-size: 10 # 가장 최근 10번 호출에 대해서
failure-rate-threshold: 50 # 실패율이 50% 넘으면 Open
wait-duration-in-open-state: 10s # Open 상태 유지 시간
permitted-number-of-calls-in-half-open-state: 2 #half open으로 전환되었을 때 몇개 흘려보낼거야?
slow-call-duration-threshold: 2s # 2초가 넘어가면, 좀 느린거 같은데? 최근 50%에 대해서, 문제로 판단한다.
slow-call-rate-threshold: 50 # 느린 호출이 50% 넘으면 Open
record-exceptions:
- feign.FeignException
- java.net.SocketTimeoutException
- java.io.IOException
instances:
pgCircuit:
base-config: default
retry:
configs:
default:
max-attempts: 3 # 최대 3번 시도 (초기 호출 포함)
wait-duration: 500ms # 재시도 간격
retry-exceptions:
- feign.RetryableException
- feign.FeignException
- java.net.SocketTimeoutException
- java.lang.Exception # 모든 예외에 대해 재시도 (ignore-exceptions 제외)
ignore-exceptions:
- com.loopers.support.error.CoreException
resilience4.yml을 통해 서킷 브레이커와 Retry에 대한 설정을 하였다.
서비스 레이어에 애노테이션을 적용하고 fallback으로 PENDING을 저장했다.
@Retry(name = "pgPayment")
@CircuitBreaker(name = "pgPayment", fallbackMethod = "createPaymentFallback")
public Payment createPayment(String userId, String orderId, BigDecimal amount, String cardType, String cardNo) {
var req = PgDto.Request.builder()
.orderId(orderId).cardType(cardType).cardNo(cardNo)
.amount(amount.toString())
.callbackUrl("http://localhost:8080/api/v1/payments/callback")
.build();
var res = pgClient.requestPayment(userId, req); // Feign
return paymentRepository.save(
Payment.builder()
.transactionKey(res.data().transactionKey())
.orderId(orderId).userId(userId).amount(amount)
.status(PaymentStatus.PENDING).cardType(cardType).cardNo(cardNo)
.build()
);
}
private Payment createPaymentFallback(String userId, String orderId, BigDecimal amount, String cardType, String cardNo, Throwable t) {
String tempKey = "TEMP-" + java.util.UUID.randomUUID();
return paymentRepository.save(
Payment.builder()
.transactionKey(tempKey)
.orderId(orderId).userId(userId).amount(amount)
.status(PaymentStatus.PENDING).cardType(cardType).cardNo(cardNo)
.build()
);
}
작업을 하다보니 서킷 브레이커의 설정을 어떻게 하느냐가 전체적인 성능을 결정 하는 것을 알게 되었다.
반응형