Throttling

API Throttling은 API 성능과 가용성을 최적화하기 위한 중요한 트래픽 관리 기법이다. 이는 시스템 자원을 보호하고 서비스의 안정성을 유지하는 데 핵심적인 역할을 한다.

API Throttling의 기본 개념

API Throttling은 시스템이 처리할 수 있는 요청의 양을 제어하는 메커니즘이다. 이는 Rate Limiting과 유사하지만 약간의 차이가 있다. Rate Limiting이 주로 요청을 ‘거부’하는 데 중점을 둔다면, Throttling은 요청의 ‘처리 속도’를 조절하는 데 초점을 맞춘다.

쉽게 말해, Throttling은 트래픽이 과도하게 몰릴 때 시스템이 완전히 중단되거나 요청을 거부하는 대신, 요청 처리 속도를 늦추거나 대기열에 넣어 점진적으로 처리하는 방식이다.

Rate Limiting과 Throttling의 차이점

두 개념이 종종 혼용되지만, 주요 차이점은 다음과 같다:

일반적으로 Rate Limiting이 ‘차단 전략’이라면, Throttling은 ‘지연 전략’이라고 볼 수 있다.

API Throttling이 필요한 이유

  1. 시스템 안정성 보장
    갑작스러운 트래픽 급증은 서버에 과부하를 주어 전체 시스템 다운을 초래할 수 있다. Throttling은 이러한 위험을 줄이고 일정한 처리 속도를 유지한다.

  2. 자원의 공정한 분배
    한 클라이언트가 과도한 요청을 보내면 다른 사용자들이 서비스를 이용하기 어려울 수 있다. Throttling은 모든 사용자가 적절한 서비스 품질을 경험할 수 있도록 한다.

  3. 비용 최적화
    클라우드 환경에서는 API 호출 수에 따라 비용이 발생할 수 있다. Throttling을 통해 불필요한 비용 증가를 방지할 수 있다.

  4. 서비스 품질(QoS) 보장
    Throttling은 중요한 요청이 우선적으로 처리되도록 하여 전반적인 서비스 품질을 유지하는 데 도움이 된다.

API Throttling의 주요 구현 메커니즘

고정 윈도우 카운터(Fixed Window Counter)

가장 단순한 형태의 Throttling으로, 정해진 시간 간격(예: 1분) 동안 허용되는 요청 수를 제한한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Python으로 구현한 고정 윈도우 Throttling
import time
from collections import defaultdict

class FixedWindowThrottler:
    def __init__(self, max_requests, window_size):
        self.max_requests = max_requests  # 윈도우당 최대 요청 수
        self.window_size = window_size    # 윈도우 크기(초)
        self.counters = defaultdict(int)  # 클라이언트별 요청 카운터
        self.window_starts = defaultdict(int)  # 클라이언트별 윈도우 시작 시간
        
    def should_throttle(self, client_id):
        current_time = int(time.time())
        window_start = current_time - (current_time % self.window_size)
        
        # 새 윈도우가 시작되면 카운터 초기화
        if self.window_starts[client_id] != window_start:
            self.counters[client_id] = 0
            self.window_starts[client_id] = window_start
        
        # 요청 카운터 증가
        self.counters[client_id] += 1
        
        # 제한 초과 여부 확인
        return self.counters[client_id] > self.max_requests

슬라이딩 윈도우(Sliding Window)

고정 윈도우의 문제점을 보완하는 방식으로, 현재 시점에서 지정된 시간(예: 지난 1분) 동안의 요청을 기준으로 제한을 적용한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Python으로 구현한 슬라이딩 윈도우 Throttling
import time
from collections import deque

class SlidingWindowThrottler:
    def __init__(self, max_requests, window_size):
        self.max_requests = max_requests  # 윈도우당 최대 요청 수
        self.window_size = window_size    # 윈도우 크기(초)
        self.request_times = defaultdict(lambda: deque())  # 클라이언트별 요청 시간 저장
        
    def should_throttle(self, client_id):
        current_time = time.time()
        
        # 윈도우 밖의 오래된 요청 제거
        while (self.request_times[client_id] and 
               self.request_times[client_id][0] < current_time - self.window_size):
            self.request_times[client_id].popleft()
        
        # 현재 윈도우 내 요청 수 확인
        if len(self.request_times[client_id]) >= self.max_requests:
            return True  # 제한 초과, Throttling 적용
        
        # 현재 요청 시간 추가
        self.request_times[client_id].append(current_time)
        return False  # 요청 허용

