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

๋ฐฐ๊ฒฝ

๊ฐ€์ƒ ๋ฉด์ ‘ ์‚ฌ๋ก€๋กœ ๋ฐฐ์šฐ๋Š” ๋Œ€๊ทœ๋ชจ ์‹œ์Šคํ…œ ์„ค๊ณ„ ๊ธฐ์ดˆ ์ฑ…์„ ์˜›๋‚ ๋ถ€ํ„ฐ ๊ตฌ๋งค๋ฅผ ํ–ˆ์—ˆ๋Š”๋ฐ ํ•ญ์ƒ ์–ธ์  ๊ฐ€๋Š” ์ฝ์–ด์•ผ์ง€ ํ•˜๊ณ  ์ƒ๊ฐ๋งŒ ํ–ˆ์—ˆ์ง€ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ์ง€ ์•Š์•˜์—ˆ๋‹ค.

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

The Next Two Years - Addy Osmani ์ตœ๊ทผ์— ์ด ๊ธ€์„ ์ฝ๊ณ  ๊ณต๊ฐ์„ ๋งŽ์ด ํ•˜๊ฒŒ๋˜์—ˆ๊ณ  ๊ทธ๋ž˜์„œ ์ด ์ฑ…์„ ๋นจ๋ฆฌ ์ฝ์œผ๋ฉด์„œ ์ง์ ‘ ๊ตฌํ˜„๋„ ํ•ด๋ณด๋Š” ๊ณผ์ •์„ ์ง€๊ธˆ ์•„๋‹ˆ๋ฉด ์–ธ์ œํ• ๊นŒ๋Š” ์ƒ๊ฐ์— ์ฝ๊ธฐ ์‹œ์ž‘ํ–ˆ๋‹ค.

YouTube ์ฑ„๋„ ์—ฌ๊ธฐ์„œ ๊ณต๋ถ€ ๊ธฐ๋ก์„ ๋ผ์ด๋ธŒ ๋ฐฉ์†ก์œผ๋กœ ๋งค์ผ ์ง„ํ–‰์„ ํ•˜๊ณ  ์žˆ๋‹ค ใ…Žใ…Ž


๊ณ„ํš

์ด ์ฑ…์—๋Š” ๋‹ค์–‘ํ•œ ๋Œ€๊ทœ๋ชจ ์„ค๊ณ„์— ๋Œ€ํ•œ ์‚ฌ๋ก€๋“ค์ด ์‹ค๋ ค ์žˆ๋‹ค. ํ•ด๋‹น ์ฃผ์ œ๋“ค์„ ์‹ค์ œ ์ฝ”๋“œ์™€ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•ด๋ณด๋ฉด์„œ ์ ์  ๊ฐœ์„ ์„ ํ•ด๋‚˜๊ฐ€๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ์ด๊ณ  ์ด๋Ÿฐ ๊ณผ์ •์—์„œ ๋ฐฐ์šฐ๊ณ ์ž ํ•˜๋Š”๊ฒƒ์€ ๋‹ค์–‘ํ•œ ๋„๋ฉ”์ธ์—์„œ ์–ด๋–ค ์‹์œผ๋กœ ์„ค๊ณ„๋ฅผ ํ•˜๋ฉด ๋˜๋Š”์ง€๋ฅผ ์ตํžˆ๊ณ  ์ด๋ฅผ AI๋ฅผ ํ†ตํ•ด์„œ ๊ตฌํ˜„์„ ํ•ด๋ณด๋Š” ๊ณผ์ •์„ ๊ฒฝํ—˜์„ ํ•˜๊ณ ์ž ํ•œ๋‹ค.

