๐Ÿ“š Rate Limiter ๊ฐœ์„  ์‹œ๋‚˜๋ฆฌ์˜ค - 3

download.jpg


6์ฐจ ์‹œ๋‚˜๋ฆฌ์˜ค : Sliding Window Counter

  • ๊ธฐ๋ณธ ๊ฐœ๋…์€ ํ˜„์žฌ ์œˆ๋„์šฐ์˜ ์นด์šดํŠธ๋งŒ ๋ณด๋Š” ๋Œ€์‹ , ์ด์ „ ์œˆ๋„์šฐ์˜ ์นด์šดํŠธ๋ฅผ ๊ฒฝ๊ณผ ๋น„์œจ๋กœ ๊ฐ์†Œ์‹œ์ผœ ํ•จ๊ป˜ ๋ฐ˜์˜ ํ•˜๋Š”๊ฑฐ๋ผ๊ณ  ํ•˜๋Š”๋ฐ ๋ง๋กœ๋ณด๋ฉด ์กฐ๊ธˆ ์–ด๋ ต๋‹ค. seukeulinsyas-2026-03-23-ohu-10-51-00.png

  • ๋งŒ์•ฝ ์ฒ˜๋ฆฌ์œจ ์ œํ•œ ์žฅ์น˜์˜ ํ•œ๋„๊ฐ€ ๋ถ„๋‹น 7๊ฐœ ์š”์ฒญ์œผ๋กœ ์„ค์ • ๋˜์–ด์žˆ์œผ๋ฉด ์ด์ „ 1๋ถ„๋™์•ˆ 5๊ฐœ์˜ ์š”์ฒญ(๋ณด๋ผ์ƒ‰ ๋„ค๋ชจ), ๊ทธ๋ฆฌ๊ณ  ํ˜„์žฌ ์‹œ์ ์— 3๊ฐœ์˜ ์š”์ฒญ์ด ์™€ ์žˆ๋Š” ์ƒํƒœ์ด๋‹ค.
  • ํ˜„์žฌ์˜ 1๋ถ„์˜ 30% ์‹œ์ ์— ๋„๋‹ฌํ•œ ์‹œ์ ์— ๋„์ฐฉํ•œ ์ƒˆ ์š”์ฒญ์˜ ๊ฒฝ์šฐ, ํ˜„์žฌ ์œˆ๋„์— ๋ช‡๊ฐœ ์š”์ฒญ์ด ์˜จ๊ฒƒ์œผ๋กœ ๋ณด๊ณ  ์ฒ˜๋ฆฌํ•ด์•ผํ• ๊นŒ?
    • ํ˜„์žฌ 1๋ถ„๊ฐ„์˜ ์š”์ฒญ ์ˆ˜ + ์ง์ „ 1๋ถ„๊ฐ„์˜ ์š”์ฒญ์ˆ˜ x ์ด๋™ ์œˆ๋„์™€ ์ง์ „ 1๋ถ„์ด ๊ฒน์น˜๋Š” ๋น„์œจ
    • ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ํ˜„์žฌ ์œˆ๋„์— ๋“ค์–ด ์žˆ๋Š” ์š”์ฒญ์€ ๊ณต์‹์— ๋”ฐ๋ฅด๋ฉด 6.5๊ฐœ์ด๋‹ค. (๋ฐ˜์˜ฌ๋ฆผ or ๋ฐ˜๋‚ด๋ฆผ)
    • => ๊ฒฐ๊ตญ ์ฒ˜๋ฆฌ์œจ ํ•œ๋„๊ฐ€ ๋ถ„๋‹น 7๊ฐœ์ด๋‹ˆ๊นŒ 30% ๋”ฑ ๋„๋‹ฌํ•œ ํ˜„์žฌ ์ดํ›„์—๋Š” ํ•œ๋„ ์ดˆ๊ณผ์— ๋‹ฌ์„ฑํ•ด์„œ ๋”์ด์ƒ ์š”์ฒญ์€ ๋ฐ›์„ ์ˆ˜ ์—†๋‹ค..
  • ๊ฒฐ๊ตญ ์ด์ „ ์‹œ๊ฐ„๋Œ€์˜ ํ‰๊ท  ์ฒ˜๋ฆฌ์œจ์— ๋”ฐ๋ผ ํ˜„์žฌ ์œˆ๋„์˜ ์ƒํƒœ๋ฅผ ๊ณ„์‚ฐํ•œ๋‹ค๋Š” ํŠน์ง•์ด ์žˆ์–ด์„œ ๊ธฐ์กด ๊ณ ์ • ์œˆ๋„์šฐ์™€ ๋‹ฌ๋ฆฌ ์งง์€ ์‹œ๊ฐ„์— burst ๋˜๋Š” ํŠธ๋ž˜ํ”ฝ์—๋„ ์ž˜ ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

