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

download.jpg


3์ฐจ ์‹œ๋‚˜๋ฆฌ์˜ค : ๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ์˜ ํ•œ๊ณ„ ๋ถ„์„

  • ์ด์ „ ๊ธ€์—์„œ ๋งํ•œ ๊ฒƒ ์ฒ˜๋Ÿผ ์„œ๋ฒ„๊ฐ€ ๋งŒ์•ฝ ์—ฌ๋Ÿฌ ๋Œ€๋ผ๋ฉด? ์ด๋ผ๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํ†ตํ•ด ๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ์˜ ํ•œ๊ณ„ ๋ถ„์„์„ ํ•ด๋ณด๊ฒ ๋‹ค

๋ฌธ์ œ ์ƒํ™ฉ

  • ํ˜„์žฌ ์ƒํ™ฉ์€ ๋‹จ์ผ ์„œ๋ฒ„ ํ™˜๊ฒฝ์ธ๋ฐ ๋งŒ์•ฝ ์„œ๋ฒ„๊ฐ€ ์—ฌ๋Ÿฌ๋Œ€์ธ ํ™˜๊ฒฝ์ด๋ผ๋ฉด?
1
client -> server -> DB
  • ๊ธฐ์กด count ๊ฐ’์„ ์„œ๋ฒ„์˜ ConcurrentHashMap(JVM ๋ฉ”๋ชจ๋ฆฌ)์— ์ €์žฅ์„ ํ•˜๋„๋ก ๊ตฌํ˜„์ด ๋˜์–ด์žˆ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ๊ตฌ์„ฑ์—์„œ๋Š” ๊ฐ๊ฐ์˜ JVM ์ธ์Šคํ„ด์Šค๋งˆ๋‹ค ์นด์šดํŠธ ๊ฐ’์ด ๋…๋ฆฝ์ ์ด๋œ๋‹ค.
  • ๊ฐ™์€ ์‚ฌ์šฉ์ž user:123์ด limit = 100์ธ๋ฐ, ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ๊ฐ€ ์š”์ฒญ์„ ๋ถ„์‚ฐํ•˜๋ฉด ๊ฐ๊ฐ์˜ ์ธ์Šคํ„ด์Šค์— 40์”ฉ ๋ถ„์‚ฐ์„ ํ–ˆ๋‹ค๊ณ  ํ•ด๋„ ๊ฐ ์ธ์Šคํ„ด์Šค ๊ธฐ์ค€์œผ๋กœ๋Š” limit๋ฅผ ์ดˆ๊ณผ๋ฅผ ํ•˜์ง€ ์•Š์•˜์ง€๋งŒ ์ „์ฒด ๊ตฌ์กฐ์—์„œ๋Š” ์ด๋ฏธ 120์œผ๋กœ limit๋ฅผ ์ดˆ๊ณผ๋ฅผ ํ–ˆ๋‹ค => ๊ฐ ์„œ๋ฒ„๋Š” ์ž๊ธฐ ์นด์šดํ„ฐ๋งŒ ๋ณด๊ธฐ ๋•Œ๋ฌธ์— ์ „์ฒด ์ œํ•œ์ด ๊นจ์ง€๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค.
1
2
3
4
Load Balancer
    โ”œโ”€โ”€ Server A (ConcurrentHashMap: user:123 โ†’ 40)
    โ”œโ”€โ”€ Server B (ConcurrentHashMap: user:123 โ†’ 40)
    โ””โ”€โ”€ Server C (ConcurrentHashMap: user:123 โ†’ 40)
  • ์ฆ‰ ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ rate limiter๋Š” ๋‹จ์ผ ์„œ๋ฒ„์—์„œ๋งŒ ์œ ํšจํ•˜๋‹ค.

๋‹ค์ค‘ ์„œ๋ฒ„ ๋ฌธ์ œ ์ƒํ™ฉ ์žฌํ˜„

  • ๊ทธ๋Ÿผ ์ง์ ‘ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ์ผ ๋•Œ ์–ด๋–ค ์ผ์ด ์ผ์–ด๋‚˜๋Š”์ง€ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ํ™•์ธํ•ด๋ดค๋‹ค.

1. ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜์€ ์ „์ฒด limit์„ ๋ณด์žฅํ•˜์ง€ ๋ชปํ•œ๋‹ค.

  • ์„œ๋ฒ„๊ฐ€ ๋งŒ์•ฝ 3๋Œ€๋ฉด limit์€ ์ง€์ผœ์ง€๋Š”๊ฐ€?
  • ์ˆœ์ฐจ ๋ถ„๋ฐฐ๋ฅผ ํ•ด๋ดค๋‹ค.
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
@Test  
@DisplayName("์„œ๋ฒ„ 3๋Œ€ ์‹œ๋ฎฌ๋ ˆ์ด์…˜: ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜์€ ์ „์ฒด limit์„ ๋ณด์žฅํ•˜์ง€ ๋ชปํ•œ๋‹ค")  
void multi_server_exceeds_limit() {  
    int serverCount = 3;  
    int limit = 100;  
    int totalRequests = 200;  
  
    FixedWindowRateLimiterV2[] servers = new FixedWindowRateLimiterV2[serverCount];  
    int[] serverRequestCount = {0, 0, 0};  
    for (int i = 0; i < serverCount; i++) {  
        servers[i] = new FixedWindowRateLimiterV2();  
    }  
  
    int totalAllowed = 0;  
    for (int i = 0; i < totalRequests; i++) {  
        FixedWindowRateLimiterV2 server = servers[i % serverCount];  
        RateLimitResult result = server.tryAcquire("user:123", limit, 60);  
        if (result.allowed()) {  
            serverRequestCount[i % serverCount]++;  
            totalAllowed++;  
        }  
    }  
  
    for (int i = 0; i < serverCount; i++) {  
        System.out.println("server " + (i + 1) + ": " + serverRequestCount[i] + " requests");  
    }  
  
    System.out.println("[์ˆœ์ฐจ ๋ถ„๋ฐฐ] ์ „์ฒด ํ—ˆ์šฉ ์ˆ˜: " + totalAllowed + " (limit: " + limit + ")");  
  
    assertThat(totalAllowed)  
            .as("์„œ๋ฒ„๊ฐ€ 3๋Œ€๋ฉด ๊ฐ๊ฐ ๋…๋ฆฝ ์นด์šดํ„ฐ๋ฅผ ๊ฐ€์ง€๋ฏ€๋กœ ์ „์ฒด limit์„ ์ดˆ๊ณผํ•œ๋‹ค")  
            .isGreaterThan(limit);  
}

seukeulinsyas-2026-03-21-ohu-10-10-55.png

  • ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ๋ฅผ ๊ฐ€์ •ํ•ด์„œ 3๋Œ€์˜ ์„œ๋ฒ„์— 200๊ฐœ์˜ ์š”์ฒญ์„ ๋ถ„๋ฐฐ๋ฅผ ํ–ˆ๋‹ค.
  • ๋‹น์—ฐํžˆ ์˜ˆ์ƒ๋Œ€๋กœ ๊ฐ๊ฐ ๋…๋ฆฝ๋œ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๊ฐ€์ง€๊ธฐ ๋•Œ๋ฌธ์— limit 100์„ ์ดˆ๊ณผ ํ•˜๋Š” 200๊ฐœ์˜ ์š”์ฒญ์ด ๋“ค์–ด์™”๋‹ค.

2. ๋™์‹œ ์š”์ฒญ + ๋‹ค์ค‘ ์„œ๋ฒ„ : ๋™์‹œ์„ฑ๊นŒ์ง€ ๊ฒน์น˜๋ฉด ๋” ์‹ฌํ•˜๊ฒŒ ์ดˆ๊ณผํ•œ๋‹ค.

  • ๋™์‹œ ์š”์ฒญ์ด ๋‹ค์ค‘ ์„œ๋ฒ„์— ๋ถ„์‚ฐ๋˜๋ฉด ์–ด๋–ป๊ฒŒ ๋˜๋Š”๊ฐ€?
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
@Test  
@DisplayName("๋™์‹œ ์š”์ฒญ + ๋‹ค์ค‘ ์„œ๋ฒ„: ๋™์‹œ์„ฑ๊นŒ์ง€ ๊ฒน์น˜๋ฉด ๋” ์‹ฌํ•˜๊ฒŒ ์ดˆ๊ณผํ•œ๋‹ค")  
void concurrent_multi_server_exceeds_limit() throws InterruptedException {  
    int serverCount = 3;  
    int limit = 100;  
    int threadCount = 300;  
  
    FixedWindowRateLimiterV2[] servers = new FixedWindowRateLimiterV2[serverCount];  
    int[] serverRequestCount = {0, 0, 0};  
    for (int i = 0; i < serverCount; i++) {  
        servers[i] = new FixedWindowRateLimiterV2();  
    }  
  
    CountDownLatch startLatch = new CountDownLatch(1);  
    CountDownLatch doneLatch = new CountDownLatch(threadCount);  
    AtomicInteger totalAllowed = new AtomicInteger(0);  
  
    ExecutorService executor = Executors.newFixedThreadPool(threadCount);  
    for (int i = 0; i < threadCount; i++) {  
        int serverIndex = i % serverCount;  
        executor.submit(() -> {  
            try {  
                startLatch.await();  
                RateLimitResult result = servers[serverIndex].tryAcquire("user:123", limit, 60);  
                if (result.allowed()) {  
                    serverRequestCount[serverIndex]++;  
                    totalAllowed.incrementAndGet();  
                }  
            } catch (InterruptedException e) {  
                Thread.currentThread().interrupt();  
            } finally {  
                doneLatch.countDown();  
            }  
        });  
    }  
  
    startLatch.countDown();  
    doneLatch.await(5, TimeUnit.SECONDS);  
    executor.shutdown();  
  
    int allowed = totalAllowed.get();  
    for (int i = 0; i < serverCount; i++) {  
        System.out.println("server " + (i + 1) + ": " + serverRequestCount[i] + " requests");  
    }  
      
    System.out.println("[๋™์‹œ ๋ถ„๋ฐฐ] ์ „์ฒด ํ—ˆ์šฉ ์ˆ˜: " + allowed + " (limit: " + limit + ")");  
  
    assertThat(allowed)  
            .as("๋‹ค์ค‘ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ์ „์ฒด limit์„ ์ดˆ๊ณผํ•œ๋‹ค")  
            .isGreaterThan(limit);  
}

seukeulinsyas-2026-03-21-ohu-10-16-17.png

  • ์—ฌ๊ธฐ๋„ ์—ญ์‹œ ์˜ˆ์ƒ๋Œ€๋กœ ๋™์‹œ์— 300 ์š”์ฒญ์„ ๋ถ„๋ฐฐ๋ฅผ ํ•ด๋„ limit๋ฅผ ์ดˆ๊ณผํ•˜๋Š” ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์™”๋‹ค.

3. ๋‹จ์ผ ์„œ๋ฒ„ vs ๋‹ค์ค‘ ์„œ๋ฒ„: ๊ฐ™์€ ์š”์ฒญ ์ˆ˜์ธ๋ฐ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ค๋ฅด๊ฒŒ ๋‚˜์˜จ๋‹ค.

  • ๊ฐ™์€ 200๊ฐœ์˜ ์š”์ฒญ์ธ๋ฐ ์™œ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ค๋ฅผ๊นŒ?
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
@Test  
@DisplayName("๋‹จ์ผ ์„œ๋ฒ„ vs ๋‹ค์ค‘ ์„œ๋ฒ„: ๊ฐ™์€ ์š”์ฒญ ์ˆ˜์ธ๋ฐ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ค๋ฅด๋‹ค")  
void single_vs_multi_server_comparison() {  
    int limit = 100;  
    int totalRequests = 200;  
  
    // ๋‹จ์ผ ์„œ๋ฒ„  
    FixedWindowRateLimiterV2 singleServer = new FixedWindowRateLimiterV2();  
    int singleAllowed = 0;  
    for (int i = 0; i < totalRequests; i++) {  
        RateLimitResult result = singleServer.tryAcquire("user:single", limit, 60);  
        if (result.allowed()) {  
            singleAllowed++;  
        }  
    }  
  
    // ๋‹ค์ค‘ ์„œ๋ฒ„ (3๋Œ€)  
    FixedWindowRateLimiterV2[] multiServers = {  
            new FixedWindowRateLimiterV2(),  
            new FixedWindowRateLimiterV2(),  
            new FixedWindowRateLimiterV2()  
    };  
    int multiAllowed = 0;  
    for (int i = 0; i < totalRequests; i++) {  
        if (multiServers[i % 3].tryAcquire("user:123", limit, 60).allowed()) {  
            multiAllowed++;  
        }  
    }  
  
    System.out.println("๋‹จ์ผ ์„œ๋ฒ„ ํ—ˆ์šฉ: " + singleAllowed);  
    System.out.println("๋‹ค์ค‘ ์„œ๋ฒ„ ํ—ˆ์šฉ: " + multiAllowed);  
  
    assertThat(singleAllowed).isEqualTo(limit);  
    assertThat(multiAllowed).isGreaterThan(limit);  
}

seukeulinsyas-2026-03-21-ohu-10-24-22.png

  • ์œ„์˜ 3๊ฐœ์˜ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๋ฌธ์ œ์ƒํ™ฉ์„ ๊ฒ€์ฆ ํ•ด๋ดค๋‹ค.

4์ฐจ ์‹œ๋‚˜๋ฆฌ์˜ค : Redis ๊ธฐ๋ฐ˜ Fixed Window

  • ๊ฒฐ๊ตญ ์„œ๋กœ ๋‹ค๋ฅธ ๋ฉ”๋ชจ๋ฆฌ ์˜์—ญ์— count๋ฅผ ์ €์žฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ƒ๊ธฐ๋Š” ๋ฌธ์ œ์ด๋‹ค.
  • ํ•ด๊ฒฐ์ฑ…์€ ๊ฐ„๋‹จํ•œ๋ฐ ๊ณตํ†ต๋œ ์˜์—ญ์—์„œ count๋ฅผ ๊ด€๋ฆฌ(๊ณต์œ  ์ €์žฅ์†Œ)ํ•˜๋ฉด ๋ ๊ฒƒ์ด๋‹ค.
  • ์ด ๊ณต์œ  ์ €์žฅ์†Œ์—์„œ Redis๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋‹ค.
  • seukeulinsyas-2026-03-21-ohu-11-10-39.png

  • โ€œ์™œ?? Redis๋ฅผ ์‚ฌ์šฉํ•˜์ฃ  ๊ณต์œ  ์ €์žฅ์†Œ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฑฐ๋ฉด ๊ทธ๋ƒฅ DB ์‚ฌ์šฉํ•ด๋„ ๋˜๋Š”๊ฑฐ ์•„๋‹Œ๊ฐ€์—ฌ?โ€
    • ๊ทธ๋ ‡๋‹ค ๊ทธ๋ƒฅ ๊ณต์œ  ์ €์žฅ์†Œ ์—ญํ• ์„ ํ• ๊ฑฐ๋ฉด ๊ทธ๋ƒฅ DB๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๋ ๊ฒƒ์ด๋‹ค.
    • ํ•˜์ง€๋งŒ ์ง€๊ธˆ ์ด ๋„๋ฉ”์ธ ํŠน์„ฑ์ƒ rate limiter ์ฆ‰ ์ œํ•œ ์‹œ๊ฐ„๋™์•ˆ ์ œํ•œ๋œ ํ—ˆ์šฉ ๊ฐ’ ๋ณด๋‹ค ์ดˆ๊ณผ๋œ ๊ฒฝ์šฐ๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•ด ํœ˜๋ฐœ์„ฑ์ธ ํŠน์ง•์ด ์žˆ๋Š” count๋ฅผ ๊ด€๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.
    • ์ถ”๊ฐ€๋กœ ๋งŒ์•ฝ Mysql ๊ฐ™์€ DB๋ฅผ ์‚ฌ์šฉ์„ ํ•œ๋‹ค๋ฉด ์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ๊ฐ€ ๋นˆ๋ฒˆํ•œ ์ƒํ™ฉ์—์„œ๋Š” ์ข‹์€ ์„ ํƒ์ง€๊ฐ€ ์•„๋‹ˆ๋‹ค. Redis๋Š” ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜์œผ๋กœ ๋งค์šฐ ๋น ๋ฅธ ์†๋„๊ฐ€ ํ•„์š”ํ•œ ์ง€๊ธˆ ์ƒํ™ฉ์— ์ ํ•ฉํ•˜๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
    • ์ถ”๊ฐ€๋กœ Redis๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์›์ž์ ์ธ ์—ฐ์‚ฐ์ธ INCR์„ ์ œ๊ณตํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋™์‹œ์„ฑ ์ œ์–ด๋„ ํŽธ์•ˆํ•˜๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public RateLimitResult tryAcquire(String key, int limit, int windowSeconds) {  
    String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));  
    String redisKey = "rate_limit:" + key + ":" + now;  
  
    Long count = redisTemplate.opsForValue().increment(redisKey);  
  
    if (count != null && count == 1) {  
        redisTemplate.expire(redisKey, Duration.ofSeconds(windowSeconds));  
    }  
  
    long resetAt = LocalDateTime.now()  
            .withSecond(0).withNano(0)  
            .plusMinutes(1)  
            .toEpochSecond(ZoneOffset.UTC);  
  
    if (count != null && count <= limit) {  
        return RateLimitResult.allowed(limit, (int) (limit - count), resetAt);  
    } else {  
        return RateLimitResult.blocked(limit, resetAt);  
    }  
}
  • ์ด์ „ ์‹œ๋‚˜๋ฆฌ์˜ค์™€ ์œ ์‚ฌํ•˜์ง€๋งŒ redis๋ผ๋Š” ๊ณต์œ  ์ €์žฅ์†Œ๋ฅผ ์ ์šฉํ•œ ์ฐจ์ด๊ฐ€ ์žˆ๋‹ค.
  • Long count = redisTemplate.opsForValue().increment(redisKey);
    • ์›์ž์ ์œผ๋กœ count๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ count๊ฐ’ ์ฆ๊ฐ€ ์‹œํ‚ค๊ธฐ
  • redisTemplate.expire(redisKey, Duration.ofSeconds(windowSeconds));
    • ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์ดˆ๊ณผ ๋˜๋ฉด redis ํ‚ค ๋งŒ๋ฃŒ ์‹œํ‚ค๊ธฐ

1. Redis ๊ธฐ๋ฐ˜ : ์„œ๋ฒ„ 3๋Œ€์—์„œ ์š”์ฒญํ•ด๋„ limit๋ฅผ ์ง€ํ‚จ๋‹ค

  • 3์ฐจ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ํ…Œ์ŠคํŠธ ํ•œ ๊ฒฐ๊ณผ์™€ ๋‹ฌ๋ฆฌ ์„œ๋ฒ„๊ฐ€ ๋‹ฌ๋ผ๋„ ๊ณต์œ  ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ˆœ์ฐจ ๋ถ„๋ฐฐ ํ–ˆ์„ ๋•Œ ์ •ํ™•ํžˆ limit๋ฅผ ์ง€ํ‚ค๋Š” ๋ชจ์Šต seukeulinsyas-2026-03-21-ohu-11-43-22.png

2. Redis ๊ธฐ๋ฐ˜ : ๋™์‹œ ์š”์ฒญ + ๋‹ค์ค‘ ์„œ๋ฒ„์—์„œ๋„ limit ์ง€ํ‚จ๋‹ค

  • ์—ญ์‹œ ์ด ๋ถ€๋ถ„๋„ 3์ฐจ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ ํ•œ ๊ฒฐ๊ณผ์™€ ๋‹ค๋ฅด๊ฒŒ ๋™์‹œ ์š”์ฒญ์—๋„ limit๋ฅผ ์ •ํ™•ํžˆ ์ง€ํ‚ค๋Š” ๋ชจ์Šต seukeulinsyas-2026-03-21-ohu-11-46-08.png
  • ์ง€๊ธˆ ๊นŒ์ง€ Fixed Window ๊ธฐ๋ฐ˜์œผ๋กœ rate๋ฅผ ์ œํ•œ ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ Fixed Window๋Š” ๋ช…ํ™•ํ•œ ๋‹จ์ ์ด ์žˆ๋Š”๋ฐ ๊ฒฝ๊ณ„๊ฐ’์—์„œ ๋น„์ •์ƒ์ ์œผ๋กœ ๋งŽ์€ ์š”์ฒญ์„ ํ—ˆ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค.

5์ฐจ ์‹œ๋‚˜๋ฆฌ์˜ค: Fixed Window ๊ฒฝ๊ณ„๊ฐ’ ๋ฌธ์ œ ์žฌํ˜„

  • Fixed Window์˜ ๊ฒฝ๊ณ„๊ฐ’ ํ•œ๊ณ„ :
    • ์ง€๊ธˆ ํ˜„์žฌ fixed window๋Š” ๊ณ ์ •๋œ ์‹œ๊ฐ„์„ ๊ธฐ์ค€์œผ๋กœ window๋ฅผ ๋‚˜๋ˆ„๊ณ  ์žˆ์–ด์„œ key๋ฅผ ๋งŒ๋“ค๋•Œ rate_limiter:user:2025:03:22:01:21 ์ด๋Ÿฐ์‹์œผ๋กœ ๋ถ„ ๋‹จ์œ„๋กœ ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ๋งŒ๋“ค๊ธฐ ๋•Œ๋ฌธ์— ์œˆ๋„์šฐ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์นด์šดํ„ฐ๊ฐ€ 0์—์„œ ๋‹ค์‹œ ์‹œ์ž‘์„ ํ•œ๋‹ค. ์ด์ „ ์œˆ๋„์šฐ์—์„œ ์–ผ๋งˆ๋‚˜ ์ผ๋Š”์ง€๋ฅผ ์ „ํ˜€ ๊ณ ๋ คํ•˜์ง€ ์•Š๋Š”๋‹ค. seukeulinsyas-2026-03-22-ojeon-12-16-29.png
  • ๊ทธ๋ฆผ ์ฒ˜๋Ÿผ ํŒŒ๋ž€ ๋„ค๋ชจ๊ฐ€ ํ•œ ์œˆ๋„์šฐ ํฌ๊ธฐ์ธ๋ฐ ์ด ํ•œ ์œˆ๋„์šฐ ๋งˆ๋‹ค 100ํšŒ ์ œํ•œ์„ ๊ฑธ์–ด๋’€์ง€๋งŒ ์ด ๊ฒฝ๊ณ„๊ฐ’์— ์š”์ฒญ์ด ๋ชฐ๋ฆฌ๋ฉด 12:00:59์ดˆ์— 100ํšŒ ์š”์ฒญํ•˜๋ฉด (12:00~12:00:59)์œˆ๋„์šฐ์˜ limit ์ด๋‚ด๋‹ˆ๊นŒ ์ „๋ถ€ ํ—ˆ์šฉ์„ ํ•˜๊ณ  12:01:00์— 100ํšŒ ์š”์ฒญํ•˜๋ฉด (12:01~12:01:59)์œˆ๋„์šฐ์˜ limit ์ด๋‚ด๋‹ˆ๊นŒ ์ „๋ถ€ ํ—ˆ์šฉ์„ ํ•œ๋‹ค ์ฆ‰ 1~2์ดˆ ์‚ฌ์ด์— 200ํšŒ๊ฐ€ ํ†ต๊ณผ ๋ผ๋ฒ„๋ฆฌ๋Š” ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹ค
  • 1๋ถ„์˜ 100ํšŒ ํ—ˆ์šฉ -> 1์ดˆ์˜ 200ํšŒ ํ—ˆ์šฉ ์ด ๋˜์–ด๋ฒ„๋ฆฌ๋Š” ์น˜๋ช…์ ์ธ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด๋‹ค

๊ฒฝ๊ณ„๊ฐ’ ๋ฌธ์ œ ์žฌํ˜„

  • ์œˆ๋„์šฐ ๊ฒฝ๊ณ„ 1์ดˆ ์‚ฌ์ด์— limit์˜ 2๋ฐฐ๊ฐ€ ํ—ˆ์šฉ๋œ๋‹ค
    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
    @Test  
    @DisplayName("์œˆ๋„์šฐ ๊ฒฝ๊ณ„ 1์ดˆ ์‚ฌ์ด์— limit์˜ 2๋ฐฐ๊ฐ€ ํ—ˆ์šฉ๋œ๋‹ค")  
    void boundary_burst_with_clock() {  
      int limit = 100;  
      
      // 12:30:59 ์‹œ์ ์˜ Clock (์œˆ๋„์šฐ 1 ๋)  
      Clock clock1 = Clock.fixed(  
              LocalDateTime.of(2026, 3, 22, 12, 30, 59).toInstant(ZoneOffset.UTC),  
              ZoneOffset.UTC  
      );  
      TestableRedisFixedWindowRateLimiter limiter1 = new TestableRedisFixedWindowRateLimiter(redisTemplate, clock1);  
      
      // 12:31:00 ์‹œ์ ์˜ Clock (์œˆ๋„์šฐ 2 ์‹œ์ž‘, 1์ดˆ ํ›„)  
      Clock clock2 = Clock.fixed(  
              LocalDateTime.of(2026, 3, 22, 12, 31, 0).toInstant(ZoneOffset.UTC),  
              ZoneOffset.UTC  
      );  
      TestableRedisFixedWindowRateLimiter limiter2 = new TestableRedisFixedWindowRateLimiter(redisTemplate, clock2);  
      
      // ์œˆ๋„์šฐ 1์—์„œ 100ํšŒ ์š”์ฒญ  
      int allowed1 = 0;  
      for (int i = 0; i < limit; i++) {  
          if (limiter1.tryAcquire("user:burst", limit, 60).allowed()) {  
              allowed1++;  
          }  
      }  
      
      // ์œˆ๋„์šฐ 2์—์„œ 100ํšŒ ์š”์ฒญ (1์ดˆ ํ›„)  
      int allowed2 = 0;  
      for (int i = 0; i < limit; i++) {  
          if (limiter2.tryAcquire("user:burst", limit, 60).allowed()) {  
              allowed2++;  
          }  
      }  
      
      int totalAllowed = allowed1 + allowed2;  
      
      System.out.println("์œˆ๋„์šฐ 1 ํ—ˆ์šฉ: " + allowed1);  
      System.out.println("์œˆ๋„์šฐ 2 ํ—ˆ์šฉ: " + allowed2);  
      System.out.println("1์ดˆ ์‚ฌ์ด ์ด ํ—ˆ์šฉ: " + totalAllowed + " (limit: " + limit + ")");  
      
      assertThat(allowed1).isEqualTo(limit);  
      assertThat(allowed2).isEqualTo(limit);  
      assertThat(totalAllowed)  
              .as("๊ฒฝ๊ณ„์—์„œ limit์˜ 2๋ฐฐ๊ฐ€ ํ—ˆ์šฉ๋œ๋‹ค")  
              .isEqualTo(limit * 2);  
    }
    
  • Clock์„ ์‚ฌ์šฉํ•ด์„œ 12:30:59์™€ 12:31:00 1์ดˆ ์ฐจ์ด๊ฐ€ ๋˜๋„๋ก ๊ณ ์ •ํ•œ ํ›„ ๊ฐ๊ฐ 100ํšŒ์”ฉ ์š”์ฒญ์„ ํ–ˆ๋Š”๋ฐ -> ๊ฒฐ๊ตญ ์•„๋ž˜ ์‚ฌ์ง„์ฒ˜๋Ÿผ 1์ดˆ ์‚ฌ์ด์— ์ด 200ํšŒ๊ฐ€ ํ—ˆ์šฉ์ด ๋˜๋Š” ๋ชจ์Šต์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

seukeulinsyas-2026-03-22-ojeon-12-23-22.png


๋‹ค์Œ ํŽธ

  • 5์ฐจ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด ์—ฌ๋Ÿฌ ์ „๋žต์ค‘ Sliding Window Counter๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฒฝ๊ณ„๊ฐ’ burst ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.