서킷 브레이커

김희영·2026년 1월 5일

토막개발지식

목록 보기
27/27

결제 시스템을 구현할 때 토스 같은 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
  ↑                                                
  └────────── (시험 호출 실패) ◀───────────┘

CODE

그럼 이제 구현 코드를 보자.
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
        );
    }
}
profile
내는 반드시 엄청난 개발자가 되고 말것어

0개의 댓글