[Daily morning study] API Rate Limiting ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

#daily morning study

Image


API Rate Limiting ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

Rate Limiting์ด๋ž€?

Rate Limiting์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ผ์ • ์‹œ๊ฐ„ ๋™์•ˆ API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ํšŸ์ˆ˜๋ฅผ ์ œํ•œํ•˜๋Š” ๊ธฐ๋ฒ•์ด๋‹ค.
์„œ๋ฒ„ ์ž์›์„ ๋ณดํ˜ธํ•˜๊ณ , ํŠน์ • ์‚ฌ์šฉ์ž์˜ ๊ณผ๋„ํ•œ ์š”์ฒญ์œผ๋กœ ์ธํ•ด ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜์ง€ ๋ชปํ•˜๋Š” ์ƒํ™ฉ์„ ๋ฐฉ์ง€ํ•œ๋‹ค.

์™œ ํ•„์š”ํ•œ๊ฐ€

  • DoS / DDoS ๋ฐฉ์–ด: ์•…์˜์ ์ธ ๋Œ€๋Ÿ‰ ์š”์ฒญ์œผ๋กœ๋ถ€ํ„ฐ ์„œ๋ฒ„๋ฅผ ๋ณดํ˜ธํ•œ๋‹ค.
  • ๊ณต์ •ํ•œ ์ž์› ๋ถ„๋ฐฐ: ํŠน์ • ํด๋ผ์ด์–ธํŠธ๊ฐ€ API๋ฅผ ๋…์ ํ•˜๋Š” ๊ฒƒ์„ ๋ง‰๋Š”๋‹ค.
  • ๋น„์šฉ ์ œ์–ด: ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ์—์„œ ๋ฌด๋ถ„๋ณ„ํ•œ ์š”์ฒญ์œผ๋กœ ์ธํ•œ ๋น„์šฉ ํญ์ฆ์„ ๋ฐฉ์ง€ํ•œ๋‹ค.
  • ์•ˆ์ •์„ฑ ๋ณด์žฅ: ํŠธ๋ž˜ํ”ฝ ๊ธ‰์ฆ ์‹œ ์„œ๋น„์Šค ์ €ํ•˜๋ฅผ ๋ง‰๋Š”๋‹ค.

์ฃผ์š” ์•Œ๊ณ ๋ฆฌ์ฆ˜

1. Fixed Window Counter

๊ฐ€์žฅ ๋‹จ์ˆœํ•œ ๋ฐฉ์‹์ด๋‹ค. ๊ณ ์ •๋œ ์‹œ๊ฐ„ ์ฐฝ(์˜ˆ: 1๋ถ„)์„ ๊ธฐ์ค€์œผ๋กœ ์š”์ฒญ ์ˆ˜๋ฅผ ์นด์šดํŠธํ•œ๋‹ค.

|-- 1๋ถ„ --| |-- 1๋ถ„ --|
  50 req      50 req

๋ฌธ์ œ์ : ์ฐฝ ๊ฒฝ๊ณ„ ๊ทผ์ฒ˜์—์„œ ๋‘ ๋ฐฐ ์š”์ฒญ์ด ํ†ต๊ณผ๋  ์ˆ˜ ์žˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด 00:59์— 50๊ฐœ, 01:00์— 50๊ฐœ ์š”์ฒญ์ด ์˜ค๋ฉด 2์ดˆ ์•ˆ์— 100๊ฐœ๊ฐ€ ์ฒ˜๋ฆฌ๋œ๋‹ค.


2. Sliding Window Log

๋ชจ๋“  ์š”์ฒญ์˜ ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ๋กœ๊ทธ๋กœ ๋‚จ๊ธฐ๊ณ , ํ˜„์žฌ ์‹œ๊ฐ ๊ธฐ์ค€์œผ๋กœ ์ตœ๊ทผ N์ดˆ ์•ˆ์˜ ์š”์ฒญ๋งŒ ์„ผ๋‹ค.

import time
from collections import deque

class SlidingWindowLog:
    def __init__(self, limit, window_sec):
        self.limit = limit
        self.window = window_sec
        self.log = deque()

    def allow(self):
        now = time.time()
        # ์˜ค๋ž˜๋œ ํƒ€์ž„์Šคํƒฌํ”„ ์ œ๊ฑฐ
        while self.log and self.log[0] < now - self.window:
            self.log.popleft()

        if len(self.log) < self.limit:
            self.log.append(now)
            return True
        return False

์ •ํ™•ํ•˜์ง€๋งŒ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ๋งŽ๋‹ค. ์š”์ฒญ์ด ๋งŽ์„์ˆ˜๋ก ๋กœ๊ทธ ํฌ๊ธฐ๊ฐ€ ์ปค์ง„๋‹ค.


3. Token Bucket

๋ฒ„ํ‚ท์— ํ† ํฐ์ด ์ผ์ • ์†๋„๋กœ ์ฑ„์›Œ์ง€๊ณ , ์š”์ฒญ๋งˆ๋‹ค ํ† ํฐ์„ ํ•˜๋‚˜์”ฉ ์†Œ๋น„ํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