๊ตฌํ˜„

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
String currentWindowKey = "rate_limit:" + key + ":" +  
        now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));  
String previousWindowKey = "rate_limit:" + key + ":" +  
        now.minusMinutes(1).format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));  
  
String prevCountStr = redisTemplate.opsForValue().get(previousWindowKey);  
long previousCount = (prevCountStr != null) ? Long.parseLong(prevCountStr) : 0;  
  
String currCountStr = redisTemplate.opsForValue().get(currentWindowKey);  
long currentCount = (currCountStr != null) ? Long.parseLong(currCountStr) : 0;  
  
// ๊ฐ€์ค‘ ํ‰๊ท  = ์ด์ „ ์œˆ๋„์šฐ ์นด์šดํŠธ ร— (1 - ๊ฒฝ๊ณผ ๋น„์œจ) + ํ˜„์žฌ ์œˆ๋„์šฐ ์นด์šดํŠธ  
double elapsedRatio = currentSecond / (double) windowSeconds;  
double weightedCount = previousCount * (1 - elapsedRatio) + currentCount;  
  
long resetAt = now.withSecond(0).withNano(0)  
        .plusMinutes(1)  
        .toEpochSecond(ZoneOffset.UTC);  
  
if (weightedCount >= limit) {  
    return RateLimitResult.blocked(limit, resetAt);  
}  
  
Long newCount = redisTemplate.opsForValue().increment(currentWindowKey);  
if (newCount != null && newCount == 1) {  
    redisTemplate.expire(currentWindowKey, Duration.ofSeconds(windowSeconds * 2L));  
}  
  
int remaining = (int) Math.max(0, limit - (weightedCount + 1));  
return RateLimitResult.allowed(limit, remaining, resetAt);
  • ํ˜„์žฌ์™€ ์ด์ „ window๋ฅผ ๋งŒ๋“ค๊ณ  ๊ฐ๊ฐ ์ •ํ•ด์ง„ ๊ณต์‹์œผ๋กœ ๊ฐ€์ค‘๊ฐ’์„ ๊ณ„์‚ฐํ•œ๋‹ค์Œ ๊ฐ€์ค‘ ํ‰๊ท  ๊ฐ’์ด ํ•œ๊ณ„ ๊ฐ’๋ณด๋‹ค ํฌ๋ฉด ์ฐจ๋‹จํ•˜๋„๋ก ๊ตฌํ˜„์„ ํ–ˆ๋‹ค.

  • ์•„๋ž˜๋Š” 5์ฐจ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ์ž‘์„ฑํ–ˆ๋˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ RedisSlidingWindowCounterRateLimiter๋ฅผ ์ ์šฉํ–ˆ์„ ๋•Œ์˜ ๊ฒฐ๊ณผ๋‹ค 5์ฐจ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ๋Š” 12:30:59์™€ 12:31:00 1์ดˆ ์ฐจ์ด๊ฐ€ ๋˜๋„๋ก ๊ณ ์ •ํ•œ ํ›„ ๊ฐ๊ฐ 100ํšŒ์”ฉ ์š”์ฒญ์„ ํ–ˆ์„๋•Œ 1์ดˆ ์‚ฌ์ด์— ์ด 200ํšŒ๊ฐ€ ํ—ˆ์šฉ์ด ๋˜์—ˆ๋˜ ๋ฐ˜๋ฉด ์ง€๊ธˆ์€ ์ •ํ™•ํžˆ limit๋งŒ ํ—ˆ์šฉ๋˜๋Š”๊ฑธ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

seukeulinsyas-2026-03-23-ohu-11-26-28.png

