Rate Limit이란?

Rate Limit(속도 제한)은 일정 시간 동안 API에 보낼 수 있는 요청 수를 제한하는 메커니즘입니다. API 서버를 보호하고, 모든 사용자에게 공정한 서비스를 제공하기 위해 거의 모든 API 서비스에서 적용합니다.

왜 Rate Limit이 필요한가요?
  • 서버 보호: 과도한 요청으로 인한 서버 과부하 방지
  • 공정한 사용: 한 사용자가 리소스를 독점하는 것 방지
  • 비용 관리: 요금제별 사용량 제어
  • 보안: 무차별 대입 공격(Brute Force) 방어

Rate Limit의 종류

1. 시간 단위별 제한

가장 일반적인 방식으로, 다양한 시간 단위로 요청 수를 제한합니다.

시간 단위 일반적인 제한 용도
분당 (RPM) 60~500회 버스트 트래픽 제어
시간당 (RPH) 1,000~10,000회 지속적인 사용량 제어
일일 (RPD) 5,000~50,000회 일일 할당량 관리
월간 (RPM) 10,000~500,000회 요금제 기반 제한

Bank API의 요금제별 제한은 다음과 같습니다:

플랜 분당 시간당 일일 월간
Free 60 1,000 5,000 10,000
Basic 100 3,000 15,000 100,000
Premium 500 10,000 50,000 500,000

2. 동시 연결 제한

동시에 열 수 있는 연결 수를 제한합니다. WebSocket이나 장시간 연결에서 주로 사용됩니다.

3. 대역폭 제한

전송할 수 있는 데이터 양을 제한합니다. 파일 다운로드나 대용량 응답에서 사용됩니다.

Rate Limit 응답 이해하기

429 Too Many Requests

Rate Limit에 도달하면 서버는 HTTP 429 상태 코드를 반환합니다.

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1736323200
Retry-After: 45

{
  "error": "rate_limit_exceeded",
  "message": "분당 요청 한도를 초과했습니다.",
  "limit": 60,
  "reset_at": "2026-01-08T10:00:00Z"
}

Rate Limit 관련 헤더

헤더 설명
X-RateLimit-Limit 현재 시간 창에서의 최대 요청 수
X-RateLimit-Remaining 남은 요청 수
X-RateLimit-Reset 제한이 리셋되는 Unix 타임스탬프
Retry-After 다음 요청까지 기다려야 할 초

Rate Limit 대응 전략

1. 지수 백오프 (Exponential Backoff)

재시도 간격을 지수적으로 늘려가는 방식입니다. 첫 번째 재시도는 1초 후, 두 번째는 2초 후, 세 번째는 4초 후... 이런 식으로 대기 시간을 늘립니다.

async function fetchWithRetry(url, options, maxRetries = 5) {
  let lastError;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (response.status === 429) {
        // Retry-After 헤더 확인
        const retryAfter = response.headers.get('Retry-After');
        const waitTime = retryAfter
          ? parseInt(retryAfter) * 1000
          : Math.pow(2, attempt) * 1000; // 지수 백오프

        console.log(`Rate limited. ${waitTime}ms 후 재시도... (${attempt + 1}/${maxRetries})`);
        await sleep(waitTime);
        continue;
      }

      return response;
    } catch (error) {
      lastError = error;
      const waitTime = Math.pow(2, attempt) * 1000;
      await sleep(waitTime);
    }
  }

  throw lastError || new Error('Max retries exceeded');
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

2. 지터 (Jitter) 추가

여러 클라이언트가 동시에 재시도하면 "thundering herd" 문제가 발생할 수 있습니다. 랜덤한 지연(지터)을 추가하면 요청이 분산됩니다.

function getBackoffWithJitter(attempt, baseMs = 1000) {
  const exponentialWait = Math.pow(2, attempt) * baseMs;
  const jitter = Math.random() * baseMs; // 0~1000ms 랜덤 추가
  return exponentialWait + jitter;
}

// 사용 예
// attempt 0: 1000~2000ms
// attempt 1: 2000~3000ms
// attempt 2: 4000~5000ms
// attempt 3: 8000~9000ms

