๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
BackEnd๐ŸŒฑ/Spring

Bucket4j๋กœ ํŠธ๋ž˜ํ”ฝ ์ œํ•œํ•˜๊ธฐ(Redis & MariaDB)

by dkswnkk 2023. 12. 3.

๊ฐœ์š”

์ตœ๊ทผ ์—…๋ฌด ํ”„๋กœ์ ํŠธ์—์„œ ํŠน์ •(์š”๊ธˆ์ด ๋ถ€๊ฐ€๋˜๋Š”) ๋กœ์ง์— ๋Œ€ํ•ด ์›”๋ณ„ ์‚ฌ์šฉ๋Ÿ‰์„ ์ œํ•œํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์™€ ๊ด€๋ จํ•˜์—ฌ ์ฒ˜๋ฆฌ์œจ ์ œํ•œ ๊ธฐ์ˆ ์„ ์•Œ์•„๋ณด์•˜๋Š”๋ฐ Bucket4j, Guava, RateLimitj, Resilience4j ๋“ฑ ๋‹ค์–‘ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ๋Š” ๊ฑธ ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ , ํ”„๋กœ์ ํŠธ ํ™˜๊ฒฝ์ธ Spring Boot 2.7.x, MariaDB(nosql & in-momory๋ถ€์žฌ), Java 11์— ์•Œ๋งž์€ Bucket4j๋ฅผ ์„ ํƒํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ๊ณ ๋ คํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์˜ ํŠน์ง•๊ณผ ๊ทธ ์„ ํƒ ์ด์œ ์ž…๋‹ˆ๋‹ค.

  • Guava: ๋‹ค์–‘ํ•œ ํ•ต์‹ฌ Java ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ œ๊ณตํ•˜์ง€๋งŒ, ๋‹จ์ˆœํžˆ Rate Limiting ๊ธฐ๋Šฅ๋งŒ์„ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๊ธฐ์—๋Š” ๋‹ค์†Œ ๋ฌด๊ฑฐ์šด ๋Š๋‚Œ์ด๋‹ค.
  • Resilience4j: ์„œํ‚ท ๋ธŒ๋ ˆ์ด์ปค๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ Rate Limiter ๊ธฐ๋Šฅ๋„ ํฌํ•จ๋˜์–ด ์žˆ์ง€๋งŒ, ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋‹จ์ˆœํžˆ Rate Limiting ๊ธฐ๋Šฅ๋งŒ์„ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๊ธฐ์—๋Š” ๋‹ค์†Œ ๋ฌด๊ฑฐ์šด ๋Š๋‚Œ์ด๋‹ค.
  • RateLimitj: ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋Š” Rate Limiter ๊ตฌํ˜„์ฒด์ด๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ณต์‹ ๋ฌธ์„œ์— ๋” ์ด์ƒ ์—…๋ฐ์ดํŠธ๊ฐ€ ์—†์„ ๊ฒƒ์ด๋ผ๊ณ  ๋ช…์‹œ๋˜์–ด ์žˆ๊ณ , Bucket4j๋ฅผ ๋Œ€์ฒด์ œ๋กœ ๊ถŒ์žฅํ•˜๊ณ  ์žˆ๋‹ค.
  • Bucket4j: ํ† ํฐ ๋ฒ„ํ‚ท ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋ฉฐ, lock-freeํ•œ ๊ตฌํ˜„์œผ๋กœ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋”ฉ ํ™˜๊ฒฝ์—์„œ์˜ ํ™•์žฅ์„ฑ์ด ์šฐ์ˆ˜ํ•˜๋ฉฐ, Rate Limiting๋งŒ์„ ๋ชฉ์ ์œผ๋กœ ์ œ๊ณต๋˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.

 ๋”ฐ๋ผ์„œ ์ด ๊ธ€์—์„œ๋Š” Bucket4j๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŠธ๋ž˜ํ”ฝ ์ œํ•œ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. 

 

 

ํ† ํฐ ๋ฒ„ํ‚ท ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ด๋ž€?

