BackEnd🌱/Spring

Bucket4j둜 νŠΈλž˜ν”½ μ œν•œν•˜κΈ°(Redis & MariaDB)

dkswnkk 2023. 12. 3. 23:43

κ°œμš”

졜근 업무 ν”„λ‘œμ νŠΈμ—μ„œ νŠΉμ •(μš”κΈˆμ΄ λΆ€κ°€λ˜λŠ”) λ‘œμ§μ— λŒ€ν•΄ 월별 μ‚¬μš©λŸ‰μ„ μ œν•œν•˜λŠ” κΈ°λŠ₯이 μΆ”κ°€λ˜μ–΄μ•Ό ν–ˆμŠ΅λ‹ˆλ‹€. 이와 κ΄€λ ¨ν•˜μ—¬ 처리율 μ œν•œ κΈ°μˆ μ„ μ•Œμ•„λ³΄μ•˜λŠ”λ° 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 호좜 μ œν•œμ„ κ΅¬ν˜„ν•˜λŠ” 데 μœ μš©ν•˜κ²Œ μ‚¬μš©λ  수 μžˆμŠ΅λ‹ˆλ‹€. λ”°λΌμ„œ, μ„œλΉ„μŠ€μ˜ μ„±λŠ₯을 ν–₯μƒμ‹œν‚€κ³  μ„œλ²„μ˜ λΆ€ν•˜λ₯Ό κ΄€λ¦¬ν•˜λŠ” 데 ν•„μš”ν•œ κΈ°λŠ₯을 쉽고 λΉ λ₯΄κ²Œ κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.