๋‹จ ์—ฌ๊ธฐ์„œ AI๋ฅผ ํ†ตํ•ด์„œ ๊ตฌํ˜„์„ ํ•˜์ง€๋งŒ ๋ชจ๋“  ๊ตฌํ˜„ ๊ณผ์ •์„ ๋‚ด๊ฐ€ ์ดํ•ดํ•˜๊ณ  ๋„˜์–ด๊ฐ€๋ ค๊ณ  ํ•œ๋‹ค. ๋”ธ๊น์œผ๋กœ ํ•˜๋ ค๋ฉด ํ•  ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ ์ง€๊ธˆ ์ œ๋Œ€๋กœ ๋ฐฐ์›Œ๋‘์ง€ ์•Š์œผ๋ฉด ๋‚˜์ค‘์— ์„ค๊ณ„๋ฅผ ํ•  ๋•Œ ์ด๊ฒŒ ์ง€๊ธˆ ์ƒํ™ฉ์— ์ ํ•ฉํ•œ ์„ ํƒ์ธ์ง€์™€ ์™œ ์ด๋Ÿฐ ์‹์œผ๋กœ ๊ตฌํ˜„์„ ํ–ˆ๋Š”์ง€์— ๋Œ€ํ•ด ์„ค๋ช…์„ ํ•  ์ˆ˜ ์—†์„ ๊ฒƒ ๊ฐ™์•„์„œ ์ด๋‹ค. ์œ„ ์ € ๋งํฌ์—์„œ๋„ ๊ตฌ๊ธ€ ํฌ๋กฌ ํŒ€ ๋ฆฌ๋” ๋ถ„ ๊ธ€์ฒ˜๋Ÿผ AI๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋™์‹œ์— ๋‚˜์˜ ๊ณต๋ถ€ ๋ฉ˜ํ† ๋กœ์จ๋„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

์šฐ์„  ์ˆœ์„œ๋Š” ์ด๋Ÿฐ์‹์œผ๋กœ ์ง„ํ–‰ ํ•  ๊ฒƒ ๊ฐ™๋‹ค.

  1. ์ฑ•ํ„ฐ ํ•˜๋‚˜๋ฅผ ํ•œ๋ฒˆ ํ›‘์–ด๋ณด๊ณ 
  2. AI์—๊ฒŒ ์ด๋Ÿฐ ์ด๋Ÿฐ ์ƒํ™ฉ์— ๋Œ€ํ•œ ์„ค๋ช…๊ณผ ํ•จ๊ป˜ ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ํŠธ๋ž˜ํ”ฝ ๋“ฑ์„ ๊ฐ€์ •ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋งŒ๋“ ๋‹ค
  3. ํ•ด๋‹น ์š”๊ตฌ์‚ฌํ•ญ์„ ์œ„ํ•œ ์ตœ์†Œ ๊ตฌํ˜„์„ ํ•œ๋‹ค
  4. ์ฑ…์—์„œ ์ œ์‹œํ•œ ์„ค๊ณ„๋ฅผ ํ†ตํ•ด ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณ„๋กœ ํ…Œ์ŠคํŠธ ๋ฐ ๊ฐœ์„ ์„ ํ•œ๋‹ค
  5. ํ•ด๋‹น ๊ธฐ๋ก์„ ๋‚จ๊ธด๋‹ค.

Rate Limiter(์ฒ˜๋ฆฌ์œจ ์ œํ•œ ์žฅ์น˜ ์„ค๊ณ„)

GitHub - Byuntil/rate-limiter

1. ํ”„๋กœ์ ํŠธ ๋ฐฐ๊ฒฝ

  • ์šด์˜ ์ค‘์ธ ์ปค๋ฎค๋‹ˆํ‹ฐ ์„œ๋น„์Šค์—์„œ ํŠน์ • ๊ฒŒ์‹œ๊ธ€์ด๋‚˜ ์ธ๊ธฐ API์— ์งง์€ ์‹œ๊ฐ„ ๋™์•ˆ ์š”์ฒญ์ด ๋ชฐ๋ฆฌ๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•œ๋‹ค.ย ์ผ๋ถ€๋Š” ์ •์ƒ ์‚ฌ์šฉ์ž ์œ ์ž…์ด์ง€๋งŒ,ย ์ผ๋ถ€๋Š” ์ƒˆ๋กœ๊ณ ์นจ ๋ฐ˜๋ณต์ด๋‚˜ ๋น„์ •์ƒ ํ˜ธ์ถœ๋กœ ์ธํ•ด ์„œ๋ฒ„ ์ž์›์„ ๊ณผ๋„ํ•˜๊ฒŒ ์‚ฌ์šฉํ•œ๋‹ค.
  • ์ด๋กœ ์ธํ•ด ๋‹ค์Œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹ค. (์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋ฒ„ ๋ถ€ํ•˜ ์ฆ๊ฐ€, DB ์กฐํšŒ๋Ÿ‰ ๊ธ‰์ฆ, ํŠน์ • ์‚ฌ์šฉ์ž์˜ ๊ณผ๋„ํ•œ ํ˜ธ์ถœ๋กœ ์ „์ฒด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ €ํ•˜, ๋กœ๊ทธ์ธ/๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž ๊ฐ„ ๊ณต์ •ํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ๋Ÿ‰ ๋ถ„๋ฐฐ)
  • ๊ทธ๋ž˜์„œ API ์š”์ฒญ ์ˆ˜๋ฅผ ์ผ์ • ๊ธฐ์ค€์œผ๋กœ ์ œํ•œํ•˜๋Š” ์ฒ˜๋ฆฌ์œจ ์ œํ•œ ์žฅ์น˜๋ฅผ ์„ค๊ณ„ํ•˜๊ณ  ๊ตฌํ˜„ํ•œ๋‹ค.