ํ† ํฐ ๋ฒ„ํ‚ท ์•Œ๊ณ ๋ฆฌ์ฆ˜(Token Bucket Algorithm)์€ ๋„คํŠธ์›Œํฌ ํŠธ๋ž˜ํ”ฝ ๋ฐ ๋ฐ์ดํ„ฐ ์ „์†ก ์†๋„๋ฅผ ์ œ์–ดํ•˜๋Š” ๋ฐ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ๊ธฐ๋ฒ•์ž…๋‹ˆ๋‹ค. ๊ณ ์ •๋œ ์†๋„๋กœ ํ† ํฐ์ด ๋ฒ„ํ‚ท์— ์ฑ„์›Œ์ง€๋ฉฐ, ๊ฐ ์š”์ฒญ์€ ๋ฒ„ํ‚ค์—์„œ ํ•˜๋‚˜ ์ด์ƒ์˜ ํ† ํฐ์„ ์†Œ๋น„ํ•ฉ๋‹ˆ๋‹ค. ๋ฒ„ํ‚ท์— ์ถฉ๋ถ„ํ•œ ํ† ํฐ์ด ์—†๋Š” ๊ฒฝ์šฐ, ์š”์ฒญ์€ ๋Œ€๊ธฐํ•˜๊ฑฐ๋‚˜ ๊ฑฐ๋ถ€๋˜๋ฉฐ ์ด ๋ฐฉ์‹์€ ํŠธ๋ž˜ํ”ฝ์„ ์œ ์—ฐํ•˜๊ฒŒ ์ œ์–ดํ•˜๊ณ  ํ”ผํฌ ์‹œ๊ฐ„์— ๋Œ€ํ•œ ๋ถ€ํ•˜๋ฅผ ๊ด€๋ฆฌํ•˜๋Š”๋ฐ ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค. ์ฃผ์š” ํŠน์ง•๊ณผ ๋™์ž‘ ์›๋ฆฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • ํ† ํฐ์˜ ์ƒ์„ฑ๊ณผ ์†Œ๋น„: ๋ฒ„ํ‚ท์—๋Š” ์ผ์ •ํ•œ ์†๋„๋กœ ํ† ํฐ์ด ์ถ”๊ฐ€๋œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, 1๋ถ„์— 100๊ฐœ์˜ ํ† ํฐ์ด ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์š”์ฒญ์ด ๋“ค์–ด์˜ฌ ๋•Œ๋งˆ๋‹ค ๋ฒ„ํ‚ท์—์„œ ํ† ํฐ์„ ํ•˜๋‚˜ ๋˜๋Š” ๊ทธ ์ด์ƒ ์†Œ๋น„ํ•œ๋‹ค.
  • ์š”์ฒญ ์ œํ•œ: ๋ฒ„ํ‚ท์— ํ† ํฐ์ด ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์œผ๋ฉด ์ƒˆ๋กœ์šด ์š”์ฒญ์€ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š๊ณ  ๋ฒ„๋ ค์ง„๋‹ค.
  • ํ† ํฐ์˜ ์žฌ์ถฉ์ „: ์ผ์ • ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ํ† ํฐ์ด ๋‹ค์‹œ ์ƒ์„ฑ๋˜์–ด ๋ฒ„ํ‚ท์— ์ถ”๊ฐ€๋œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ดˆ๊ธฐ์— 120๊ฐœ์˜ ํ† ํฐ์œผ๋กœ ์‹œ์ž‘ํ•˜์—ฌ 1๋ถ„์— 100๊ฐœ์”ฉ ์žฌ์ถฉ์ „๋˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

 

 

Bucket4j ์ ์šฉ