토큰 버킷(Token Bucket)

API 요청에 토큰을 사용하는 방식으로, 일정 속도로 버킷에 토큰이 추가되고 각 요청은 토큰을 소비한다. 버킷이 비어있으면 요청이 지연되거나 조절된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Java로 구현한 토큰 버킷 Throttling
public class TokenBucketThrottler {
    private final long capacity;       // 버킷 용량
    private final double refillRate;   // 초당 리필되는 토큰 수
    private double tokens;             // 현재 토큰 수
    private long lastRefillTimestamp;  // 마지막 리필 시간
    
    public TokenBucketThrottler(long capacity, double refillRate) {
        this.capacity = capacity;
        this.refillRate = refillRate;
        this.tokens = capacity;
        this.lastRefillTimestamp = System.currentTimeMillis();
    }
    
    public synchronized long getWaitTime(int tokensRequired) {
        refill();
        
        if (tokens >= tokensRequired) {
            tokens -= tokensRequired;
            return 0;  // 즉시 처리 가능
        }
        
        // 필요한 토큰이 리필될 때까지 대기 시간 계산
        double tokensNeeded = tokensRequired - tokens;
        double waitTime = tokensNeeded / refillRate * 1000;
        
        return (long) Math.ceil(waitTime);
    }
    
    private void refill() {
        long now = System.currentTimeMillis();
        double tokensSinceLastRefill = (now - lastRefillTimestamp) / 1000.0 * refillRate;
        tokens = Math.min(capacity, tokens + tokensSinceLastRefill);
        lastRefillTimestamp = now;
    }
}

누수 버킷(Leaky Bucket)

일정한 속도로 요청을 처리하는 방식으로, 요청이 버킷에 추가되고 일정한 속도로 ‘누수’된다. 버킷이 가득 차면 새 요청이 거부되거나 대기열에 추가된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// JavaScript로 구현한 누수 버킷 Throttling
class LeakyBucketThrottler {
  constructor(capacity, leakRate) {
    this.capacity = capacity;     // 버킷 용량
    this.leakRate = leakRate;     // 초당 처리율
    this.currentLevel = 0;        // 현재 버킷 수위
    this.lastLeakTime = Date.now();
    this.queue = [];              // 대기열
  }
  
  leak() {
    const now = Date.now();
    const elapsedTime = (now - this.lastLeakTime) / 1000; // 초 단위 경과 시간
    const leakedAmount = elapsedTime * this.leakRate;
    
    this.currentLevel = Math.max(0, this.currentLevel - leakedAmount);
    this.lastLeakTime = now;
  }
  
  throttle(request, size = 1) {
    this.leak();
    
    // 버킷에 공간이 있으면 요청 추가
    if (this.currentLevel + size <= this.capacity) {
      this.currentLevel += size;
      return { throttled: false, waitTime: 0 };
    }
    
    // 버킷이 가득 찼지만 대기열에 추가 가능
    if (this.queue.length < this.capacity) {
      const waitTime = (size + this.currentLevel - this.capacity) / this.leakRate * 1000;
      this.queue.push({ request, size, time: Date.now() + waitTime });
      return { throttled: true, waitTime };
    }
    
    // 대기열도 가득 참, 요청 거부
    return { throttled: true, waitTime: -1 };
  }
  
  // 주기적으로 호출하여 처리할 수 있는 요청 확인
  processQueue() {
    this.leak();
    
    while (this.queue.length > 0 && this.currentLevel + this.queue[0].size <= this.capacity) {
      const { request, size } = this.queue.shift();
      this.currentLevel += size;
      // 요청 처리 로직
    }
  }
}

고급 Throttling 전략

적응형 Throttling(Adaptive Throttling)

시스템 부하나 성능 지표에 따라 동적으로 Throttling 매개변수를 조정하는 방식이다. 이는 시스템이 과부하 상태에 가까워지면 더 엄격한 제한을 적용하고, 여유가 있을 때는 제한을 완화하는 방식으로 동작한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Python으로 구현한 적응형 Throttling 예시
import time
import psutil  # 시스템 모니터링용 라이브러리