2. ์„œ๋น„์Šค ๊ฐ€์ •

  • ์„œ๋น„์Šค๋Š” ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ ๊ธฐ๋Šฅ์ด ์žˆ๋Š” ์ปค๋ฎค๋‹ˆํ‹ฐ ์„œ๋น„์Šค๋ผ๊ณ  ๊ฐ€์ •ํ•œ๋‹ค.
  • ๋Œ€์ƒ API - GETย /posts/{postId}
  • API ํŠน์ง•ย 
    • ์กฐํšŒ ์š”์ฒญ์ด ๋งค์šฐ ๋งŽ๋‹ค
    • ์ธ๊ธฐ ๊ฒŒ์‹œ๊ธ€์— ํŠธ๋ž˜ํ”ฝ์ด ๋ชฐ๋ฆด ์ˆ˜ ์žˆ๋‹ค
    • ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” IP ๊ธฐ์ค€์œผ๋กœ ์‹๋ณ„ํ•ด์•ผ ํ•œ๋‹ค
    • ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ์‚ฌ์šฉ์ž ID ๊ธฐ์ค€์œผ๋กœ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค

3. ๋ชฉํ‘œ

  • ์ด ํ”„๋กœ์ ํŠธ์˜ ๋ชฉํ‘œ๋Š” ๋‹จ์ˆœํžˆ ์š”์ฒญ์„ ๋ง‰๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ,ย ํ˜„์‹ค์ ์ธ ์ œ์•ฝ์ด ์žˆ๋Š” ํ™˜๊ฒฝ์—์„œ rate limiter๋ฅผ ์ ์ง„์ ์œผ๋กœ ์„ค๊ณ„ยท๊ตฌํ˜„ยท๊ฐœ์„ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
  • ์ตœ์ข…์ ์œผ๋กœ ๋‹ค์Œ์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•œ๋‹ค.
    • ๋‹จ์ผ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ์˜ ๊ธฐ๋ณธ ์ฒ˜๋ฆฌ์œจ ์ œํ•œ ๊ตฌํ˜„
    • ๋™์‹œ์„ฑ ๋ฌธ์ œ ์žฌํ˜„ ๋ฐ ๊ฐœ์„ 
    • ๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ์˜ ํ•œ๊ณ„ ๋ถ„์„
    • Redis ๊ธฐ๋ฐ˜ ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ์œจ ์ œํ•œ ์žฅ์น˜ ๊ตฌํ˜„
    • ์•Œ๊ณ ๋ฆฌ์ฆ˜๋ณ„ ์ฐจ์ด์  ๋น„๊ต
    • ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•œ ๊ฒ€์ฆ