3. 요청 큐잉 (Request Queue)

요청을 큐에 넣고 Rate Limit에 맞춰 순차적으로 처리합니다.

class RateLimitedQueue {
  constructor(requestsPerMinute) {
    this.queue = [];
    this.processing = false;
    this.intervalMs = (60 * 1000) / requestsPerMinute;
  }

  async add(requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ requestFn, resolve, reject });
      this.process();
    });
  }

  async process() {
    if (this.processing || this.queue.length === 0) return;
    this.processing = true;

    while (this.queue.length > 0) {
      const { requestFn, resolve, reject } = this.queue.shift();

      try {
        const result = await requestFn();
        resolve(result);
      } catch (error) {
        reject(error);
      }

      // 다음 요청까지 대기
      await new Promise(r => setTimeout(r, this.intervalMs));
    }

    this.processing = false;
  }
}

// 사용 예: 분당 60회 제한
const queue = new RateLimitedQueue(60);

// 100개 요청을 큐에 추가 (자동으로 1초 간격으로 처리)
const results = await Promise.all(
  accountIds.map(id =>
    queue.add(() => fetchTransactions(id))
  )
);

4. 사전 Rate Limit 체크

응답 헤더를 확인해서 Rate Limit에 가까워지면 미리 속도를 줄입니다.

class SmartApiClient {
  constructor() {
    this.remaining = Infinity;
    this.resetTime = 0;
  }

  async request(url, options) {
    // 남은 요청이 적으면 대기
    if (this.remaining < 5) {
      const waitTime = this.resetTime - Date.now();
      if (waitTime > 0) {
        console.log(`Rate limit 임박. ${waitTime}ms 대기...`);
        await new Promise(r => setTimeout(r, waitTime));
      }
    }

    const response = await fetch(url, options);

    // 헤더에서 Rate Limit 정보 업데이트
    this.remaining = parseInt(
      response.headers.get('X-RateLimit-Remaining') || '60'
    );
    this.resetTime = parseInt(
      response.headers.get('X-RateLimit-Reset') || '0'
    ) * 1000;

    return response;
  }
}

Rate Limit 최적화 Best Practices

📦

배치 요청 활용

여러 계좌를 조회할 때 개별 요청 대신 배치 API를 사용하면 요청 수를 줄일 수 있습니다.

💾

캐싱 적용

자주 요청하는 데이터는 캐시하세요. 잔액 조회는 1분, 거래내역은 5분 정도 캐싱이 적절합니다.

요청 분산

모든 요청을 한 번에 보내지 말고, 시간대별로 분산하세요. 특히 배치 작업은 새벽 시간에 실행을 권장합니다.

📊

사용량 모니터링

Rate Limit 헤더를 로깅하고 대시보드로 모니터링하면 패턴을 파악하고 최적화할 수 있습니다.

피해야 할 패턴

  • 무한 재시도: 최대 재시도 횟수를 설정하세요
  • 즉시 재시도: 항상 백오프를 적용하세요
  • Rate Limit 무시: 429 응답을 에러로만 처리하지 마세요
  • 헤더 미확인: 응답 헤더에서 Rate Limit 정보를 활용하세요

완성된 Rate Limit 처리 클래스

지금까지 배운 모든 기법을 통합한 완성된 API 클라이언트입니다.

class BankApiClient {
  constructor(apiKey, secretKey, options = {}) {
    this.apiKey = apiKey;
    this.secretKey = secretKey;
    this.baseUrl = options.baseUrl || 'https://api.bankapi.co.kr';
    this.maxRetries = options.maxRetries || 5;

    // Rate Limit 상태
    this.remaining = Infinity;
    this.resetTime = 0;

    // 요청 큐
    this.queue = [];
    this.processing = false;
    this.minInterval = options.minInterval || 100; // 최소 100ms 간격
  }

  async request(endpoint, options = {}) {
    return new Promise((resolve, reject) => {
      this.queue.push({
        endpoint,
        options,
        resolve,
        reject,
        retries: 0
      });
      this.processQueue();
    });
  }