ํ† ํฐ ์ถฉ์ „ ์†๋„: 10 tokens/sec
๋ฒ„ํ‚ท ์šฉ๋Ÿ‰: 100 tokens

์š”์ฒญ ๋“ค์–ด์˜ด โ†’ ํ† ํฐ ์žˆ์œผ๋ฉด ์†Œ๋น„ ํ›„ ํ—ˆ์šฉ
             โ†’ ํ† ํฐ ์—†์œผ๋ฉด ๊ฑฐ์ ˆ

ํŠน์ง•

  • ๋ฒ„์ŠคํŠธ ํŠธ๋ž˜ํ”ฝ์„ ์–ด๋А ์ •๋„ ํ—ˆ์šฉํ•œ๋‹ค (๋ฒ„ํ‚ท ์šฉ๋Ÿ‰๋งŒํผ).
  • ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ด๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ค‘ ํ•˜๋‚˜๋‹ค.
  • AWS API Gateway, Stripe ๋“ฑ์—์„œ ์‚ฌ์šฉํ•œ๋‹ค.
import time

class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.capacity = capacity
        self.tokens = capacity
        self.refill_rate = refill_rate  # tokens per second
        self.last_refill = time.time()

    def allow(self):
        now = time.time()
        elapsed = now - self.last_refill
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
        self.last_refill = now

        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

4. Leaky Bucket

์š”์ฒญ์„ ํ์— ๋„ฃ๊ณ  ์ผ์ •ํ•œ ์†๋„๋กœ๋งŒ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ๋„คํŠธ์›Œํฌ ํŠธ๋ž˜ํ”ฝ ์…ฐ์ดํ•‘์—์„œ ์œ ๋ž˜ํ–ˆ๋‹ค.

์š”์ฒญ โ†’ [ํ] โ†’ ์ผ์ • ์†๋„๋กœ ์ฒ˜๋ฆฌ
              (์ดˆ๋‹น 10๊ฐœ๋งŒ ๋‚˜๊ฐ)
  • ์ฒ˜๋ฆฌ ์†๋„๊ฐ€ ์ผ์ •ํ•˜๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ๋ฒ„์ŠคํŠธ๋ฅผ ์™„์ „ํžˆ ํก์ˆ˜ํ•˜์ง€ ๋ชปํ•˜๊ณ  ํ๊ฐ€ ๋„˜์น˜๋ฉด ์š”์ฒญ์ด ๋ฒ„๋ ค์ง„๋‹ค.
  • Token Bucket๊ณผ ๊ฐœ๋…์ด ๋น„์Šทํ•˜์ง€๋งŒ ๋ฐฉํ–ฅ์ด ๋ฐ˜๋Œ€๋‹ค.

5. Sliding Window Counter (์‹ค๋ฌด ๊ถŒ์žฅ)

Fixed Window์˜ ๊ฒฝ๊ณ„ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ฉด์„œ๋„ Sliding Window Log๋ณด๋‹ค ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์ ๊ฒŒ ์“ด๋‹ค.

ํ˜„์žฌ ์ฐฝ๊ณผ ์ด์ „ ์ฐฝ์˜ ์นด์šดํŠธ๋ฅผ ๊ฐ€์ค‘ ํ‰๊ท ์œผ๋กœ ํ•ฉ์‚ฐํ•œ๋‹ค.

ํ˜„์žฌ ์ฐฝ ๊ฒฝ๊ณผ ๋น„์œจ = 0.3 (30% ์ง€๋‚จ)
์ด์ „ ์ฐฝ ๊ฐ€์ค‘์น˜   = 1 - 0.3 = 0.7

์ถ”์ • ์š”์ฒญ ์ˆ˜ = ์ด์ „_์นด์šดํŠธ ร— 0.7 + ํ˜„์žฌ_์นด์šดํŠธ

Redis์˜ INCR + EXPIRE ์กฐํ•ฉ์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ ์‰ฌ์›Œ์„œ ์‹ค๋ฌด์—์„œ ์ž์ฃผ ์“ด๋‹ค.


Redis๋ฅผ ์ด์šฉํ•œ ๊ตฌํ˜„ ์˜ˆ์‹œ

๋ถ„์‚ฐ ํ™˜๊ฒฝ์—์„œ๋Š” ๊ฐ ์„œ๋ฒ„๊ฐ€ ๋ณ„๋„ ์นด์šดํ„ฐ๋ฅผ ๊ฐ€์ง€๋ฉด ์•ˆ ๋˜๋ฏ€๋กœ, Redis ๊ฐ™์€ ์ค‘์•™ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

import redis
import time

r = redis.Redis(host='localhost', port=6379)

def is_allowed(user_id: str, limit: int, window_sec: int) -> bool:
    key = f"rate_limit:{user_id}:{int(time.time() // window_sec)}"
    
    pipe = r.pipeline()
    pipe.incr(key)
    pipe.expire(key, window_sec * 2)
    results = pipe.execute()
    
    current_count = results[0]
    return current_count <= limit

