본문 바로가기

카테고리 없음

외부 PG 지연이 전체 응답을 망치던 문제를 서킷 브레이커로 해결하기

반응형

이커머스 개발을 하며 외부 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()
    );
}



작업을 하다보니 서킷  브레이커의 설정을 어떻게 하느냐가 전체적인 성능을 결정 하는 것을 알게 되었다.

반응형