3์ฐจ ์๋๋ฆฌ์ค : ๋ค์ค ์๋ฒ ํ๊ฒฝ์์์ ํ๊ณ ๋ถ์
- ์ด์ ๊ธ์์ ๋งํ ๊ฒ ์ฒ๋ผ ์๋ฒ๊ฐ ๋ง์ฝ ์ฌ๋ฌ ๋๋ผ๋ฉด? ์ด๋ผ๋ ์๋๋ฆฌ์ค๋ฅผ ํตํด ๋ค์ค ์๋ฒ ํ๊ฒฝ์์์ ํ๊ณ ๋ถ์์ ํด๋ณด๊ฒ ๋ค
๋ฌธ์ ์ํฉ
- ํ์ฌ ์ํฉ์ ๋จ์ผ ์๋ฒ ํ๊ฒฝ์ธ๋ฐ ๋ง์ฝ ์๋ฒ๊ฐ ์ฌ๋ฌ๋์ธ ํ๊ฒฝ์ด๋ผ๋ฉด?
1 | |
- ๊ธฐ์กด count ๊ฐ์ ์๋ฒ์ ConcurrentHashMap(JVM ๋ฉ๋ชจ๋ฆฌ)์ ์ ์ฅ์ ํ๋๋ก ๊ตฌํ์ด ๋์ด์๋ค๋ฉด ์๋์ ๊ฐ์ ๊ตฌ์ฑ์์๋ ๊ฐ๊ฐ์ JVM ์ธ์คํด์ค๋ง๋ค ์นด์ดํธ ๊ฐ์ด ๋ ๋ฆฝ์ ์ด๋๋ค.
- ๊ฐ์ ์ฌ์ฉ์ user:123์ด limit = 100์ธ๋ฐ, ๋ก๋๋ฐธ๋ฐ์๊ฐ ์์ฒญ์ ๋ถ์ฐํ๋ฉด ๊ฐ๊ฐ์ ์ธ์คํด์ค์ 40์ฉ ๋ถ์ฐ์ ํ๋ค๊ณ ํด๋ ๊ฐ ์ธ์คํด์ค ๊ธฐ์ค์ผ๋ก๋ limit๋ฅผ ์ด๊ณผ๋ฅผ ํ์ง ์์์ง๋ง ์ ์ฒด ๊ตฌ์กฐ์์๋ ์ด๋ฏธ 120์ผ๋ก limit๋ฅผ ์ด๊ณผ๋ฅผ ํ๋ค => ๊ฐ ์๋ฒ๋ ์๊ธฐ ์นด์ดํฐ๋ง ๋ณด๊ธฐ ๋๋ฌธ์ ์ ์ฒด ์ ํ์ด ๊นจ์ง๋ ๋ฌธ์ ๊ฐ ์๋ค.
1 | |
- ์ฆ ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ rate limiter๋ ๋จ์ผ ์๋ฒ์์๋ง ์ ํจํ๋ค.
๋ค์ค ์๋ฒ ๋ฌธ์ ์ํฉ ์ฌํ
- ๊ทธ๋ผ ์ง์ ํ ์คํธ๋ฅผ ์ํด ๋ค์ค ์๋ฒ ํ๊ฒฝ์ผ ๋ ์ด๋ค ์ผ์ด ์ผ์ด๋๋์ง ํ ์คํธ ์ฝ๋๋ก ํ์ธํด๋ดค๋ค.
1. ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ์ ์ ์ฒด limit์ ๋ณด์ฅํ์ง ๋ชปํ๋ค.
- ์๋ฒ๊ฐ ๋ง์ฝ 3๋๋ฉด limit์ ์ง์ผ์ง๋๊ฐ?
- ์์ฐจ ๋ถ๋ฐฐ๋ฅผ ํด๋ดค๋ค.
1 | |
- ๋ก๋ ๋ฐธ๋ฐ์๋ฅผ ๊ฐ์ ํด์ 3๋์ ์๋ฒ์ 200๊ฐ์ ์์ฒญ์ ๋ถ๋ฐฐ๋ฅผ ํ๋ค.
- ๋น์ฐํ ์์๋๋ก ๊ฐ๊ฐ ๋ ๋ฆฝ๋ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๊ฐ์ง๊ธฐ ๋๋ฌธ์ limit 100์ ์ด๊ณผ ํ๋ 200๊ฐ์ ์์ฒญ์ด ๋ค์ด์๋ค.
2. ๋์ ์์ฒญ + ๋ค์ค ์๋ฒ : ๋์์ฑ๊น์ง ๊ฒน์น๋ฉด ๋ ์ฌํ๊ฒ ์ด๊ณผํ๋ค.
- ๋์ ์์ฒญ์ด ๋ค์ค ์๋ฒ์ ๋ถ์ฐ๋๋ฉด ์ด๋ป๊ฒ ๋๋๊ฐ?
1 | |
- ์ฌ๊ธฐ๋ ์ญ์ ์์๋๋ก ๋์์ 300 ์์ฒญ์ ๋ถ๋ฐฐ๋ฅผ ํด๋ limit๋ฅผ ์ด๊ณผํ๋ ๊ฒฐ๊ณผ๊ฐ ๋์๋ค.
3. ๋จ์ผ ์๋ฒ vs ๋ค์ค ์๋ฒ: ๊ฐ์ ์์ฒญ ์์ธ๋ฐ ๊ฒฐ๊ณผ๊ฐ ๋ค๋ฅด๊ฒ ๋์จ๋ค.
- ๊ฐ์ 200๊ฐ์ ์์ฒญ์ธ๋ฐ ์ ๊ฒฐ๊ณผ๊ฐ ๋ค๋ฅผ๊น?
1 | |
- ์์ 3๊ฐ์ ํ ์คํธ๋ฅผ ํตํด ๋ฌธ์ ์ํฉ์ ๊ฒ์ฆ ํด๋ดค๋ค.
4์ฐจ ์๋๋ฆฌ์ค : Redis ๊ธฐ๋ฐ Fixed Window
- ๊ฒฐ๊ตญ ์๋ก ๋ค๋ฅธ ๋ฉ๋ชจ๋ฆฌ ์์ญ์ count๋ฅผ ์ ์ฅํ๊ธฐ ๋๋ฌธ์ ์๊ธฐ๋ ๋ฌธ์ ์ด๋ค.
- ํด๊ฒฐ์ฑ ์ ๊ฐ๋จํ๋ฐ ๊ณตํต๋ ์์ญ์์ count๋ฅผ ๊ด๋ฆฌ(๊ณต์ ์ ์ฅ์)ํ๋ฉด ๋ ๊ฒ์ด๋ค.
- ์ด ๊ณต์ ์ ์ฅ์์์ Redis๋ฅผ ์ฌ์ฉํ ๊ฒ์ด๋ค.
-