Bucket4j๋Š” ๋กœ์ปฌ ๋ฉ”๋ชจ๋ฆฌ ์™ธ์—๋„, JDBC(MySQL, PostgreSQL, Oracle, MSSQL)์™€ Redis์™€ ๊ฐ™์ด ๋‹ค์–‘ํ•œ ๋ถ„์‚ฐ ํ™˜๊ฒฝ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜๋Š” MariaDB์™€ Redis์— Bucket4j๋ฅผ ์ ์šฉํ•œ ์˜ˆ์ œ ์ฝ”๋“œ๋“ค์ž…๋‹ˆ๋‹ค. ๋กœ์ปฌ ๋ฉ”๋ชจ๋ฆฌ(์บ์‹œ)๋ฅผ ์‚ฌ์šฉํ•œ ์ฝ”๋“œ์— ๋Œ€ํ•ด์„œ๋Š” ์ธํ„ฐ๋„ท์— ๋‹ค์–‘ํ•œ ์˜ˆ์ œ๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ, ํ•„์š”ํ•˜๋‹ค๋ฉด ํ•ด๋‹น ์ž๋ฃŒ๋“ค์„ ์ฐธ์กฐํ•˜์‹œ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ๋Š” ๋Œ€๊ฐœ ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋™์‹œ์— ์šด์˜๋˜๋ฏ€๋กœ, ์ด์ฒ˜๋Ÿผ ๋ถ„์‚ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— Bucket4j๋ฅผ ์ ์šฉํ•˜๋Š” ๊ฒƒ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

  • Bucket4j๋ฅผ MariaDB์— ์ ์šฉํ•œ ์˜ˆ์ œ: Bucket4j-mariadb
  • Bucket4j๋ฅผ Redis์— ์ ์šฉํ•œ ์˜ˆ์ œ: Bucket4j-redis

 

Bucket4j-mariadb

1. table ์ƒ์„ฑ

๋จผ์ € ์•„๋ž˜์™€ ๊ฐ™์ด 'bucket'์ด๋ผ๋Š” ์ด๋ฆ„์˜ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋Š” ํ…Œ์ด๋ธ” ์ด๋ฆ„์„ 'buckets'๋กœ ์ง€์ •ํ•˜์˜€์ง€๋งŒ, ์‹ค์ œ๋กœ BucketTableSetting.getDefault() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ํ…Œ์ด๋ธ” ์ด๋ฆ„์ด 'bucket'์œผ๋กœ ๋ฐ˜ํ™˜๋˜๋ฏ€๋กœ, ํ…Œ์ด๋ธ” ์ด๋ฆ„์„ 'bucket'์œผ๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ํŽธ๋ฆฌํ•ฉ๋‹ˆ๋‹ค. 

create table bucket
(
    id    bigint not null
        primary key,
    state blob   null
);

2. dependency

'bucket4j-mysql' ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

dependencies {
    ...    
    
    // bucket4j
    implementation 'com.bucket4j:bucket4j-mysql:8.7.0'
}

3. APIRateLimiter

@Slf4j
@Component
public class APIRateLimiter {
    // ๋ฒ„ํ‚ท์˜ ์šฉ๋Ÿ‰์„ ์„ค์ •, ์ด๋Š” ๋ฒ„ํ‚ท์— ๋‹ด๊ธธ ์ˆ˜ ์žˆ๋Š” ํ† ํฐ์˜ ์ตœ๋Œ€ ์ˆ˜๋ฅผ ์˜๋ฏธ
    private static final int CAPACITY = 3;
    // ํ† ํฐ์ด ์–ผ๋งˆ๋‚˜ ๋น ๋ฅด๊ฒŒ ์žฌ์ถฉ์ „๋ ์ง€ ์„ค์ •, ์ด๋Š” ์ง€์ •๋œ ์‹œ๊ฐ„ ๋™์•ˆ ๋ฒ„ํ‚ท์— ์ถ”๊ฐ€๋  ํ† ํฐ์˜ ์ˆ˜๋ฅผ ์˜๋ฏธ
    private static final int REFILL_AMOUNT = 3;
    // ํ† ํฐ์ด ์žฌ์ถฉ์ „๋˜๋Š” ๋นˆ๋„๋ฅผ ์„ค์ •
    private static final Duration REFILL_DURATION = Duration.ofSeconds(5);