class AdaptiveThrottler:
    def __init__(self, base_rate, min_rate, max_rate):
        self.base_rate = base_rate  # 기본 처리율
        self.min_rate = min_rate    # 최소 처리율
        self.max_rate = max_rate    # 최대 처리율
        self.current_rate = base_rate
        self.last_adjustment = time.time()
        self.adjustment_interval = 10  # 10초마다 조정
        
    def get_current_rate(self):
        # 시스템 부하에 따라 처리율 조정
        current_time = time.time()
        
        if current_time - self.last_adjustment >= self.adjustment_interval:
            # CPU 사용률 확인
            cpu_usage = psutil.cpu_percent()
            memory_usage = psutil.virtual_memory().percent
            
            # 부하에 따른 처리율 조정
            if cpu_usage > 80 or memory_usage > 80:
                # 높은 부하: 처리율 감소
                self.current_rate = max(self.min_rate, self.current_rate * 0.8)
            elif cpu_usage < 30 and memory_usage < 50:
                # 낮은 부하: 처리율 증가
                self.current_rate = min(self.max_rate, self.current_rate * 1.2)
            
            self.last_adjustment = current_time
            
        return self.current_rate
    
    def should_throttle(self, request_count, time_window):
        current_rate = self.get_current_rate()
        max_requests = current_rate * time_window
        
        return request_count > max_requests

우선순위 기반 Throttling(Priority-based Throttling)

모든 요청을 동일하게 취급하지 않고, 중요도나 우선순위에 따라 차등 적용하는 방식이다. 이는 중요한 작업이 지연되지 않도록 보장한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// JavaScript로 구현한 우선순위 기반 Throttling
class PriorityThrottler {
  constructor() {
    this.queues = {
      high: [],      // 높은 우선순위 대기열
      medium: [],    // 중간 우선순위 대기열
      low: []        // 낮은 우선순위 대기열
    };
    this.processingRates = {
      high: 100,     // 초당 100개 처리
      medium: 50,    // 초당 50개 처리
      low: 10        // 초당 10개 처리
    };
    this.lastProcessTimes = {
      high: Date.now(),
      medium: Date.now(),
      low: Date.now()
    };
  }
  
  throttle(request, priority = 'medium') {
    // 우선순위 검증
    if (!['high', 'medium', 'low'].includes(priority)) {
      priority = 'medium';
    }
    
    // 요청을 해당 우선순위 대기열에 추가
    this.queues[priority].push({
      request,
      timestamp: Date.now()
    });
    
    // 대기열 처리 시도
    this.processQueues();
  }
  
  processQueues() {
    const now = Date.now();
    
    // 각 우선순위 대기열 처리
    ['high', 'medium', 'low'].forEach(priority => {
      const elapsed = (now - this.lastProcessTimes[priority]) / 1000;
      const capacity = Math.floor(elapsed * this.processingRates[priority]);
      
      if (capacity > 0 && this.queues[priority].length > 0) {
        // 처리 가능한 요청 수 계산
        const toProcess = Math.min(capacity, this.queues[priority].length);
        
        // 요청 처리
        const processed = this.queues[priority].splice(0, toProcess);
        processed.forEach(item => {
          // 요청 처리 로직
          console.log(`처리: ${priority} 우선순위 요청, 대기 시간: ${now - item.timestamp}ms`);
        });
        
        this.lastProcessTimes[priority] = now;
      }
    });
  }
}

분산 Throttling(Distributed Throttling)

마이크로서비스 아키텍처나 여러 서버가 있는 환경에서 중앙 집중식으로 Throttling을 관리하는 방식이다. Redis와 같은 인메모리 데이터 스토어를 활용하여 구현할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// C#과 Redis를 사용한 분산 Throttling 예시
using StackExchange.Redis;
using System;
using System.Threading.Tasks;

public class DistributedThrottler
{
    private readonly ConnectionMultiplexer _redis;
    private readonly double _tokensPerSecond;
    private readonly int _bucketCapacity;
    
    public DistributedThrottler(string redisConnectionString, double tokensPerSecond, int bucketCapacity)
    {
        _redis = ConnectionMultiplexer.Connect(redisConnectionString);
        _tokensPerSecond = tokensPerSecond;
        _bucketCapacity = bucketCapacity;
    }
    