4. ์š”๊ตฌ์‚ฌํ•ญ

  • ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ
    • ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” IP ๊ธฐ์ค€์œผ๋กœ ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ ์ œํ•œํ•œ๋‹ค
    • ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ์‚ฌ์šฉ์ž ID ๊ธฐ์ค€์œผ๋กœ ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ ์ œํ•œํ•œ๋‹ค
    • ๊ด€๋ฆฌ์ž๋Š” ์ œํ•œ ๋Œ€์ƒ์—์„œ ์ œ์™ธํ•œ๋‹ค
    • ์ œํ•œ ์ดˆ๊ณผ ์‹œ 429 Too Many Requests ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค
    • ์‘๋‹ต ํ—ค๋”์— ๋‚จ์€ ์š”์ฒญ ์ˆ˜์™€ ์ œํ•œ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ๋‹ค
  • ์ •์ฑ… ์š”๊ตฌ์‚ฌํ•ญ
    • ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž:ย 1๋ถ„์— 30ํšŒ
    • ๊ด€๋ฆฌ์ž:ย ์ œํ•œ ์—†์Œ
  • ๋น„๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ
    • ๋™์‹œ ์š”์ฒญ ์ƒํ™ฉ์—์„œ๋„ ๊ฐ€๋Šฅํ•œ ํ•œ ์ •ํ™•ํ•ด์•ผ ํ•œ๋‹ค
    • ์„œ๋ฒ„๊ฐ€ ์—ฌ๋Ÿฌ ๋Œ€์—ฌ๋„ ๋™์ผํ•œ ์ œํ•œ ์ •์ฑ…์ด ์ ์šฉ๋˜์–ด์•ผ ํ•œ๋‹ค
    • ์ถ”ํ›„ ๋‹ค๋ฅธ API์—๋„ ํ™•์žฅ ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค
    • ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ต์ฒด๊ฐ€ ๊ฐ€๋Šฅํ•˜๋„๋ก ๊ตฌ์กฐ๋ฅผ ๋ถ„๋ฆฌํ•ด์•ผ ํ•œ๋‹ค

1์ฐจ ์‹œ๋‚˜๋ฆฌ์˜ค : ๋‹จ์ผ ์„œ๋ฒ„ + ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ Fixed Window

  • ๊ฐ€์žฅ ๋‹จ์ˆœํ•œ ๋ฐฉ์‹
  • ์‚ฌ์šฉ์ž๋ณ„ key ์ƒ์„ฑ -> ํ˜„์žฌ 1๋ถ„ ์œˆ๋„์šฐ ๋‚ด ์š”์ฒญ ์ˆ˜๋ฅผ ์ €์žฅํ•œ๋‹ค -> ์ œํ•œ ํšŸ์ˆ˜๋ฅผ ๋„˜์œผ๋ฉด ์ฐจ๋‹จํ•œ๋‹ค
  • ex) user:123 -> 1๋ถ„๋™์•ˆ 100ํšŒ, ip:10.0.0.1 -> 1๋ถ„ ๋™์•ˆ 30ํšŒ
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
@Component  
public class FixedWindowRateLimiter implements RateLimiter {  
  
    private final Map<String, Integer> request = new HashMap<>();  
  