    // MySQLSelectForUpdateBasedProxyManager ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑ, ์ด ๊ฐ์ฒด๋Š” ๋ฒ„ํ‚ท์˜ ์ƒ์„ฑ ๋ฐ ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹น
    private final MySQLSelectForUpdateBasedProxyManager<Long> proxyManager;
    // ๋™์ผํ•œ API ํ‚ค์— ๋Œ€ํ•œ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๋ฒ„ํ‚ท์„ ์žฌ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋ฒ„ํ‚ท์„ ์ €์žฅํ•˜๋Š” ๋งต์„ ์ƒ์„ฑ
    private final ConcurrentMap<String, BucketProxy> buckets = new ConcurrentHashMap<>();

    /**
     * APIRateLimiter ์ƒ์„ฑ์ž.
     * 
     * @param dataSource ๋ฐ์ดํ„ฐ ์†Œ์Šค
     */
    public APIRateLimiter(DataSource dataSource) {
        // ๋ฒ„ํ‚ท ํ…Œ์ด๋ธ” ์„ค์ •์„ ๊ฐ€์ ธ์˜ด
        var tableSettings = BucketTableSettings.getDefault();
        // SQL ํ”„๋ก์‹œ ์„ค์ •์„ ์ƒ์„ฑ
        var sqlProxyConfiguration = SQLProxyConfiguration.builder()
                .withTableSettings(tableSettings)
                .build(dataSource);
        // SQL ํ”„๋ก์‹œ ์„ค์ •์„ ์ด์šฉํ•ด MySQLSelectForUpdateBasedProxyManager ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑ
        proxyManager = new MySQLSelectForUpdateBasedProxyManager<>(sqlProxyConfiguration);
    }

    /**
     * API ํ‚ค์— ํ•ด๋‹นํ•˜๋Š” ๋ฒ„ํ‚ท์„ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜, ์—†์„ ๊ฒฝ์šฐ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ.
     * 
     * @param apiKey API ํ‚ค
     * @return ํ•ด๋‹น API ํ‚ค์— ๋Œ€์‘ํ•˜๋Š” ๋ฒ„ํ‚ท
     */
    private BucketProxy getOrCreateBucket(String apiKey) {
        return buckets.computeIfAbsent(apiKey, key -> {
            // API ํ‚ค์˜ ํ•ด์‹œ ์ฝ”๋“œ๋ฅผ ๋ฒ„ํ‚ท ID๋กœ ์‚ฌ์šฉ
            Long bucketId = (long) key.hashCode();
            // ๋ฒ„ํ‚ท ์„ค์ •์„ ์ƒ์„ฑ
            var bucketConfiguration = createBucketConfiguration();
            // ๋ฒ„ํ‚ท ID์™€ ๋ฒ„ํ‚ท ์„ค์ •์„ ์ด์šฉํ•ด ๋ฒ„ํ‚ท์„ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ ๋ฐ˜ํ™˜
            return proxyManager.builder().build(bucketId, bucketConfiguration);
        });
    }

    /**
     * ๋ฒ„ํ‚ท ์„ค์ •์„ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ.
     * 
     * @return ์ƒ์„ฑ๋œ ๋ฒ„ํ‚ท ์„ค์ •
     */
    private BucketConfiguration createBucketConfiguration() {
        return BucketConfiguration.builder()
                // ๋ฒ„ํ‚ท์— ๋Œ€ํ•œ ์ œํ•œ(์šฉ๋Ÿ‰๊ณผ ์žฌ์ถฉ์ „ ์†๋„)์„ ์„ค์ •
                .addLimit(Bandwidth.builder().capacity(CAPACITY).refillIntervally(REFILL_AMOUNT, REFILL_DURATION).build())
                .build();
    }

    /**
     * API ํ‚ค์— ํ•ด๋‹นํ•˜๋Š” ๋ฒ„ํ‚ท์—์„œ ํ† ํฐ์„ ์†Œ๋น„ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜๋Š” ๋ฉ”์„œ๋“œ.
     * 
     * @param apiKey API ํ‚ค
     * @return ํ† ํฐ ์†Œ๋น„ ์„ฑ๊ณต ์—ฌ๋ถ€
     */
    public boolean tryConsume(String apiKey) {
        // API ํ‚ค์— ํ•ด๋‹นํ•˜๋Š” ๋ฒ„ํ‚ท์„ ๊ฐ€์ ธ์˜ด
        BucketProxy bucket = getOrCreateBucket(apiKey);
        // ๋ฒ„ํ‚ท์—์„œ ํ† ํฐ์„ ์†Œ๋น„ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜
        boolean consumed = bucket.tryConsume(1);
        log.info("API Key: {}, Consumed: {}, Time: {}", apiKey, consumed, LocalDateTime.now());
        return consumed;
    }
}