Lua ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์“ฐ๋ฉด INCR๊ณผ EXPIRE๋ฅผ ์›์ž์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])

local count = redis.call('INCR', key)
if count == 1 then
    redis.call('EXPIRE', key, window)
end

if count > limit then
    return 0
else
    return 1
end

HTTP ์‘๋‹ต ํ—ค๋”

Rate Limit ์ •๋ณด๋Š” ์‘๋‹ต ํ—ค๋”๋กœ ํด๋ผ์ด์–ธํŠธ์— ์•Œ๋ ค์ฃผ๋Š” ๊ฒƒ์ด ๊ด€๋ก€๋‹ค.

ํ—ค๋”์˜๋ฏธ
X-RateLimit-Limitํ—ˆ์šฉ๋œ ์ตœ๋Œ€ ์š”์ฒญ ์ˆ˜
X-RateLimit-Remaining๋‚จ์€ ์š”์ฒญ ํšŸ์ˆ˜
X-RateLimit-Reset์นด์šดํ„ฐ ์ดˆ๊ธฐํ™” ์‹œ๊ฐ (Unix timestamp)
Retry-After429 ์‘๋‹ต ์‹œ ์žฌ์‹œ๋„๊นŒ์ง€ ๋Œ€๊ธฐ ์‹œ๊ฐ„ (์ดˆ)

ํ•œ๋„ ์ดˆ๊ณผ ์‹œ HTTP 429 Too Many Requests๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.


์‹ค๋ฌด ์ ์šฉ ์‹œ ๊ณ ๋ ค์‚ฌํ•ญ

์‹๋ณ„ ๊ธฐ์ค€ ์„ ํƒ

  • IP ๊ธฐ๋ฐ˜: ๊ตฌํ˜„์ด ๋‹จ์ˆœํ•˜์ง€๋งŒ NAT ํ™˜๊ฒฝ์—์„œ ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ™์€ IP๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • API ํ‚ค ๊ธฐ๋ฐ˜: ์ธ์ฆ๋œ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์ •ํ™•ํžˆ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์‚ฌ์šฉ์ž ID ๊ธฐ๋ฐ˜: ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ ํ•ฉํ•˜๋‹ค.

๋ถ„์‚ฐ ํ™˜๊ฒฝ ์ฃผ์˜์‚ฌํ•ญ

  • ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋„์›Œ์ ธ ์žˆ์œผ๋ฉด ๋ฐ˜๋“œ์‹œ Redis ๊ฐ™์€ ๊ณต์œ  ์ €์žฅ์†Œ๋ฅผ ์จ์•ผ ํ•œ๋‹ค.
  • Redis ์žฅ์•  ์‹œ fail-open(ํ—ˆ์šฉ) ๋˜๋Š” fail-closed(๊ฑฐ๋ถ€) ์ •์ฑ…์„ ๋ฏธ๋ฆฌ ๊ฒฐ์ •ํ•ด์•ผ ํ•œ๋‹ค.

๊ณ„์ธต๋ณ„ ์ ์šฉ

  • API Gateway ๋ ˆ๋ฒจ์—์„œ ์ ์šฉํ•˜๋ฉด ์„œ๋ฒ„๊นŒ์ง€ ์š”์ฒญ์ด ๋„๋‹ฌํ•˜์ง€ ์•Š์•„ ํšจ์œจ์ ์ด๋‹ค.
  • Nginx์˜ limit_req_zone, Kong, AWS API Gateway ๋“ฑ์ด ๋‚ด์žฅ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

์ฐจ๋ณ„ํ™”๋œ ํ•œ๋„

  • ๋ฌด๋ฃŒ/์œ ๋ฃŒ ํ”Œ๋žœ๋งˆ๋‹ค ๋‹ค๋ฅธ ํ•œ๋„๋ฅผ ์ ์šฉํ•˜๊ฑฐ๋‚˜, ์—”๋“œํฌ์ธํŠธ๋ณ„๋กœ ๋‹ค๋ฅธ ์ •์ฑ…์„ ์ ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค.

์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋น„๊ต

์•Œ๊ณ ๋ฆฌ์ฆ˜์ •ํ™•๋„๋ฉ”๋ชจ๋ฆฌ๋ฒ„์ŠคํŠธ ํ—ˆ์šฉ๊ตฌํ˜„ ๋‚œ์ด๋„
Fixed Window๋‚ฎ์Œ๋‚ฎ์ŒO๋งค์šฐ ์‰ฌ์›€
Sliding Window Log๋†’์Œ๋†’์ŒX๋ณดํ†ต
Token Bucket๋†’์Œ๋‚ฎ์ŒO (์šฉ๋Ÿ‰๋งŒํผ)๋ณดํ†ต
Leaky Bucket๋†’์Œ๋ณดํ†ตX๋ณดํ†ต
Sliding Window Counter๋†’์Œ๋‚ฎ์ŒX๋ณดํ†ต

์‹ค๋ฌด์—์„œ๋Š” Token Bucket ๋˜๋Š” Sliding Window Counter + Redis ์กฐํ•ฉ์ด ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ธ๋‹ค.