    public async Task<ThrottleResult> ThrottleAsync(string clientId, int tokens = 1)
    {
        var db = _redis.GetDatabase();
        var key = $"throttle:{clientId}";
        
        // Redis에서 현재 버킷 상태 가져오기
        var bucketScript = @"
            local bucket_key = KEYS[1]
            local tokens = tonumber(ARGV[1])
            local capacity = tonumber(ARGV[2])
            local rate = tonumber(ARGV[3])
            local now = tonumber(ARGV[4])
            
            -- 현재 버킷 상태 가져오기 또는 초기화
            local bucket = redis.call('hmget', bucket_key, 'tokens', 'last_refill')
            local current_tokens = tonumber(bucket[1]) or capacity
            local last_refill = tonumber(bucket[2]) or now
            
            -- 토큰 리필 계산
            local delta = math.max(0, now - last_refill)
            local refill = delta * rate
            current_tokens = math.min(capacity, current_tokens + refill)
            
            -- 토큰이 충분한지 확인
            local allowed = current_tokens >= tokens
            local wait_time = 0
            
            if allowed then
                current_tokens = current_tokens - tokens
            else
                wait_time = (tokens - current_tokens) / rate
            end
            
            -- 버킷 상태 업데이트
            redis.call('hmset', bucket_key, 'tokens', current_tokens, 'last_refill', now)
            redis.call('expire', bucket_key, 300)  -- 5분 후 만료
            
            return {allowed, wait_time}
        ";
        
        var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0;
        var result = await db.ScriptEvaluateAsync(
            bucketScript,
            new RedisKey[] { key },
            new RedisValue[] { tokens, _bucketCapacity, _tokensPerSecond, now }
        );
        
        var resultArray = (RedisValue[])result;
        bool allowed = (bool)resultArray[0];
        double waitTime = (double)resultArray[1];
        
        return new ThrottleResult
        {
            Allowed = allowed,
            WaitTimeSeconds = waitTime
        };
    }
    
    public class ThrottleResult
    {
        public bool Allowed { get; set; }
        public double WaitTimeSeconds { get; set; }
    }
}

API Throttling 구현 시 고려사항

클라이언트 식별

Throttling을 적용할 때 어떤 기준으로 클라이언트를 구분할지 결정해야 한다:

  1. IP 주소: 가장 기본적인 방법이지만, NAT나 프록시 환경에서는 정확하지 않을 수 있다.
  2. API 키: 더 정확한 식별이 가능하지만 키 관리와 발급이 필요하다.
  3. 사용자 ID: 로그인한 사용자를 기준으로 제한을 적용한다.
  4. 복합 식별자: 여러 속성(IP + API 키 + 엔드포인트 등)을 조합하여 정교한 식별을 구현한다.

명확한 피드백 제공

Throttling이 적용될 때 클라이언트에게 유용한 정보를 제공해야 한다:

  1. HTTP 상태 코드: 일반적으로 429 (Too Many Requests)를 사용한다.
  2. 헤더 정보: 현재 제한, 남은 요청 수, 재시도 시간 등의 정보를 헤더에 포함한다.
  3. 응답 본문: 자세한 오류 메시지와 대응 방안을 제공한다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1605086234
Retry-After: 60

{
  "status": "error",
  "code": "rate_limit_exceeded",
  "message": "요청 빈도가 너무 높습니다. 60초 후에 다시 시도해 주세요.",
  "details": {
    "limit": 100,
    "period": "1h",
    "retry_after": 60
  }
}

모니터링 및 분석

Throttling 정책의 효과를 지속적으로 평가하고 최적화해야 한다:

  1. 요청 패턴 분석: 시간대별, 사용자별, 엔드포인트별 요청 패턴을 분석한다.
  2. Throttling 이벤트 로깅: 제한이 적용된 상황에 대한 상세 정보를 기록한다.
  3. 알림 설정: 비정상적인 패턴이나 과도한 Throttling이 발생할 경우 알림을 받는다.
  4. 대시보드 구축: Throttling 상태와 효과를 시각화하는 대시보드를 구축한다.

다양한 환경에서의 API Throttling 구현

API 게이트웨이 활용

대부분의 API 게이트웨이 제품은 Throttling 기능을 기본적으로 제공한다:

  1. AWS API Gateway: 사용량 계획(Usage Plans)을 통한 Throttling 설정
  2. Kong: 여러 플러그인을 통한 다양한 Throttling 전략 지원
  3. NGINX: limit_reqlimit_conn 모듈을 통한 Throttling
  4. Azure API Management: 정책을 통한 세분화된 Throttling 설정