๋งŒ์•ฝ ์—ฌ๊ธฐ์„œ ์ปค์Šคํ…€ํ•œ ํ…Œ์ด๋ธ” ํ˜น์€ ์ปฌ๋Ÿผ์„ ์ง€์ •ํ•˜๊ฑฐ๋‚˜, ์Šคํ‚ค๋งˆ๋ฅผ ์ง€์ •ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์˜ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

  var tableSettings = BucketTableSettings.customSettings("test.bucket","id", "state");

 

Bucket4j-redis

Bucket4j๋ฅผ Redis์— ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ MariaDB์— ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ๊ฑฐ์˜ ๋™์ผํ•ฉ๋‹ˆ๋‹ค. Bucket4j๋Š” Redis์˜ Redisson, Jedis, Lettuce๋ฅผ ๋ชจ๋‘ ์ง€์›ํ•˜๋ฉฐ, ์•„๋ž˜ ์˜ˆ์ œ์—์„œ๋Š” Lettuce๋ฅผ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

1. dependency

์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

dependencies {
    ...    
    
    // bucket4j
    implementation 'com.bucket4j:bucket4j-redis:8.7.0'
}

2. APIRateLimiter

@Slf4j
@Component
public class APIRateLimiter {
    // ๋ฒ„ํ‚ท์˜ ์šฉ๋Ÿ‰์„ ์„ค์ •, ์ด๋Š” ๋ฒ„ํ‚ท์— ๋‹ด๊ธธ ์ˆ˜ ์žˆ๋Š” ํ† ํฐ์˜ ์ตœ๋Œ€ ์ˆ˜๋ฅผ ์˜๋ฏธ
    private static final int CAPACITY = 3;
    // ํ† ํฐ์ด ์–ผ๋งˆ๋‚˜ ๋น ๋ฅด๊ฒŒ ์žฌ์ถฉ์ „๋ ์ง€ ์„ค์ •, ์ด๋Š” ์ง€์ •๋œ ์‹œ๊ฐ„ ๋™์•ˆ ๋ฒ„ํ‚ท์— ์ถ”๊ฐ€๋  ํ† ํฐ์˜ ์ˆ˜๋ฅผ ์˜๋ฏธ
    private static final int REFILL_AMOUNT = 3;
    // ํ† ํฐ์ด ์žฌ์ถฉ์ „๋˜๋Š” ๋นˆ๋„๋ฅผ ์„ค์ •
    private static final Duration REFILL_DURATION = Duration.ofSeconds(5);

    // LettuceBasedProxyManager ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑ, ์ด ๊ฐ์ฒด๋Š” ๋ฒ„ํ‚ท์˜ ์ƒ์„ฑ ๋ฐ ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹น
    private final LettuceBasedProxyManager<String> proxyManager;
    // ๋™์ผํ•œ API ํ‚ค์— ๋Œ€ํ•œ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๋ฒ„ํ‚ท์„ ์žฌ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋ฒ„ํ‚ท์„ ์ €์žฅํ•˜๋Š” ๋งต์„ ์ƒ์„ฑ
    private final ConcurrentMap<String, Bucket> buckets = new ConcurrentHashMap<>();

    /**
     * APIRateLimiter ์ƒ์„ฑ์ž.
     * 
     * @param redisClient Redis ํด๋ผ์ด์–ธํŠธ
     */
    public APIRateLimiter(RedisClient redisClient) {
        // Redis ์—ฐ๊ฒฐ์„ ์ƒ์„ฑ
        StatefulRedisConnection<String, byte[]> connection = redisClient.connect(RedisCodec.of(StringCodec.UTF8, ByteArrayCodec.INSTANCE));
        // Redis ์—ฐ๊ฒฐ์„ ์ด์šฉํ•ด LettuceBasedProxyManager ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑ
        this.proxyManager = LettuceBasedProxyManager.builderFor(connection)
                .withExpirationStrategy(ExpirationAfterWriteStrategy.basedOnTimeForRefillingBucketUpToMax(Duration.ofSeconds(100)))
                .build();
    }