    @Override  
    public RateLimitResult tryAcquire(String key, int limit, int windowSeconds) {  
        String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));  
        String value = key + ":" + now;  
  
        int count = request.getOrDefault(value, 0) + 1;  
        request.put(value, count);  
  
        long resetAt = LocalDateTime.now()  
                .withSecond(0).withNano(0)  
                .plusMinutes(1)  
                .toEpochSecond(ZoneOffset.UTC);  
  
        if (count <= limit) {  
            return RateLimitResult.allowed(limit, limit - count, resetAt);  
        } else {  
            return RateLimitResult.blocked(limit, resetAt);  
        }  
    }  
    
    @Scheduled(fixedRate = 60_000)  
    public void cleanUp() {  
        String currentWindow = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));  
        request.entrySet().removeIf(e -> !e.getKey().contains(":" + currentWindow));  
    }
  • ํ•ด๋‹น ํ•„ํ„ฐ๋ฅผ ์ธํ„ฐ์…‰ํ„ฐ์—์„œ ์‚ฌ์šฉํ•˜์—ฌ rateLimiter.tryAcquire() HTTP ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„์„œ rate limit๋ฅผ ํ•˜๋„๋ก ๊ตฌํ˜„์„ ํ–ˆ๋‹ค.

  • HashMap ์‚ฌ์šฉ ์ด์œ ?
    • ์‚ฌ์šฉ์ž๋ณ„ ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ key-value๋กœ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด HashMap์„ ์„ ํƒํ–ˆ๋‹ค.
    • 1์ฐจ์—์„œ๋Š” ๋™์‹œ์„ฑ์„ ๊ณ ๋ คํ•˜์ง€ ์•Š๊ณ , ๊ฐ€์žฅ ์ง๊ด€์ ์ธ ๋ฐฉ์‹(getOrDefault + put)์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค.
  • Interceptor ์„ ํƒํ•œ ์ด์œ ?
    • spring mvc๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ˆœ์„œ๋ฅผ ๊ฐ€์ง„๋‹ค
    • HTTP ์š”์ฒญ -> Filter -> DispatcherServlet -> Interceptor -> Controller
    • ๊ทผ๋ฐ Filter์— ๋น„ํ•ด Interceptor๋Š” ๋” spring mvc์— ๊ตฌ์กฐ์— ํŠนํ™”๋œ ํ•„ํ„ฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณต์„ ํ•œ๋‹ค. spring bean์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์˜์กด์„ฑ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ๋Š”๊ฒƒ์ด ๊ฐ€์žฅ ํฐ ํŠน์ง•์ด๋‹ค.
    • ๊ทธ๋ž˜์„œ FixedWindowRateLimiter๋Š” ์Šคํ”„๋ง ๋นˆ์— ์˜์กดํ•˜๋ฏ€๋กœ ์‚ฌ์šฉ์„ ํ–ˆ๋‹ค.

RateLimitInterceptor ํ๋ฆ„

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
preHandle ์ง„์ž…
    โ”‚
    โ–ผ
๊ด€๋ฆฌ์ž ์—ฌ๋ถ€ ํ™•์ธ (isAdmin)
    โ”‚ YES โ†’ return true (ํ†ต๊ณผ)
    โ”‚ NO
    โ–ผ
ํด๋ผ์ด์–ธํŠธ ํ‚ค ์ถ”์ถœ (resolve)
    โ”‚ ๋กœ๊ทธ์ธ: "user:123"
    โ”‚ ๋น„๋กœ๊ทธ์ธ: "ip:127.0.0.1"
    โ–ผ
limit ๊ฒฐ์ •
    โ”‚ ๋กœ๊ทธ์ธ: defaultLimit (100)
    โ”‚ ๋น„๋กœ๊ทธ์ธ: anonymousLimit (30)
    โ–ผ
RateLimiter.tryAcquire(key, limit, windowSize)
    โ”‚
    โ”œโ”€ allowed=true โ†’ ํ—ค๋” ์ถ”๊ฐ€ ํ›„ return true
    โ”‚
    โ””โ”€ allowed=false โ†’ 429 JSON ์‘๋‹ต ํ›„ return false


2์ฐจ ์‹œ๋‚˜๋ฆฌ์˜ค: ๋™์‹œ์„ฑ ๋ฌธ์ œ ์žฌํ˜„ ๋ฐ ๊ฐœ์„ 

๋ชฉํ‘œ

  • 1์ฐจ ๊ตฌํ˜„(FixedWindowRateLimiter)์—์„œ ๋™์‹œ ์š”์ฒญ ์‹œ limit์„ ์ดˆ๊ณผํ•œ ์š”์ฒญ์ด ํ—ˆ์šฉ๋˜๋Š” ํ˜„์ƒ์„ย ์žฌํ˜„ํ•˜๊ณ ,ย ์›์ธ์„ ๋ถ„์„ํ•œ ๋’คย ๊ฐœ์„ ํ•ด๋ณด์ž

ํ˜„์žฌ ๊ตฌํ˜„์˜ ๋ฌธ์ œ

1
2
3
4
@Component  
public class FixedWindowRateLimiter implements RateLimiter {  
  
