Throttling#
API Throttling은 API 성능과 가용성을 최적화하기 위한 중요한 트래픽 관리 기법이다. 이는 시스템 자원을 보호하고 서비스의 안정성을 유지하는 데 핵심적인 역할을 한다.
API Throttling의 기본 개념#
API Throttling은 시스템이 처리할 수 있는 요청의 양을 제어하는 메커니즘이다. 이는 Rate Limiting과 유사하지만 약간의 차이가 있다. Rate Limiting이 주로 요청을 ‘거부’하는 데 중점을 둔다면, Throttling은 요청의 ‘처리 속도’를 조절하는 데 초점을 맞춘다.
쉽게 말해, Throttling은 트래픽이 과도하게 몰릴 때 시스템이 완전히 중단되거나 요청을 거부하는 대신, 요청 처리 속도를 늦추거나 대기열에 넣어 점진적으로 처리하는 방식이다.
Rate Limiting과 Throttling의 차이점#
두 개념이 종종 혼용되지만, 주요 차이점은 다음과 같다:
- Rate Limiting: 지정된 시간 내에 허용되는 요청 수를 제한한다. 제한을 초과하는 요청은 일반적으로 거부된다.
- Throttling: 요청 처리 속도를 조절한다. 시스템 부하에 따라 요청을 지연시키거나 대기열에 넣어 점진적으로 처리한다.
일반적으로 Rate Limiting이 ‘차단 전략’이라면, Throttling은 ‘지연 전략’이라고 볼 수 있다.
API Throttling이 필요한 이유#
시스템 안정성 보장
갑작스러운 트래픽 급증은 서버에 과부하를 주어 전체 시스템 다운을 초래할 수 있다. Throttling은 이러한 위험을 줄이고 일정한 처리 속도를 유지한다.
자원의 공정한 분배
한 클라이언트가 과도한 요청을 보내면 다른 사용자들이 서비스를 이용하기 어려울 수 있다. Throttling은 모든 사용자가 적절한 서비스 품질을 경험할 수 있도록 한다.
비용 최적화
클라우드 환경에서는 API 호출 수에 따라 비용이 발생할 수 있다. Throttling을 통해 불필요한 비용 증가를 방지할 수 있다.
서비스 품질(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을 적용할 때 어떤 기준으로 클라이언트를 구분할지 결정해야 한다:
- IP 주소: 가장 기본적인 방법이지만, NAT나 프록시 환경에서는 정확하지 않을 수 있다.
- API 키: 더 정확한 식별이 가능하지만 키 관리와 발급이 필요하다.
- 사용자 ID: 로그인한 사용자를 기준으로 제한을 적용한다.
- 복합 식별자: 여러 속성(IP + API 키 + 엔드포인트 등)을 조합하여 정교한 식별을 구현한다.
명확한 피드백 제공#
Throttling이 적용될 때 클라이언트에게 유용한 정보를 제공해야 한다:
- HTTP 상태 코드: 일반적으로 429 (Too Many Requests)를 사용한다.
- 헤더 정보: 현재 제한, 남은 요청 수, 재시도 시간 등의 정보를 헤더에 포함한다.
- 응답 본문: 자세한 오류 메시지와 대응 방안을 제공한다.
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 정책의 효과를 지속적으로 평가하고 최적화해야 한다:
- 요청 패턴 분석: 시간대별, 사용자별, 엔드포인트별 요청 패턴을 분석한다.
- Throttling 이벤트 로깅: 제한이 적용된 상황에 대한 상세 정보를 기록한다.
- 알림 설정: 비정상적인 패턴이나 과도한 Throttling이 발생할 경우 알림을 받는다.
- 대시보드 구축: Throttling 상태와 효과를 시각화하는 대시보드를 구축한다.
다양한 환경에서의 API Throttling 구현#
API 게이트웨이 활용#
대부분의 API 게이트웨이 제품은 Throttling 기능을 기본적으로 제공한다:
- AWS API Gateway: 사용량 계획(Usage Plans)을 통한 Throttling 설정
- Kong: 여러 플러그인을 통한 다양한 Throttling 전략 지원
- NGINX:
limit_req
와 limit_conn
모듈을 통한 Throttling - Azure API Management: 정책을 통한 세분화된 Throttling 설정
클라우드 서비스에서의 Throttling#
클라우드 환경에서는 서비스별로 다양한 Throttling 메커니즘을 제공한다:
- AWS Lambda: 동시 실행 제한 및 조절(Concurrency Limits)
- Google Cloud Functions: 초당 요청 수 제한
- Azure Functions: 소비 계획에 따른 확장 제한
컨테이너 환경에서의 Throttling#
Kubernetes와 같은 컨테이너 오케스트레이션 환경에서도 Throttling을 구현할 수 있다:
- 리소스 할당: CPU/메모리 제한을 통한 간접적 Throttling
- 서비스 메시: Istio나 Linkerd와 같은 서비스 메시를 통한 트래픽 제어
- 커스텀 메트릭: HPA(Horizontal Pod Autoscaler)와 커스텀 메트릭을 활용한 동적 Throttling
실제 사례 연구: 주요 서비스의 Throttling 전략#
Stripe API의 Throttling#
Stripe는 결제 처리의 안정성을 보장하기 위해 세심한 Throttling 전략을 사용한다:
- 단계적 제한: 계정당 초당 25-100개의 요청 제한
- 리소스별 차등 제한: 민감한 작업(결제 처리)에 더 엄격한 제한 적용
- Webhook 전송 제한: 초당 최대 100개로 Webhook 이벤트 전송 제한
- 버스트 허용: 단기간의 트래픽 급증을 허용하는 유연한 정책
Twitter API는 다양한 사용 시나리오에 맞게 계층화된 Throttling을 적용한다:
- 엔드포인트별 차등: 다양한 엔드포인트에 따라 15분당 15-450회 제한
- 스트리밍 API: 스트리밍 연결에 대한 별도 제한
- 애플리케이션별/사용자별 구분: 앱과 사용자 수준에서 모두 제한 적용
- 계층별 서비스: 무료/유료 계정에 따른 차등 제한
Netflix API의 적응형 부하 감소#
Netflix는 서비스 안정성을 유지하기 위해 적응형 Throttling 전략을 사용한다:
- 차등 서비스 감소: 시스템 부하에 따라 점진적으로 서비스 기능 축소
- 우선순위 기반 처리: 핵심 기능(비디오 스트리밍)을 우선적으로 처리
- 클라이언트 유형별 차등: 디바이스 유형에 따른 차등 제한
- 지역별 조정: 지역별 서버 부하에 따른 동적 조정
최적화된 Throttling 전략 설계#
- 맞춤형 전략 개발
서비스의 특성과 요구사항에 맞는 Throttling 전략을 설계해야 한다:- 트래픽 패턴 분석: 과거 트래픽 데이터를 분석하여 패턴 파악
- 사용자 행동 이해: 사용자의 API 사용 패턴과 요구사항 분석
- 중요 작업 식별: 시스템에서 가장 중요하고 리소스를 많이 소비하는 작업 식별
- 단계적 접근: 모든 요청을 동일하게 취급하지 않고 중요도에 따라 차등 적용
- 다층 방어 전략
여러 수준의 Throttling을 조합하여 강력한 보호 체계를 구축한다:- 네트워크 수준: 인프라 차원의 기본적인 DDoS 방어
- 게이트웨이 수준: API 게이트웨이에서의 기본 Throttling
- 서비스 수준: 개별 서비스나 마이크로서비스별 Throttling
- 엔드포인트 수준: 특정 API 엔드포인트에 대한 세부 Throttling
- 사용자 수준: 사용자 계층이나 구독 수준에 따른 차등 제한
- 비즈니스 모델과의 통합
Throttling은 기술적 제한을 넘어 비즈니스 모델의 일부로 설계될 수 있다:- 등급별 서비스: 다양한 구독 등급에 따른 차등화된 API 접근
- 사용량 기반 과금: 실제 API 사용량에 따른 과금 모델
- 추가 용량 구매: 기본 제한을 초과하는 추가 요청에 대한 별도 구매 옵션
- 예측 가능한 비용: 사용자에게 예측 가능한 비용 구조 제공
클라이언트 측면의 대응 전략#
API를 소비하는 클라이언트 입장에서도 Throttling에 효과적으로 대응하기 위한 전략이 필요하다:
백오프 및 재시도 메커니즘#
Throttling으로 인해 요청이 거부되었을 때 효율적으로 대응하는 방법이다:
- 지수 백오프(Exponential Backoff): 재시도 간격을 점진적으로 증가시켜 서버 부하를 분산시킨다.
- 지터(Jitter) 추가: 재시도 시간에 무작위성을 추가하여 여러 클라이언트의 동시 재시도를 방지한다.
- 최대 재시도 횟수 설정: 무한 재시도를 방지하기 위한 상한선을 설정한다.
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 가능성을 줄이는 방법이다:
- 배치 처리: 여러 작은 요청을 하나의 큰 요청으로 통합한다.
- 불필요한 요청 제거: 중복되거나 필요 없는 API 호출을 제거한다.
- 조건부 요청: 변경된 경우에만 데이터를 가져오는 조건부 요청(If-Modified-Since 등)을 활용한다.
- 필요한 데이터만 요청: 필드 필터링을 통해 필요한 데이터만 요청한다.
클라이언트 측 캐싱#
서버 요청 횟수를 줄이기 위한 효과적인 캐싱 전략:
- 메모리 캐시: 자주 사용되는 데이터를 메모리에 캐싱한다.
- 영구 캐시: 로컬 스토리지나 인덱스드DB에 데이터를 저장한다.
- 캐시 무효화 정책: 데이터 신선도를 유지하기 위한 적절한 무효화 정책을 설정한다.
- 스탠딩 쿼리: 백그라운드에서 주기적으로 데이터를 가져와 캐시를 갱신한다.
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 위험을 줄이는 방법:
- 리전별 엔드포인트 활용: 글로벌 서비스의 경우 여러 리전의 엔드포인트를 활용한다.
- API 키 순환: 여러 API 키를 번갈아 사용하여 제한을 분산시킨다.
- 부하 분산: 클라이언트 요청을 여러 서버에 분산시킨다.
사용자 경험과 Throttling#
Throttling은 기술적 제약이지만, 사용자 경험을 해치지 않도록 설계해야 한다:
명확한 커뮤니케이션#
사용자에게 제한 사항과 대응 방안을 명확히 안내한다:
- 사전 알림: 제한에 가까워지면 사용자에게 미리 경고한다.
- 친절한 오류 메시지: 제한이 적용되었을 때 이해하기 쉬운 메시지를 제공한다.
- 대안 제시: 배치 처리, 오프라인 처리 등의 대안을 제안한다.
- 문서화: API 문서에 제한 사항과 모범 사례를 명확히 기술한다.
점진적 성능 저하#
시스템이 과부하 상태일 때 전체 서비스를 중단하는 대신 점진적으로 기능을 축소한다:
- 필수 기능 우선: 핵심 기능을 우선적으로 유지한다.
- 비필수 기능 비활성화: 부하가 높을 때 부가 기능을 일시적으로 비활성화한다.
- 데이터 간소화: 응답 데이터의 크기나 복잡성을 줄인다.
- 캐시 수명 연장: 캐시 TTL(Time-To-Live)을 일시적으로 연장한다.
사용자 피드백 반영#
Throttling 정책을 지속적으로 개선하기 위해 사용자 피드백을 수집하고 반영한다:
- 사용 패턴 모니터링: 사용자의 실제 API 사용 패턴을 분석한다.
- 제한 도달 빈도 분석: 어떤 사용자나 엔드포인트가 자주 제한에 도달하는지 파악한다.
- 사용자 설문조사: API 소비자로부터 직접 피드백을 수집한다.
- 정책 조정: 피드백과 데이터를 기반으로 Throttling 정책을 조정한다.
Throttling과 비즈니스 전략#
Throttling은 단순한 기술적 제약이 아니라 전체 비즈니스 전략의 일부로 접근해야 한다:
서비스 수준 계약(SLA) 관리#
Throttling은 SLA를 유지하고 관리하는 데 중요한 역할을 한다:
- 약속된 성능 보장: 과도한 사용으로 인한 성능 저하를 방지한다.
- 용량 계획: 실제 사용량에 기반한 인프라 확장 계획을 수립한다.
- 공정한 자원 분배: 소수의 사용자가 자원을 독점하는 것을 방지한다.
수익화 전략#
API Throttling은 수익화 전략과 밀접한 관련이 있다:
- 등급별 제한: 다양한 가격 등급에 따른 차등화된 API 접근을 제공한다.
- 사용량 기반 과금: 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
| // 등급별 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은 중요한 역할을 한다:
- 맞춤형 제한: 파트너별로 맞춤화된 Throttling 정책을 제공한다.
- 사용량 계약: 예상 사용량에 따른 명확한 계약을 체결한다.
- 확장 계획: 파트너의 성장에 따른 API 용량 확장 계획을 수립한다.
- 투명한 모니터링: 파트너에게 사용량과 제한에 대한 투명한 모니터링 도구를 제공한다.
보안 관점에서의 Throttling#
Throttling은 API 보안 전략의 중요한 부분이다:
공격 방어
다양한 형태의 공격을 방어하는 데 도움이 된다:
- DDoS 방어: 분산 서비스 거부 공격으로부터 시스템 보호
- 무차별 대입 공격 방지: 인증 엔드포인트에 대한 특별 제한으로 무차별 대입 공격 방지
- 스크래핑 제한: 자동화된 데이터 수집을 제한하여 콘텐츠 보호
- 계정 탈취 방지: 의심스러운 활동 패턴 감지 시 추가 제한 적용
이상 탐지와 결합
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
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
|
용어 정리#
참고 및 출처#