클라우드 서비스에서의 Throttling

클라우드 환경에서는 서비스별로 다양한 Throttling 메커니즘을 제공한다:

  1. AWS Lambda: 동시 실행 제한 및 조절(Concurrency Limits)
  2. Google Cloud Functions: 초당 요청 수 제한
  3. Azure Functions: 소비 계획에 따른 확장 제한

컨테이너 환경에서의 Throttling

Kubernetes와 같은 컨테이너 오케스트레이션 환경에서도 Throttling을 구현할 수 있다:

  1. 리소스 할당: CPU/메모리 제한을 통한 간접적 Throttling
  2. 서비스 메시: Istio나 Linkerd와 같은 서비스 메시를 통한 트래픽 제어
  3. 커스텀 메트릭: HPA(Horizontal Pod Autoscaler)와 커스텀 메트릭을 활용한 동적 Throttling

실제 사례 연구: 주요 서비스의 Throttling 전략

Stripe API의 Throttling

Stripe는 결제 처리의 안정성을 보장하기 위해 세심한 Throttling 전략을 사용한다:

  1. 단계적 제한: 계정당 초당 25-100개의 요청 제한
  2. 리소스별 차등 제한: 민감한 작업(결제 처리)에 더 엄격한 제한 적용
  3. Webhook 전송 제한: 초당 최대 100개로 Webhook 이벤트 전송 제한
  4. 버스트 허용: 단기간의 트래픽 급증을 허용하는 유연한 정책

Twitter API의 계층화된 접근

Twitter API는 다양한 사용 시나리오에 맞게 계층화된 Throttling을 적용한다:

  1. 엔드포인트별 차등: 다양한 엔드포인트에 따라 15분당 15-450회 제한
  2. 스트리밍 API: 스트리밍 연결에 대한 별도 제한
  3. 애플리케이션별/사용자별 구분: 앱과 사용자 수준에서 모두 제한 적용
  4. 계층별 서비스: 무료/유료 계정에 따른 차등 제한

Netflix API의 적응형 부하 감소

Netflix는 서비스 안정성을 유지하기 위해 적응형 Throttling 전략을 사용한다:

  1. 차등 서비스 감소: 시스템 부하에 따라 점진적으로 서비스 기능 축소
  2. 우선순위 기반 처리: 핵심 기능(비디오 스트리밍)을 우선적으로 처리
  3. 클라이언트 유형별 차등: 디바이스 유형에 따른 차등 제한
  4. 지역별 조정: 지역별 서버 부하에 따른 동적 조정

최적화된 Throttling 전략 설계

  1. 맞춤형 전략 개발
    서비스의 특성과 요구사항에 맞는 Throttling 전략을 설계해야 한다:
    1. 트래픽 패턴 분석: 과거 트래픽 데이터를 분석하여 패턴 파악
    2. 사용자 행동 이해: 사용자의 API 사용 패턴과 요구사항 분석
    3. 중요 작업 식별: 시스템에서 가장 중요하고 리소스를 많이 소비하는 작업 식별
    4. 단계적 접근: 모든 요청을 동일하게 취급하지 않고 중요도에 따라 차등 적용
  2. 다층 방어 전략
    여러 수준의 Throttling을 조합하여 강력한 보호 체계를 구축한다:
    1. 네트워크 수준: 인프라 차원의 기본적인 DDoS 방어
    2. 게이트웨이 수준: API 게이트웨이에서의 기본 Throttling
    3. 서비스 수준: 개별 서비스나 마이크로서비스별 Throttling
    4. 엔드포인트 수준: 특정 API 엔드포인트에 대한 세부 Throttling
    5. 사용자 수준: 사용자 계층이나 구독 수준에 따른 차등 제한
  3. 비즈니스 모델과의 통합
    Throttling은 기술적 제한을 넘어 비즈니스 모델의 일부로 설계될 수 있다:
    1. 등급별 서비스: 다양한 구독 등급에 따른 차등화된 API 접근
    2. 사용량 기반 과금: 실제 API 사용량에 따른 과금 모델
    3. 추가 용량 구매: 기본 제한을 초과하는 추가 요청에 대한 별도 구매 옵션
    4. 예측 가능한 비용: 사용자에게 예측 가능한 비용 구조 제공

클라이언트 측면의 대응 전략

API를 소비하는 클라이언트 입장에서도 Throttling에 효과적으로 대응하기 위한 전략이 필요하다:

백오프 및 재시도 메커니즘

Throttling으로 인해 요청이 거부되었을 때 효율적으로 대응하는 방법이다:

  1. 지수 백오프(Exponential Backoff): 재시도 간격을 점진적으로 증가시켜 서버 부하를 분산시킨다.
  2. 지터(Jitter) 추가: 재시도 시간에 무작위성을 추가하여 여러 클라이언트의 동시 재시도를 방지한다.
  3. 최대 재시도 횟수 설정: 무한 재시도를 방지하기 위한 상한선을 설정한다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// JavaScript에서의 지수 백오프 + 지터 구현
async function fetchWithBackoff(url, options, maxRetries = 5) {
  let retries = 0;
  
  while (retries < maxRetries) {
    try {
      const response = await fetch(url, options);
      
      if (response.status !== 429) {
        return response;
      }
      
      // 429 상태인 경우 Retry-After 헤더 확인
      const retryAfter = response.headers.get('Retry-After');
      let waitTime;
      
      if (retryAfter) {
        // 서버가 제안한 대기 시간 사용
        waitTime = parseInt(retryAfter, 10) * 1000;
      } else {
        // 지수 백오프 + 지터 적용
        const baseWaitTime = Math.pow(2, retries) * 1000;
        const jitter = Math.random() * 1000;
        waitTime = baseWaitTime + jitter;
      }
      
      console.log(`요청이 Throttling되었습니다. ${waitTime}ms 후 재시도…`);
      await new Promise(resolve => setTimeout(resolve, waitTime));
      
      retries++;
    } catch (error) {
      console.error('요청 중 오류 발생:', error);
      retries++;
      
      // 네트워크 오류의 경우 지수 백오프 적용
      const waitTime = Math.pow(2, retries) * 1000;
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }
  }
  
  throw new Error('최대 재시도 횟수 초과');
}

요청 최적화

API 요청을 최적화하여 Throttling 가능성을 줄이는 방법이다:

  1. 배치 처리: 여러 작은 요청을 하나의 큰 요청으로 통합한다.
  2. 불필요한 요청 제거: 중복되거나 필요 없는 API 호출을 제거한다.
  3. 조건부 요청: 변경된 경우에만 데이터를 가져오는 조건부 요청(If-Modified-Since 등)을 활용한다.
  4. 필요한 데이터만 요청: 필드 필터링을 통해 필요한 데이터만 요청한다.

클라이언트 측 캐싱

서버 요청 횟수를 줄이기 위한 효과적인 캐싱 전략:

  1. 메모리 캐시: 자주 사용되는 데이터를 메모리에 캐싱한다.
  2. 영구 캐시: 로컬 스토리지나 인덱스드DB에 데이터를 저장한다.
  3. 캐시 무효화 정책: 데이터 신선도를 유지하기 위한 적절한 무효화 정책을 설정한다.
  4. 스탠딩 쿼리: 백그라운드에서 주기적으로 데이터를 가져와 캐시를 갱신한다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# Python으로 구현한 클라이언트 측 캐싱 예시
import time
import requests
from functools import lru_cache

class CachingApiClient:
    def __init__(self, base_url, cache_ttl=300):
        self.base_url = base_url
        self.cache_ttl = cache_ttl
        self.cache = {}
        
    def get(self, endpoint, params=None):
        # 캐시 키 생성
        cache_key = self._make_cache_key(endpoint, params)
        
        # 캐시에서 데이터 확인
        cached_data = self.cache.get(cache_key)
        if cached_data:
            timestamp, data = cached_data
            # 캐시가 유효한지 확인
            if time.time() - timestamp < self.cache_ttl:
                print(f"캐시에서 데이터 반환: {endpoint}")
                return data
        
        # 캐시에 없거나 만료된 경우 API 호출
        try:
            print(f"API 호출: {endpoint}")
            response = requests.get(f"{self.base_url}/{endpoint}", params=params)
            response.raise_for_status()
            data = response.json()
            
            # 결과 캐싱
            self.cache[cache_key] = (time.time(), data)
            return data
        except requests.exceptions.RequestException as e:
            # 429 에러인 경우 백오프 로직 적용
            if hasattr(e.response, 'status_code') and e.response.status_code == 429:
                retry_after = int(e.response.headers.get('Retry-After', 5))
                print(f"Throttling 감지: {retry_after}초 후 재시도")
                time.sleep(retry_after)
                return self.get(endpoint, params)  # 재귀적 재시도
            raise
    
    def _make_cache_key(self, endpoint, params):
        # 딕셔너리를 정렬하여 일관된 캐시 키 생성
        if params:
            param_str = '&'.join(f"{k}={v}" for k, v in sorted(params.items()))
            return f"{endpoint}?{param_str}"
        return endpoint
    
    def clear_cache(self):
        self.cache.clear()
        
    def invalidate(self, endpoint, params=None):
        cache_key = self._make_cache_key(endpoint, params)
        if cache_key in self.cache:
            del self.cache[cache_key]