    private final Map<String, Integer> request = new HashMap<>();
1
2
3
4
5
6
7
8
int count = request.getOrDefault(value, 0) + 1; //์ฝ๊ธฐ
request.put(value, count); //์“ฐ๊ธฐ

if (count <= limit) {
    return RateLimitResult.allowed(...);
} else {
    return RateLimitResult.blocked(...);
}
  • ๋งŒ์•ฝ 200๊ฐœ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์š”์ฒญํ•˜๋ฉด ํ—ˆ์šฉ ์ˆ˜๊ฐ€ limit๋ฅผ ์ดˆ๊ณผํ• ๊นŒ? seukeulinsyas-2026-03-21-ojeon-12-55-49.png
    • ์‹ค์ œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋Œ๋ ค๋ณธ ๊ฒฐ๊ณผ allow ๋œ ์ˆ˜๊ฐ€ limit๋กœ ์„ค์ •ํ•œ 100๋ณด๋‹ค ํฐ ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ธฐ๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ์ด์œ ๊ฐ€ ๋ญ˜๊นŒ?
  • ํ˜„์žฌ ์ƒํƒœ
    • ์ง€๊ธˆ FixedWindowRateLimiter ๊ฐ™์€ ๊ฒฝ์šฐ @Component์ด๋ฏ€๋กœ ์‹ฑ๊ธ€ํ†ค ๋นˆ ํ•˜๋‚˜๋งŒ ์กด์žฌ๋ฅผ ํ•ด์„œ ๋ชจ๋“  ์Šค๋ ˆ๋“œ๊ฐ€ HashMap์„ ๊ณต์œ ํ•˜๊ณ  ์žˆ๋‹ค.
    • getOrDefault() ์ฝ๊ธฐ์™€ request.put() ์“ฐ๊ธฐ๊ฐ€ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ๋‹ค.
    • ๋‹จ์ผ ์Šค๋ ˆ๋“œ์—์„œ๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์ง€๋งŒ, ๋™์‹œ ์š”์ฒญ ์‹œ ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๊ฐ™์€ ๊ฐ’์„ ์ฝ๊ณ  ๊ฐ™์€ ๊ฐ’์„ ์“ธ ์ˆ˜ ์žˆ๋‹ค
      • ์˜ˆ๋ฅผ ๋“ค์–ด ์Šค๋ ˆ๋“œ A,B,C๊ฐ€ ์žˆ์œผ๋ฉด count๊ฐ€ ํ˜„์žฌ 99๋ผ๊ณ  ๊ฐ€์ •์„ ํ•˜๋ฉด A ์Šค๋ ˆ๋“œ๊ฐ€ ์ฝ๊ณ  ์“ฐ๋Š” ์‚ฌ์ด์— ์Šค๋ ˆ๋“œ B์™€ C๋„ ํ˜„์žฌ count ๊ฐ’์„ ์ฝ์œผ๋ฉด ์—ฌ์ „ํžˆ 99์ด๋‹ค. ๊ทธ๋ž˜์„œ 3๊ฐœ์˜ ์Šค๋ ˆ๋“œ ๋ชจ๋‘ ํ—ˆ์šฉ์ด ๋˜๋Š” ๋งค์ง~~ ์ด ๋ฐœ์ƒํ•œ๋‹ค.
      • ๊ทธ๋ž˜์„œ ์œ„ ์‚ฌ์ง„๊ณผ ๊ฐ™์ด ํ—ˆ์šฉ๋œ ์š”์ฒญ ์ˆ˜๊ฐ€ limit ๋ณด๋‹ค ๋” ์ปค์ง€๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ธฐ๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋ฏธ ์ดˆ๊ณผ ํ—ˆ์šฉ๋œ B์™€ C๋Š” ๋˜๋Œ๋ฆด ์ˆ˜ ์—†๋‹ค

๊ฐœ์„ 