ํ•œ๊ณ„

  • ๋ฌผ๋ก  ์•„์ง ํ•œ๊ณ„๋Š” ์žˆ๋‹ค. ์ด์ „ ์œˆ๋„์šฐ ์š”์ฒญ์ด ๊ท ๋“ฑํ•˜๊ฒŒ ๋ถ„ํฌํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•œ ์ƒํƒœ์—์„œ ์ถ”์ •์น˜๋ฅผ ๊ณ„์‚ฐํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์†Œ ๋А์Šจํ•˜๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.
  • ํ•˜์ง€๋งŒ ์ด ํ•œ๊ณ„๋Š” ์‹ฌ๊ฐํ•œ๊ฑด ์•„๋‹ˆ๋ผ๊ณ  ํ•œ๋‹ค. ํด๋ผ์šฐ๋“œํ”Œ๋ ˆ์–ด์—์„œ ์‹ค์‹œํ–ˆ๋˜ ์‹คํ—˜์— ๋”ฐ๋ฅด๋ฉด 40์–ต๊ฐœ์˜ ์š”์ฒญ ๊ฐ€์šด๋ฐ ์‹œ์Šคํ…œ์˜ ์‹ค์ œ ์ƒํƒœ์™€ ๋งž์ง€ ์•Š๊ฒŒ ํ—ˆ์šฉ๋˜๊ฑฐ๋‚˜ ๋ฒ„๋ ค์ง„ ์š”์ฒญ์€ 0.003%์— ๋ถˆ๊ณผํ–ˆ๋‹ค๊ณ  ํ•œ๋‹ค.

ํ›„๊ธฐ

  • ํ•œ ์ฑ•ํ„ฐ๋ฅผ ์ฝ์œผ๋ฉด์„œ ์ดํ•ดํ•˜๊ณ  ์ฝ”๋“œ๋กœ ์˜ฎ๊ธฐ๋Š” ๊ณผ์ •๊นŒ์ง€ ์‹ฌ๋„ ์žˆ๊ฒŒ ๊ณต๋ถ€๋ฅผ ํ•ด๋ดค๋‹ค
  • ์ด ์„ ํƒ์— ๋Œ€ํ•œ ์˜๋„๋ฅผ ํŒŒ์•…ํ•˜๋ฉด์„œ ํ•œ๊ณ„๋ฅผ ๊ฐœ์„ ํ•ด ๋‚˜๊ฐ€๋ฉด์„œ ๋‹จ๊ณ„๋ณ„๋กœ ๊ตฌํ˜„์„ ํ•œ ๊ฒฝํ—˜์€ ๋‹ค์†Œ ์‹œ๊ฐ„์ด ๊ฑธ๋ ธ์ง€๋งŒ ์ฝ”๋“œ ์ดํ•ด ๋ฟ๋งŒ์ด ์•„๋‹ˆ๋ผ ์‚ฌ๊ณ ํ•˜๋Š” ๊ณผ์ •๋„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์–ด์„œ ์ข‹์•˜๋˜๊ฒƒ ๊ฐ™๋‹ค.
  • ์•ž์œผ๋กœ 2๊ถŒ๊นŒ์ง€ ํ•˜๋ฉด ๋„ˆ๋ฌด ๋งŽ์€ ์ฑ•ํ„ฐ๊ฐ€ ๋‚จ์•„์žˆ๋Š”๋ฐ ๋ช‡ ์‹œ๊ฐ„์„ ํ–ˆ๋Š”์ง€๋Š” ์ •ํ™•ํžˆ ์„ธ์–ด๋ณด์ง€๋Š” ์•Š์•˜๋Š”๋ฐ ์ผ์ž๋กœ๋Š” ํ•˜๋ฃจ์— 2์‹œ๊ฐ„ ์ •๋„ํ•ด์„œ 4์ผ์ •๋„ ํ–ˆ๋˜๊ฒƒ ๊ฐ™๋‹ค.
  • ๋ชจ๋“  ์ฑ•ํ„ฐ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ํ•˜๊ธฐ ๋ณด๋‹ค๋Š” ์žฌ๋ฐŒ์–ด ๋ณด์ด๊ณ  ๋‚˜์—๊ฒŒ ๋„์›€์ด ๋งŽ์ด ๋ ๋งŒํ•œ ์ฃผ์ œ๋กœ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ •ํ•ด์„œ ์ฒœ์ฒœํžˆ ํ•ด๋ณผ ์ƒ๊ฐ์ด๋‹ค.
  • ๊ทธ๋ž˜๋„ ํ•œ ์ฑ•ํ„ฐ์ด์ง€๋งŒ ๋๊นŒ์ง€ ์™„์ฃผํ•ด์„œ ๊ธฐ๋ถ„์ด ์ข‹๋‹ค.