분산 요청

여러 인스턴스나 리전에 요청을 분산하여 Throttling 위험을 줄이는 방법:

  1. 리전별 엔드포인트 활용: 글로벌 서비스의 경우 여러 리전의 엔드포인트를 활용한다.
  2. API 키 순환: 여러 API 키를 번갈아 사용하여 제한을 분산시킨다.
  3. 부하 분산: 클라이언트 요청을 여러 서버에 분산시킨다.

사용자 경험과 Throttling

Throttling은 기술적 제약이지만, 사용자 경험을 해치지 않도록 설계해야 한다:

명확한 커뮤니케이션

사용자에게 제한 사항과 대응 방안을 명확히 안내한다:

  1. 사전 알림: 제한에 가까워지면 사용자에게 미리 경고한다.
  2. 친절한 오류 메시지: 제한이 적용되었을 때 이해하기 쉬운 메시지를 제공한다.
  3. 대안 제시: 배치 처리, 오프라인 처리 등의 대안을 제안한다.
  4. 문서화: API 문서에 제한 사항과 모범 사례를 명확히 기술한다.

점진적 성능 저하

시스템이 과부하 상태일 때 전체 서비스를 중단하는 대신 점진적으로 기능을 축소한다:

  1. 필수 기능 우선: 핵심 기능을 우선적으로 유지한다.
  2. 비필수 기능 비활성화: 부하가 높을 때 부가 기능을 일시적으로 비활성화한다.
  3. 데이터 간소화: 응답 데이터의 크기나 복잡성을 줄인다.
  4. 캐시 수명 연장: 캐시 TTL(Time-To-Live)을 일시적으로 연장한다.

사용자 피드백 반영

Throttling 정책을 지속적으로 개선하기 위해 사용자 피드백을 수집하고 반영한다:

  1. 사용 패턴 모니터링: 사용자의 실제 API 사용 패턴을 분석한다.
  2. 제한 도달 빈도 분석: 어떤 사용자나 엔드포인트가 자주 제한에 도달하는지 파악한다.
  3. 사용자 설문조사: API 소비자로부터 직접 피드백을 수집한다.
  4. 정책 조정: 피드백과 데이터를 기반으로 Throttling 정책을 조정한다.

Throttling과 비즈니스 전략

Throttling은 단순한 기술적 제약이 아니라 전체 비즈니스 전략의 일부로 접근해야 한다:

서비스 수준 계약(SLA) 관리

Throttling은 SLA를 유지하고 관리하는 데 중요한 역할을 한다:

  1. 약속된 성능 보장: 과도한 사용으로 인한 성능 저하를 방지한다.
  2. 용량 계획: 실제 사용량에 기반한 인프라 확장 계획을 수립한다.
  3. 공정한 자원 분배: 소수의 사용자가 자원을 독점하는 것을 방지한다.

수익화 전략

API Throttling은 수익화 전략과 밀접한 관련이 있다:

  1. 등급별 제한: 다양한 가격 등급에 따른 차등화된 API 접근을 제공한다.
  2. 사용량 기반 과금: API 호출 수에 따른 과금 모델을 구현한다.
  3. 프리미엄 기능: 높은 등급의 사용자에게 더 높은 속도나 우선 처리를 제공한다.
  4. 버스트 크레딧: 일시적인 사용량 증가를 위한 추가 크레딧 판매를 고려한다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 등급별 Throttling 정책 예시 (JSON 구성)
{
  "free_tier": {
    "requests_per_day": 1000,
    "requests_per_minute": 20,
    "burst_capacity": 100,
    "concurrent_requests": 5,
    "retry_after": 60
  },
  "basic_tier": {
    "requests_per_day": 10000,
    "requests_per_minute": 100,
    "burst_capacity": 500,
    "concurrent_requests": 20,
    "retry_after": 30
  },
  "premium_tier": {
    "requests_per_day": 100000,
    "requests_per_minute": 1000,
    "burst_capacity": 5000,
    "concurrent_requests": 100,
    "retry_after": 10
  },
  "enterprise_tier": {
    "requests_per_day": "unlimited",
    "requests_per_minute": 10000,
    "burst_capacity": 50000,
    "concurrent_requests": 500,
    "retry_after": 5,
    "dedicated_resources": true
  }
}

