특정 시간 동안 실행 횟수를 제한하기 위한 라이브러를 검색해서 아래 3가지 정도를 찾았다.

이 글에서는 각 라이브러리의 사용법을 간단하게 살펴본다.

Guava RateLimiter

Guava에 포함된 RateLimiter를 사용하면 초당 실행 횟수를 제한할 수 있다. 이 클래스를 사용하려면 다음 의존을 추가한다.

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>26.0-jre</version>
</dependency>

사용 방법은 다음과 같다.

private RateLimiter limiter = RateLimiter.create(4.0); // 초당 4개

public void someLimit() {
    if (limiter.tryAcquire()) { // 1개 사용 요청
        // 초과하지 않음
        ... 코드1 // 초당 4번까지 실행 가능

    } else {
        // 제한 초과

    }
}

RateLimiter.create() 메서드는 초당 몇 개를 허용할지를 인수로 받는다. 위 예의 경우 4.0을 값으로 주었는데 이는 초당 4개를 허용함을 뜻한다. 이 값을 0.2로 주면 초당 0.4개를 허용하므로 5초당 1개를 허용한다.


실행 횟수를 초과했는지 검사할 때 tryAcquire() 메서드를 사용한다. 이 메서드는 허용하는 횟수를 초과하지 않았으면 true를 리턴하고 초과했으면 false를 리턴한다. 따라서 위 코드는 someLimit() 메서드의 '코드1'을 초당 4번까지 실행한다. 만약 1초 이내에 5번을 실행하면 그 중 한 번은 tryAcquire() 메서드가 false를 리턴한다.


RateLimiter는 실행 가능 시점을 분배하는 방식을 사용한다. 예를 들어 다음과 같이 초당 실행 가능한 횟수를 5.0으로 지정하고 0.1초마다 tryAcquire() 메서드를 실행한다고 하자.


RateLimiter limiter = RateLimiter.create(5.0);


Timer timer = new Timer(true);


timer.scheduleAtFixedRate(new TimerTask() {

    @Override

    public void run() {

        if (limiter.tryAcquire()) {

            log.info("!! OK");

        } else {

            log.warn("XX");

        }

    }

}, 0, 100); // 0.1초마다 실행


Thread.sleep(1500);


위 코드를 실행한 결과는 다음과 같다. 이 결과를 보면 "!! OK"와 "XX"가 번갈아 출력된 것을 알 수 있다. 즉 초당 5.0으로 횟수를 제한했을 때 1초에 10번 tryAcquire()를 실행하면 연속해서 5번 실행 가능한 게 아니고 1초에 5번 실행 가능한 시점이 분산되는 것을 알 수 있다.

17:27:39.503 [Timer-0] INFO guava.RateLimiterTest - !! OK
17:27:39.596 [Timer-0] WARN guava.RateLimiterTest - XX
17:27:39.695 [Timer-0] INFO guava.RateLimiterTest - !! OK
17:27:39.797 [Timer-0] WARN guava.RateLimiterTest - XX
17:27:39.898 [Timer-0] INFO guava.RateLimiterTest - !! OK
17:27:39.998 [Timer-0] WARN guava.RateLimiterTest - XX
17:27:40.098 [Timer-0] INFO guava.RateLimiterTest - !! OK
17:27:40.197 [Timer-0] WARN guava.RateLimiterTest - XX
17:27:40.297 [Timer-0] INFO guava.RateLimiterTest - !! OK
17:27:40.396 [Timer-0] WARN guava.RateLimiterTest - XX
17:27:40.498 [Timer-0] INFO guava.RateLimiterTest - !! OK
17:27:40.594 [Timer-0] WARN guava.RateLimiterTest - XX
17:27:40.695 [Timer-0] INFO guava.RateLimiterTest - !! OK
17:27:40.795 [Timer-0] WARN guava.RateLimiterTest - XX
17:27:40.897 [Timer-0] INFO guava.RateLimiterTest - !! OK
17:27:40.997 [Timer-0] WARN guava.RateLimiterTest - XX


[노트]

RateLimiter의 내부 구현은 1초 동안 사용하지 않은 개수를 누적한다. 예를 들어 RateLimiter.create(10)으로 만든 RateLimiter를 1초 동안 사용하지 않았다면 5개가 누적되어 이후 1초 동안은 10개를 사용할 수 있다.


RateLimitJ

RateLimitJ는 Redis, Hazelcast를 이용해서 시간 당 실행 횟수를 제한할 수 있다. 메모리를 이용한 구현도 지원하므로 Redis나 Hazelcast가 없어도 사용할 수 있다. 이 글에서는 인메모리 구현의 사용법을 살펴본다. 인메모리 구현을 사용하려면 다음 의존을 추가한다. Redis를 이용한 구현을 사용하는 방법은 https://github.com/mokies/ratelimitj 사이트를 참고한다.


<dependency>

    <groupId>es.moki.ratelimitj</groupId>

    <artifactId>ratelimitj-inmemory</artifactId>

    <version>0.5.0</version>

</dependency>


RateLimitJ를 이용한 실행 횟수 제한 방법은 다음과 같다.


// 1분에 10개 제한

RequestLimitRule limitRule = RequestLimitRule.of(Duration.ofMinutes(1), 10);

RequestRateLimiter rateLimiter =

        new InMemorySlidingWindowRequestRateLimiter(limitRule);


if (rateLimiter.overLimitWhenIncremented("key")) {

    // 제한 초과


} else {

    // 초과하지 않음


}