    /**
     * API ํ‚ค์— ํ•ด๋‹นํ•˜๋Š” ๋ฒ„ํ‚ท์„ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜, ์—†์„ ๊ฒฝ์šฐ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ.
     * 
     * @param apiKey API ํ‚ค
     * @return ํ•ด๋‹น API ํ‚ค์— ๋Œ€์‘ํ•˜๋Š” ๋ฒ„ํ‚ท
     */
    private Bucket getOrCreateBucket(String apiKey) {
        return buckets.computeIfAbsent(apiKey, key -> {
            // ๋ฒ„ํ‚ท ์„ค์ •์„ ์ƒ์„ฑ
            BucketConfiguration configuration = createBucketConfiguration();
            // ๋ฒ„ํ‚ท ID์™€ ๋ฒ„ํ‚ท ์„ค์ •์„ ์ด์šฉํ•ด ๋ฒ„ํ‚ท์„ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ ๋ฐ˜ํ™˜
            return proxyManager.builder().build(key, configuration);
        });
    }

    /**
     * ๋ฒ„ํ‚ท ์„ค์ •์„ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ.
     * 
     * @return ์ƒ์„ฑ๋œ ๋ฒ„ํ‚ท ์„ค์ •
     */
    private BucketConfiguration createBucketConfiguration() {
        return BucketConfiguration.builder()
                // ๋ฒ„ํ‚ท์— ๋Œ€ํ•œ ์ œํ•œ(์šฉ๋Ÿ‰๊ณผ ์žฌ์ถฉ์ „ ์†๋„)์„ ์„ค์ •
                .addLimit(Bandwidth.simple(CAPACITY, REFILL_DURATION).withInitialTokens(REFILL_AMOUNT))
                .build();
    }

    /**
     * API ํ‚ค์— ํ•ด๋‹นํ•˜๋Š” ๋ฒ„ํ‚ท์—์„œ ํ† ํฐ์„ ์†Œ๋น„ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜๋Š” ๋ฉ”์„œ๋“œ.
     * 
     * @param apiKey API ํ‚ค
     * @return ํ† ํฐ ์†Œ๋น„ ์„ฑ๊ณต ์—ฌ๋ถ€
     */
    public boolean tryConsume(String apiKey) {
        // API ํ‚ค์— ํ•ด๋‹นํ•˜๋Š” ๋ฒ„ํ‚ท์„ ๊ฐ€์ ธ์˜ด
        Bucket bucket = getOrCreateBucket(apiKey);
        // ๋ฒ„ํ‚ท์—์„œ ํ† ํฐ์„ ์†Œ๋น„ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜
        boolean consumed = bucket.tryConsume(1);
        log.info("API Key: {}, Consumed: {}, Time: {}", apiKey, consumed, LocalDateTime.now());
        return consumed;
    }
}

 

 

ํ…Œ์ŠคํŠธ

์•„๋ž˜์™€ ๊ฐ™์ด AOP(Aspect-Oriented Programming)๋ฅผ ์ ์šฉํ•˜์—ฌ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ๋†’์ด๊ณ  ์ค‘๋ณต์„ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

RateLimit

๋จผ์ €, ๊ฐ ๋ฉ”์„œ๋“œ์— ์ ์šฉํ•  ์‚ฌ์šฉ์ž ์ •์˜ ์• ๋…ธํ…Œ์ด์…˜์ธ RateLimit์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด ์• ๋…ธํ…Œ์ด์…˜์€ API ํ‚ค๋ฅผ ์ง€์ •ํ•˜๋Š” 'key'๋ผ๋Š” ์†์„ฑ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimit {
    String key();
}

RateLimitingAspect