파트너십 관리

외부 파트너와의 협력에서 Throttling은 중요한 역할을 한다:

  1. 맞춤형 제한: 파트너별로 맞춤화된 Throttling 정책을 제공한다.
  2. 사용량 계약: 예상 사용량에 따른 명확한 계약을 체결한다.
  3. 확장 계획: 파트너의 성장에 따른 API 용량 확장 계획을 수립한다.
  4. 투명한 모니터링: 파트너에게 사용량과 제한에 대한 투명한 모니터링 도구를 제공한다.

보안 관점에서의 Throttling

Throttling은 API 보안 전략의 중요한 부분이다:

  1. 공격 방어
    다양한 형태의 공격을 방어하는 데 도움이 된다:

    1. DDoS 방어: 분산 서비스 거부 공격으로부터 시스템 보호
    2. 무차별 대입 공격 방지: 인증 엔드포인트에 대한 특별 제한으로 무차별 대입 공격 방지
    3. 스크래핑 제한: 자동화된 데이터 수집을 제한하여 콘텐츠 보호
    4. 계정 탈취 방지: 의심스러운 활동 패턴 감지 시 추가 제한 적용
  2. 이상 탐지와 결합
    Throttling을 이상 탐지 시스템과 결합하여 더 효과적인 보호를 제공한다:

    1. 행동 기반 Throttling: 일반적인 사용 패턴에서 벗어난 행동 감지 시 제한 강화
    2. 위험 기반 조정: 요청의 위험도에 따라 동적으로 제한 조정
    3. 점진적 응답: 의심스러운 활동 감지 시 점진적으로 제한 강화
    4. 자동 차단: 명백한 악의적 행동 감지 시 일시적 차단 적용
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    
    # Python으로 구현한 이상 탐지 기반 Throttling 예시
    class AnomalyBasedThrottler:
        def __init__(self, base_limit, strict_limit):
            self.base_limit = base_limit      # 기본 요청 제한
            self.strict_limit = strict_limit  # 의심 행동 시 제한
            self.user_patterns = {}           # 사용자별 행동 패턴
            self.suspicious_users = set()     # 의심스러운 사용자 목록
    
        def should_throttle(self, user_id, endpoint, request_count, time_window):
            # 사용자별 제한 결정
            limit = self.strict_limit if user_id in self.suspicious_users else self.base_limit
    
            # 요청 패턴 업데이트
            if user_id not in self.user_patterns:
                self.user_patterns[user_id] = {}
    
            if endpoint not in self.user_patterns[user_id]:
                self.user_patterns[user_id][endpoint] = []
    
            # 최근 요청 패턴 기록
            self.user_patterns[user_id][endpoint].append(time.time())
    
            # 오래된 기록 제거
            current_time = time.time()
            self.user_patterns[user_id][endpoint] = [
                t for t in self.user_patterns[user_id][endpoint] 
                if current_time - t < time_window
            ]
    
            # 이상 패턴 감지
            pattern_length = len(self.user_patterns[user_id][endpoint])
            if pattern_length > 0:
                # 초당 요청 속도 계산
                rate = pattern_length / time_window
    
                # 의심스러운 속도 감지
                if rate > self.base_limit * 0.8 and user_id not in self.suspicious_users:
                    print(f"의심스러운 행동 감지: {user_id}, 요청 속도: {rate}/초")
                    self.suspicious_users.add(user_id)
    
                # 정상 패턴으로 복귀 감지
                elif rate < self.base_limit * 0.5 and user_id in self.suspicious_users:
                    print(f"정상 행동 복귀: {user_id}, 요청 속도: {rate}/초")
                    self.suspicious_users.remove(user_id)
    
            # 제한 초과 여부 확인
            return request_count > limit
    

용어 정리

용어설명

참고 및 출처