  • ๊ทธ๋ ‡๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์„๊นŒ? ๊ฒฐ๋ก  ๋ถ€ํ„ฐ ๋งํ•˜๋ฉด ConcurrentHashMap + AtomicInteger
1
2
3
4
5
6
7
8
private final ConcurrentHashMap<String, AtomicInteger> request = new ConcurrentHashMap<>();  
  
@Override  
public RateLimitResult tryAcquire(String key, int limit, int windowSeconds) {  
    String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));  
    String value = key + ":" + now;  
  
    int count = request.computeIfAbsent(value, k -> new AtomicInteger(0)).incrementAndGet();
  • ConcurrentHashMap๊ณผ AtomicInteger์‚ฌ์šฉ ์ด์œ ?
    • ConcurrentHashMap์€ ๋™์‹œ์„ฑ ์ปฌ๋ ‰์…˜์˜ ํ•œ ์ข…๋ฅ˜๋กœ HashMap์— ๊ฐœ์„  ๋ฒ„์ „์ด๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋Š”๋ฐ ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ ์— ๊ฐ™์€ Map์— ์ ‘๊ทผํ•  ๋•Œ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๋ณด์žฅํ•œ๋‹ค.
      • HashMap๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š” ๋™์‹œ ์ ‘๊ทผ ์‹œ ๋‚ด๋ถ€ ๊ตฌ์กฐ๊ฐ€ ๊นจ์งˆ ์ˆ˜ ์žˆ์ง€๋งŒ ConcurrentHashMap์€ ๋‚ด๋ถ€์ ์œผ๋กœ ๋ฒ„ํ‚ท ๋‹จ์œ„ ๋ฝ์„ ์‚ฌ์šฉํ•ด์„œ ์ด๋ฅผ ๋ฐฉ์ง€ํ•œ๋‹ค.
    • AtomicInteger ๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š” ์นด์šดํŠธ์˜ ์ฝ๊ธฐ + ์ฆ๊ฐ€ + ์“ฐ๊ธฐ๋ฅผ ํ•˜๋‚˜์˜ ์›์ž์  ์—ฐ์‚ฐ์œผ๋กœ incrementAndGet()์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค. CAS์—ฐ์‚ฐ์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‘ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ํ˜ธ์ถœํ•ด๋„ ๋ฐ˜๋“œ์‹œ ๋‹ค๋ฅธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ๊ฐ™์€ ๊ฐ’์„ ์ฝ๊ณ  ๊ฐ™์€ ๊ฐ’์„ ์“ฐ๋Š” ๋ฌธ์ œ๋ฅผ ์ฐจ๋‹จํ•œ๋‹ค.
      • int count = request.computeIfAbsent(value, k -> new AtomicInteger(0)).incrementAndGet(); ์ด ์—ฐ์‚ฐ ์ž์ฒด๊ฐ€ ์›์ž์ ์œผ๋กœ ์ง„ํ–‰๋˜๋Š” ๊ฒƒ์ด๋‹ค.

    seukeulinsyas-2026-03-21-ojeon-1-27-12.png

    • ์ด์ œ ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์š”์ฒญํ•ด๋„ ํ—ˆ์šฉ์ˆ˜๊ฐ€ limit๋ฅผ ์ดˆ๊ณผํ•˜์ง€ ์•Š๊ณ  ์ •ํ™•ํžˆ 100์—์„œ ๋ฉˆ์ถ”๊ฒŒ ๋˜์—ˆ๋‹ค.

๋‹ค์Œ ํŽธ

  • ์ด์ œ ๋‹จ์ผ ์„œ๋ฒ„์—์„œ ์ƒ๊ธธ์ˆ˜ ์žˆ๋Š” ๋™์‹œ์„ฑ์€ ํ•ด๊ฒฐํ–ˆ์ง€๋งŒ, ์„œ๋ฒ„๊ฐ€ ๋งŒ์•ฝ ์—ฌ๋Ÿฌ ๋Œ€๋ผ๋ฉด? ๊ฐ ์„œ๋ฒ„์˜ ๋…๋ฆฝ์ ์ด counter๊ฐ€ ์ƒ๊ธฐ๋Š”๋ฐ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜์—†๊ฒŒ ๋œ๋‹ค.
  • ๊ฒฐ๊ตญ ์–ด๋–ค ๊ณต์œ  ์ €์žฅ์†Œ๊ฐ€ ํ•„์š”ํ•œ๊ฒƒ์ธ๋ฐ ๋‹ค์ŒํŽธ์—์„œ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐ ํ•ด ๋ณผ ์ˆ˜ ์žˆ์„์ง€์— ๋Œ€ํ•ด ๋‹ค๋ค„๋ณด์ž