๋‹ค์Œ์œผ๋กœ, RateLimit ์• ๋…ธํ…Œ์ด์…˜์„ ์ฒ˜๋ฆฌํ•  Aspect ํด๋ž˜์Šค์ธ RateLimitingAspect๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด ํด๋ž˜์Šค๋Š” APIRateLimiter ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ API ํ‚ค์— ํ•ด๋‹นํ•˜๋Š” ๋ฒ„ํ‚ท์—์„œ ํ† ํฐ์„ ์†Œ๋น„ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ํ† ํฐ์ด ์ถฉ๋ถ„ํ•˜๋ฉด ์š”์ฒญ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

@Aspect
@Component
public class RateLimitingAspect {

    private final APIRateLimiter apiRateLimiter;

    @Autowired
    public RateLimitingAspect(APIRateLimiter apiRateLimiter) {
        this.apiRateLimiter = apiRateLimiter;
    }

    @Around("@annotation(rateLimit)")
    public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        if (apiRateLimiter.tryConsume(rateLimit.key())) {
            return joinPoint.proceed();
        } else {
            throw new RuntimeException("Rate limit exceeded for key: " + rateLimit.key());
        }
    }
}

RateLimitingController

@RestController
@Slf4j
public class RateLimitingController {
    @Autowired
    private RateLimitingService rateLimitingService;

    @GetMapping("/test")
    public String test() {
        return rateLimitingService.run();
    }
}

RateLimitingService

@Service
public class RateLimitingService {
    @RateLimit(key = "someUniqueKey")
    public String run() {
        return "์š”์ฒญ ์„ฑ๊ณต";
    }
}

ํ† ํฐ ์ตœ๋Œ€ ๊ฐฏ์ˆ˜์ธ 3๊ฐœ๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ์‹คํŒจ, 5์ดˆํ›„ ํ† ํฐ์ด ์ฑ„์›Œ์ง€๋ฉด ์„ฑ๊ณต

 

 

์ •๋ฆฌ

Bucket4j ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜๋ฉด, API ํ˜ธ์ถœ์˜ ์ฒ˜๋ฆฌ์œจ์„ ์†์‰ฝ๊ฒŒ ์ œํ•œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ๋‹ค์–‘ํ•œ ๊ณณ์—์„œ ์ ์šฉํ•˜๊ณ  ๋ถ€๊ฐ€ ๊ธฐ๋Šฅ์„ ๋„ฃ์„ ์ˆ˜ ์žˆ๋Š”๋ฐ, Baeldung์˜ Bucket4j ๊ฐ€์ด๋“œ๋ฅผ ๋ณด๋ฉด ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ์ด์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌ์œจ์„ ์ œํ•œํ•  ์ˆ˜๋„ ์žˆ๊ณ , ์•„๋ž˜์ฒ˜๋Ÿผ ์‚ฌ์šฉ์ž์˜ ์š”๊ธˆ์ œ์— ๋”ฐ๋ผ API ํ˜ธ์ถœ ์ œํ•œ์„ ๋‹ค๋ฅด๊ฒŒ ์ ์šฉํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

enum PricingPlan {
    // ํ•œ์‹œ๊ฐ„์— 20๋ฒˆ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
    FREE {
        Bandwidth getLimit() {
            return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1)));
        }
    },
    // ํ•œ์‹œ๊ฐ„์— 40๋ฒˆ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
    BASIC {
        Bandwidth getLimit() {
            return Bandwidth.classic(40, Refill.intervally(40, Duration.ofHours(1)));
        }
    },
    // ํ•œ์‹œ๊ฐ„์— 100๋ฒˆ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
    PROFESSIONAL {
        Bandwidth getLimit() {
            return Bandwidth.classic(100, Refill.intervally(100, Duration.ofHours(1)));
        }
    };
    //..
}

Bucket4j๋Š” ์ด์™€ ๊ฐ™์ด ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ์—์„œ API ํ˜ธ์ถœ ์ œํ•œ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, ์„œ๋น„์Šค์˜ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ค๊ณ  ์„œ๋ฒ„์˜ ๋ถ€ํ•˜๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์„ ์‰ฝ๊ณ  ๋น ๋ฅด๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋Œ“๊ธ€