특정 시간 동안 실행 횟수를 제한하기 위한 라이브러를 검색해서 아래 3가지 정도를 찾았다.
- RateLimiter (guava, https://github.com/google/guava)
- RateLimitJ (https://github.com/mokies/ratelimitj)
- Bucket4j (https://github.com/vladimir-bukhtoyarov/bucket4j)
Guava RateLimiter
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);
[노트]
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 문서를 참고한다.