Toss Open API Rate Limiter 적용 TDD

Background

백엔드는 5개의 Toss Gateway(Account, Alert/Stock/Holding/Order용 각 Toss 구현체)를 통해 Toss 증권 Open API를 호출한다. 현재 구현에는 401 재시도만 있고 429(Too Many Requests) 방어 및 연속 실패에 대한 Circuit Breaker가 없다.

Overview

Resilience4j RateLimiter + CircuitBreaker를 공통 레이어에 도입해 Toss API 호출을 보호한다. 각 API 그룹별 TPS 한도를 설정하고, 한도 초과 시 즉시 실패(큐잉 없음)로 상위 레이어에 TossRateLimitException 을 throw한다. 연속 실패율 50% 초과 시 Circuit이 OPEN되어 Toss API 호출 자체를 30초간 차단한다.

Terminology

용어설명
RateLimiter지정된 주기에 허용된 횟수만큼만 호출을 허용하는 Resilience4j 컴포넌트
CircuitBreaker실패율이 임계치를 넘으면 회로를 열어 Fast Fail하는 Resilience4j 컴포넌트
API 그룹Toss Open API가 Rate Limit를 적용하는 단위 (MARKET_DATA, STOCK, ACCOUNT, ASSET, ORDER, AUTH)
RequestNotPermittedRateLimiter 한도 초과 시 throw되는 Resilience4j 예외
TossRateLimitExceptionRequestNotPermitted 를 래핑해 도메인 경계에서 사용하는 애플리케이션 예외
CallNotPermittedExceptionCircuitBreaker OPEN 시 throw되는 Resilience4j 예외
TossCircuitOpenExceptionCallNotPermittedException 을 래핑한 애플리케이션 예외

Define Problem

AS-IS

Gateway.fetchPrices()
  └─ retryOn401 { RestClient.get("/api/v1/prices") }
  • 429 응답 → 스택 트레이스 로그 → 정제되지 않은 5xx 반환
  • 연속 Toss API 장애 시 매 폴링마다 커넥션 소모
  • 폴링 주기 30초 × ACTIVE 알림 N개 → N 요청/30s (현재는 작지만 확장 시 문제)

TO-BE

Gateway.fetchPrices()
  └─ CircuitBreaker.executeSupplier {
       RateLimiter.executeSupplier {
         retryOn401 { RestClient.get("/api/v1/prices") }
       }
     }
  • 한도 초과 → TossRateLimitException (구조화된 에러)
  • 연속 실패 → CircuitBreaker OPEN → Fast Fail (Toss API 부하 차단)
  • 설정값을 application.yml 에서 외부화

Possible Solutions

벤치마킹 참조 제품

제품명카테고리참조 패턴
Resilience4jJava/Kotlin 회복탄력성 라이브러리RateLimiter + CircuitBreaker 조합
Bucket4jJava 토큰 버킷 구현토큰 버킷 알고리즘
Spring Cloud Gateway RateLimiterHTTP Gateway 레벨Redis 기반 분산 Rate Limit

방안 비교

방안설명왜 채택 / 미채택
Resilience4j (채택)Gateway 호출 코드에 rateLimiter.executeSupplier { } 래핑Spring Boot autoconfigure와 통합, YAML 설정 외부화, CircuitBreaker 조합이 자연스럽다
Bucket4j토큰 버킷 알고리즘 직접 구현직접 구현 비용 높음, Resilience4j와 중복 도입 불필요
Spring Cloud GatewayHTTP 필터 레이어현재 Spring MVC 기반이고, API Gateway 별도 도입은 과도한 복잡도

결정: Resilience4j 단일 라이브러리로 RateLimiter + CircuitBreaker 동시 해결. Gateway 구현에 AOP 없이 명시적 래핑을 적용해 테스트 가능성 확보.

Detail Design

처리 흐름

sequenceDiagram
    participant C as UseCase / Scheduler
    participant G as TossXxxGateway
    participant F as TossRateLimiterFacade
    participant CB as CircuitBreaker(toss)
    participant RL as RateLimiter(group)
    participant T as Toss Open API

    C->>G: fetchPrices(symbols)
    G->>F: execute(MARKET_DATA) { block }
    F->>CB: executeSupplier
    CB->>RL: executeSupplier
    RL-->>F: RequestNotPermitted → TossRateLimitException
    CB->>T: RestClient.get("/api/v1/prices")
    T-->>CB: 200 OK
    CB-->>G: StockPrice list
    G-->>C: List<StockPrice>

클래스 의존

flowchart LR
    subgraph Presentation
        EH[GlobalExceptionHandler]
    end
    subgraph Infrastructure
        G1[TossStockPriceGateway]
        G2[TossStockGatewayImpl]
        G3[TossHoldingGateway]
        G4[TossOrderGateway]
        G5[TossAccountGateway]
        FA[TossRateLimiterFacade]
        CFG[TossRateLimiterConfig]
    end
    subgraph Common_Exception
        EX1[TossRateLimitException]
        EX2[TossCircuitOpenException]
    end

    G1 --> FA
    G2 --> FA
    G3 --> FA
    G4 --> FA
    G5 --> FA
    FA --> CFG
    FA --> EX1
    FA --> EX2
    EH --> EX1
    EH --> EX2

AS-IS / TO-BE 비교

항목AS-ISTO-BE
429 처리미처리 (스택 트레이스)TossRateLimitException throw
연속 실패 방어없음CircuitBreaker OPEN
설정 외부화없음application.yml resilience4j 섹션
예외 HTTP 매핑500429 / 503

ERD

변경 없음 — DB 스키마 영향 없는 인프라 레이어 변경.

Testing Plan

  • MARKET_DATA 그룹 10 req 연속 성공 → 11번째 TossRateLimitException throw 확인 (infrastructure)
  • ACCOUNT 그룹 1 req 성공 → 2번째 TossRateLimitException throw 확인 (infrastructure)
  • CircuitBreaker 실패율 50% 초과 (10건 중 5건 실패) → OPEN 전이 확인 (infrastructure)
  • CircuitBreaker OPEN 상태에서 TossCircuitOpenException 즉시 throw 확인 (infrastructure)
  • TossRateLimitException → HTTP 429, TossCircuitOpenException → HTTP 503 매핑 확인 (presentation)

Release Scenario

  1. build.gradle.kts 에 Resilience4j 의존성 추가 후 빌드 확인
  2. application.yml 에 resilience4j 설정 추가
  3. TossRateLimiterConfig / TossRateLimiterFacade / 예외 클래스 신규 생성
  4. 각 Gateway에 TossRateLimiterFacade 주입 후 래핑 적용
  5. GlobalExceptionHandler 에 예외 매핑 추가
  6. 통합 테스트 통과 후 PR 생성

롤백: TossRateLimiterFacade 코드를 제거하고 Gateway 원복 시 기존 동작으로 즉시 복구 가능. 설정만 추가되므로 스키마 롤백 불필요.

Project Information

  • 담당자: biuea
  • Jira Epic: STK6

Document History

날짜변경 내용작성자
2026-06-19초안 작성Claude
2026-06-21스킬 포맷 적용Claude