- โ์?? Redis๋ฅผ ์ฌ์ฉํ์ฃ ๊ณต์ ์ ์ฅ์๋ฅผ ๋ง๋๋ ๊ฑฐ๋ฉด ๊ทธ๋ฅ DB ์ฌ์ฉํด๋ ๋๋๊ฑฐ ์๋๊ฐ์ฌ?โ
- ๊ทธ๋ ๋ค ๊ทธ๋ฅ ๊ณต์ ์ ์ฅ์ ์ญํ ์ ํ ๊ฑฐ๋ฉด ๊ทธ๋ฅ DB๋ฅผ ์ฌ์ฉํด๋ ๋ ๊ฒ์ด๋ค.
- ํ์ง๋ง ์ง๊ธ ์ด ๋๋ฉ์ธ ํน์ฑ์ rate limiter ์ฆ ์ ํ ์๊ฐ๋์ ์ ํ๋ ํ์ฉ ๊ฐ ๋ณด๋ค ์ด๊ณผ๋ ๊ฒฝ์ฐ๋ฅผ ๋ง๊ธฐ ์ํด ํ๋ฐ์ฑ์ธ ํน์ง์ด ์๋ count๋ฅผ ๊ด๋ฆฌํด์ผ ํ๋ค.
- ์ถ๊ฐ๋ก ๋ง์ฝ Mysql ๊ฐ์ DB๋ฅผ ์ฌ์ฉ์ ํ๋ค๋ฉด ์ฝ๊ธฐ์ ์ฐ๊ธฐ๊ฐ ๋น๋ฒํ ์ํฉ์์๋ ์ข์ ์ ํ์ง๊ฐ ์๋๋ค. Redis๋ ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ์ผ๋ก ๋งค์ฐ ๋น ๋ฅธ ์๋๊ฐ ํ์ํ ์ง๊ธ ์ํฉ์ ์ ํฉํ๋ค๊ณ ๋ณผ ์ ์๋ค.
- ์ถ๊ฐ๋ก Redis๋ฅผ ์ฌ์ฉํ๋ฉด ์์์ ์ธ ์ฐ์ฐ์ธ INCR์ ์ ๊ณตํ๊ธฐ ๋๋ฌธ์ ๋์์ฑ ์ ์ด๋ ํธ์ํ๋ค๋ ์ฅ์ ์ด ์๋ค.
1 | |
- ์ด์ ์๋๋ฆฌ์ค์ ์ ์ฌํ์ง๋ง redis๋ผ๋ ๊ณต์ ์ ์ฅ์๋ฅผ ์ ์ฉํ ์ฐจ์ด๊ฐ ์๋ค.
Long count = redisTemplate.opsForValue().increment(redisKey);- ์์์ ์ผ๋ก count๊ฐ ๊ฐ์ ธ์ค๊ธฐ count๊ฐ ์ฆ๊ฐ ์ํค๊ธฐ
redisTemplate.expire(redisKey, Duration.ofSeconds(windowSeconds));- ๋ง๋ฃ ์๊ฐ ์ด๊ณผ ๋๋ฉด redis ํค ๋ง๋ฃ ์ํค๊ธฐ
1. Redis ๊ธฐ๋ฐ : ์๋ฒ 3๋์์ ์์ฒญํด๋ limit๋ฅผ ์งํจ๋ค
- 3์ฐจ ์๋๋ฆฌ์ค์์ ํ
์คํธ ํ ๊ฒฐ๊ณผ์ ๋ฌ๋ฆฌ ์๋ฒ๊ฐ ๋ฌ๋ผ๋ ๊ณต์ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์์ฐจ ๋ถ๋ฐฐ ํ์ ๋ ์ ํํ limit๋ฅผ ์งํค๋ ๋ชจ์ต