  async processQueue() {
    if (this.processing || this.queue.length === 0) return;
    this.processing = true;

    while (this.queue.length > 0) {
      // Rate Limit 임박 시 대기
      if (this.remaining < 3 && this.resetTime > Date.now()) {
        const waitTime = this.resetTime - Date.now() + 100;
        await this.sleep(waitTime);
      }

      const item = this.queue[0];

      try {
        const result = await this.executeRequest(item);
        this.queue.shift();
        item.resolve(result);
      } catch (error) {
        if (error.status === 429 && item.retries < this.maxRetries) {
          // Rate Limit: 지수 백오프로 재시도
          item.retries++;
          const backoff = this.getBackoffWithJitter(item.retries);
          console.log(`Rate limited. ${backoff}ms 후 재시도 (${item.retries}/${this.maxRetries})`);
          await this.sleep(backoff);
          // 큐의 맨 앞에 유지 (다음 루프에서 재시도)
        } else {
          this.queue.shift();
          item.reject(error);
        }
      }

      // 최소 간격 유지
      await this.sleep(this.minInterval);
    }

    this.processing = false;
  }

  async executeRequest({ endpoint, options }) {
    const url = `${this.baseUrl}${endpoint}`;
    const headers = {
      'Authorization': `Bearer ${this.apiKey}:${this.secretKey}`,
      'Content-Type': 'application/json',
      ...options.headers
    };

    const response = await fetch(url, {
      ...options,
      headers
    });

    // Rate Limit 헤더 업데이트
    this.remaining = parseInt(
      response.headers.get('X-RateLimit-Remaining') || '60'
    );
    const resetHeader = response.headers.get('X-RateLimit-Reset');
    if (resetHeader) {
      this.resetTime = parseInt(resetHeader) * 1000;
    }

    if (!response.ok) {
      const error = new Error(`API Error: ${response.status}`);
      error.status = response.status;
      error.retryAfter = response.headers.get('Retry-After');
      throw error;
    }

    return response.json();
  }

  getBackoffWithJitter(attempt) {
    const base = Math.pow(2, attempt) * 1000;
    const jitter = Math.random() * 1000;
    return base + jitter;
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 편의 메서드
  async getTransactions(bankCode, accountNumber, startDate, endDate) {
    return this.request('/v1/transactions', {
      method: 'POST',
      body: JSON.stringify({
        bankCode,
        accountNumber,
        startDate,
        endDate
      })
    });
  }
}

// 사용 예
const client = new BankApiClient('pk_live_xxx', 'sk_live_xxx');

// 여러 계좌 조회 (자동으로 Rate Limit 처리)
const accounts = [
  { bank: 'NH', account: '123-456-789' },
  { bank: 'NH', account: '987-654-321' },
  // ... 더 많은 계좌
];

const results = await Promise.all(
  accounts.map(acc =>
    client.getTransactions(acc.bank, acc.account, '2026-01-01', '2026-01-08')
  )
);

마무리

Rate Limit은 API를 안정적으로 운영하기 위한 필수 요소입니다. 클라이언트 입장에서는 불편해 보일 수 있지만, 적절한 대응 전략을 구현하면 오히려 더 안정적인 애플리케이션을 만들 수 있습니다.

핵심 포인트 요약

  • 429 응답은 에러가 아닌 신호입니다. 적절히 대응하세요.
  • 지수 백오프 + 지터는 필수입니다.
  • 응답 헤더를 활용해 Rate Limit 상태를 추적하세요.
  • 요청 큐잉으로 자동화된 Rate Limit 관리를 구현하세요.
  • 캐싱으로 불필요한 요청을 줄이세요.

Bank API는 모든 응답에 Rate Limit 관련 헤더를 포함하고 있어, 위 전략들을 바로 적용할 수 있습니다. 더 궁금한 점이 있다면 FAQ를 확인하거나 문의해 주세요.

Bank API로 시작해보세요

무료 플랜으로 시작해서 Rate Limit 처리를 직접 테스트해보세요.

무료로 시작하기 API 문서 보기