RequestLimitRule은 제한 규칙을 적용한다. RequestLimitRule.of() 메서드를 이용해서 제한 규칙을 생성하는데 첫 번째 파라미터는 시간 범위이고 두 번째 파라미터는 제한 횟수이다. 위 코드는 1분 동안 10으로 제한하는 규칙을 생성한다.


이 규칙을 사용해서 InMemorySlidingWindowRequestRateLimiter 객체를 생성하면 사용할 준비가 끝난다.


RequestRateLimiter#overLimitWhenIncremented(key) 메서드는 특정 시간 동안 지정한 횟수를 초과했는지 검사한다. 초과했으면 true를 리턴하고 초과하지 않았으면 false를 리턴한다. 따라서 이 메서드가 false를 리턴할 때 기능을 실행하면 된다.


overLimitWhenIncremented() 메서드는 인수로 key를 받는다. 이 key 별로 규칙을 적용한다. 예를 들어 URI 마다 실행 횟수를 제한하고 싶다면 다음과 같이 key로 URI를 사용하면 된다.


// 각 URI마다 실행 횟수 제한

if (rateLimiter.overLimitWhenIncremented(request.getRequestURI())) {

    // 해당 URI에 대한 제한 초과


} else {

    // 해당 URI에 대한 접근 허용


}


Guava의 RateLimiter와 달리 RateLimitJ는 지정한 횟수에 다다를 때까지 실행을 허용하고 그 이후로는 시간이 지날 때까지 실행을 허용하지 않는다. 예를 들어 다음 코드를 보자.


RequestLimitRule limitRule = RequestLimitRule.of(Duration.ofSeconds(1), 5);

RequestRateLimiter rateLimiter =

        new InMemorySlidingWindowRequestRateLimiter(limitRule);


Timer timer = new Timer(true);


timer.scheduleAtFixedRate(new TimerTask() {

    @Override

    public void run() {

        if (rateLimiter.overLimitWhenIncremented("key")) {

            log.warn("XX"); // 제한 초과로 실행하지 않음

        } else {

            log.info("!! OK"); // 제한을 초과하지 않아 실행함

        }

    }

}, 0, 100);


Thread.sleep(1500);


이 코드는 1초 당 5개로 제한하는 RequestRateLimiter를 사용하고 0.1초마다 한 번씩 기능을 실행하고 있다. 이 코드를 실행하면 다음과 같이 처음 5개 요청을 실행하고 그 이후 1초가 지날 때까지 5개 요청은 제한 초과로 실행하지 않은 것을 알 수 있다.


17:46:38.061 [Timer-0] INFO ratelimitj.RateLimitJTest - !! OK

17:46:38.130 [Timer-0] INFO ratelimitj.RateLimitJTest - !! OK

17:46:38.230 [Timer-0] INFO ratelimitj.RateLimitJTest - !! OK

17:46:38.330 [Timer-0] INFO ratelimitj.RateLimitJTest - !! OK

17:46:38.430 [Timer-0] INFO ratelimitj.RateLimitJTest - !! OK

17:46:38.531 [Timer-0] WARN ratelimitj.RateLimitJTest - XX

17:46:38.632 [Timer-0] WARN ratelimitj.RateLimitJTest - XX

17:46:38.733 [Timer-0] WARN ratelimitj.RateLimitJTest - XX

17:46:38.833 [Timer-0] WARN ratelimitj.RateLimitJTest - XX

17:46:38.931 [Timer-0] WARN ratelimitj.RateLimitJTest - XX

17:46:39.033 [Timer-0] INFO ratelimitj.RateLimitJTest - !! OK

17:46:39.134 [Timer-0] INFO ratelimitj.RateLimitJTest - !! OK

17:46:39.235 [Timer-0] INFO ratelimitj.RateLimitJTest - !! OK

17:46:39.330 [Timer-0] INFO ratelimitj.RateLimitJTest - !! OK

17:46:39.432 [Timer-0] INFO ratelimitj.RateLimitJTest - !! OK


Bucket4j

Bucket4j는 hazelcast, infinispan을 이용한 구현 외에 인메모리 구현을 지원한다. 이 글에서는 인모메리 구현을 이용한 횟수 제한에 대해 살펴본다. 다른 구현에 대한 내용은 https://github.com/vladimir-bukhtoyarov/bucket4j 문서를 참고한다.


인메모리 구현을 사용하려면 다음 의존을 추가한다.


<dependency>

    <groupId>com.github.vladimir-bukhtoyarov</groupId>

    <artifactId>bucket4j-core</artifactId>

    <version>4.0.1</version>

</dependency>


사용법은 다음과 같다.


// 1초에 5개 사용 제한

Bandwidth limit = Bandwidth.simple(5, Duration.ofSeconds(1));

// 버킷 생성

Bucket bucket = Bucket4j.builder().addLimit(limit).build();


if (bucket.tryConsume(1)) { // 1개 사용 요청

    // 초과하지 않음


} else {

    // 제한 초과

    

}


Bandwith는 지정한 시간 동안 제한할 개수를 지정한다. 위 코드는 1초 동안 5개를 허용하는 Bandwith를 생성한다. 이 Bandwith를 이용해서 Bucket을 생성한다. Bucket#tryConsume() 메서드는 사용할 개수를 인수로 받으며, 사용 가능할 경우 true를 리턴하고 사용 가능하지 않으면 false를 리턴한다.



[노트]

Bucket4j는 다중 Bandwidth를 지원한다. 또한 사용 가능 개수를 시간이 흘러감에 따라 점진적으로(greedy) 채우는 방식과 시간 간격마다 채우는 방식을 지원한다. 이에 대한 내용은 Bucket4j 문서를 참고한다.



+ Recent posts