2. Redis ๊ธฐ๋ฐ : ๋์ ์์ฒญ + ๋ค์ค ์๋ฒ์์๋ limit ์งํจ๋ค
- ์ญ์ ์ด ๋ถ๋ถ๋ 3์ฐจ ์๋๋ฆฌ์ค ํ
์คํธ ํ ๊ฒฐ๊ณผ์ ๋ค๋ฅด๊ฒ ๋์ ์์ฒญ์๋ limit๋ฅผ ์ ํํ ์งํค๋ ๋ชจ์ต

- ์ง๊ธ ๊น์ง Fixed Window ๊ธฐ๋ฐ์ผ๋ก rate๋ฅผ ์ ํ ํ๊ณ ์์๋ค.
- ํ์ง๋ง Fixed Window๋ ๋ช ํํ ๋จ์ ์ด ์๋๋ฐ ๊ฒฝ๊ณ๊ฐ์์ ๋น์ ์์ ์ผ๋ก ๋ง์ ์์ฒญ์ ํ์ฉํ ์ ์๋ ๋ฌธ์ ๊ฐ ์๋ค.
5์ฐจ ์๋๋ฆฌ์ค: Fixed Window ๊ฒฝ๊ณ๊ฐ ๋ฌธ์ ์ฌํ
- Fixed Window์ ๊ฒฝ๊ณ๊ฐ ํ๊ณ :
- ์ง๊ธ ํ์ฌ fixed window๋ ๊ณ ์ ๋ ์๊ฐ์ ๊ธฐ์ค์ผ๋ก window๋ฅผ ๋๋๊ณ ์์ด์ key๋ฅผ ๋ง๋ค๋ rate_limiter:user:2025:03:22:01:21 ์ด๋ฐ์์ผ๋ก ๋ถ ๋จ์๋ก ํ์์คํฌํ๋ฅผ ๋ง๋ค๊ธฐ ๋๋ฌธ์ ์๋์ฐ๊ฐ ๋ฐ๋๋ฉด ์นด์ดํฐ๊ฐ 0์์ ๋ค์ ์์์ ํ๋ค. ์ด์ ์๋์ฐ์์ ์ผ๋ง๋ ์ผ๋์ง๋ฅผ ์ ํ ๊ณ ๋ คํ์ง ์๋๋ค.

- ์ง๊ธ ํ์ฌ fixed window๋ ๊ณ ์ ๋ ์๊ฐ์ ๊ธฐ์ค์ผ๋ก window๋ฅผ ๋๋๊ณ ์์ด์ key๋ฅผ ๋ง๋ค๋ rate_limiter:user:2025:03:22:01:21 ์ด๋ฐ์์ผ๋ก ๋ถ ๋จ์๋ก ํ์์คํฌํ๋ฅผ ๋ง๋ค๊ธฐ ๋๋ฌธ์ ์๋์ฐ๊ฐ ๋ฐ๋๋ฉด ์นด์ดํฐ๊ฐ 0์์ ๋ค์ ์์์ ํ๋ค. ์ด์ ์๋์ฐ์์ ์ผ๋ง๋ ์ผ๋์ง๋ฅผ ์ ํ ๊ณ ๋ คํ์ง ์๋๋ค.
- ๊ทธ๋ฆผ ์ฒ๋ผ ํ๋ ๋ค๋ชจ๊ฐ ํ ์๋์ฐ ํฌ๊ธฐ์ธ๋ฐ ์ด ํ ์๋์ฐ ๋ง๋ค 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ํ๊ฐ ํ์ฉ์ด ๋๋ ๋ชจ์ต์ ๋ณผ ์ ์๋ค.
๋ค์ ํธ
- 5์ฐจ ์๋๋ฆฌ์ค๋ฅผ ๊ฐ์ ํ๊ธฐ ์ํด ์ฌ๋ฌ ์ ๋ต์ค Sliding Window Counter๋ฅผ ์ฌ์ฉํด์ ๊ฒฝ๊ณ๊ฐ burst ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด๋ณด๋ ค๊ณ ํ๋ค.




