이전
bucket4j
토큰 버킷 알고리즘을 기반으로 하는 Java 속도 제한 라이브러리. (Java rate-limiting library based on token-bucket algorithm.)
특정 유저들에게 특정 메소드에 대한 요청 제한을 두거나, API 공개 후 특정 요금제에 맞는 요청제한을 제어 할 수 있는 방법은?
Token Bucket 알고리즘은 패킷에 토큰을 심어놨다가 요청이 들어올때마다 하나씩 줄여서 0이 되면 요청을 거부하도록 하는 알고리즘이다.
Bucket4J Class 주요 클래스
Refill
일정시간마다 몇개의 Token을 충전할지 지정하는 클래스. 예를 들어 초당 10개의 버킷 또는 5분당 100개의 토큰 등입니다.
- Greedy : bandwidth를 생성할때 사용되는 기본 리필 유형입니다.
// 아래 두 줄의 코드는 완전히 동일합니다. Bandwidth.simple(100, Duration.ofMinutes(1)) Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1)))
- Interval : 리필은 간격방식으로 토큰을 재생성합니다. 전체 토큰을 재생성하기 전에 전체 기간이 경과할 때 까지 기다립니다.
// 분당 100개 토큰 생성
Refill.intervally(100, Duration.ofMinutes(1));
// 1분에 5개의 요청을 할 수 있는 분당 Limit 설정
Refill.intervally(5, Duration.ofMinutes(1));
// 10초에 2개의 요청을 할 수 있는 초당 Limit 설정
Refill.intervally(2, Duration.ofSeconds(10));
Bandwidth
Bucket의 총 크기를 지정하는 클래스, 앞에서 Token 충전 주기 및 개수를 지정한 Refill 클래스를 사용하여 만든다.
Bandwidth bandwitdh = Bandwidth.classic(long capacity, Refill refill)
Bucket
해당 클래스는 최종적으로 트래픽 제어를 할 클래스이며 앞에서 만든 Bandwidth 클래스를 사용하여 Build 한다. 다음과 같이 빌더로 객체를 구성한다.
Bucket bucket = Bucket.builder()
.addLimit(bandwitdh)
.build();
사용자가 직접 생성자를 통해 라이브러리의 객체를 구성하지 않도록 라이브러리 작성자가 명시적으로 결정한 결과물 그렇게 한이유는 미래에 이전 버전과의 호환성을 깨뜨리지 않고 내부 구현을 변경할 수 있습니다. 또한 Fluent Builder API는 현대적인 디자인 패턴이기 때문이다.
주문 요청은 10분에 10개의 요청만 처리 할수 있다.
@RequestMapping("/order")
public class OrderController {
private final Bucket bucket;
private final Response response;
private final OrderService orderService;
public OrderController(Response response, OrderService orderService) {
this.response = response;
this.orderService = orderService;
//10분에 10개의 요청을 처리할 수 있는 Bucket 생성
Bandwidth limit = Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(10)));
this.bucket = Bucket.builder()
.addLimit(limit)
.build();
}
/**
* 주문
*/
@PostMapping
public ResponseEntity<Body> order(@RequestBody @Valid OrderCreate orderCreate) {
if (bucket.tryConsume(1)) { //
OrderResponse orderResponse = orderService.order(orderCreate);
return response.success(Code.SAVE_SUCCESS, orderResponse);
}
System.out.println("TOO MANY REQUEST");
return response.fail(ErrorCode.TOO_MANY_REQUESTS);
}
}
tryConsume is
/**
* Tries to consume a specified number of tokens from this bucket.
*
* @param numTokens The number of tokens to consume from the bucket, must be a positive number.
*
* @return {@code true} if the tokens were consumed, {@code false} otherwise.
*/
boolean tryConsume(long numTokens);
요금제 구현
rate plans
public enum PricingPlan {
//1시간에 3번 사용가능한 무제한 요금제
FREE {
public Bandwidth getLimit() {
return Bandwidth.classic(3, Refill.intervally(3, Duration.ofHours(1)));
}
},
//1시간에 5 사용가능한 Basic 요금제
BASIC {
public Bandwidth getLimit() {
return Bandwidth.classic(5, Refill.intervally(5, Duration.ofHours(1)));
}
},
//1시간에 10번 사용가능한 Professional 요금제
PROFESSIONAL {
public Bandwidth getLimit() {
return Bandwidth.classic(10, Refill.intervally(10, Duration.ofHours(1)));
}
};
public abstract Bandwidth getLimit();
public static PricingPlan resolvePlanFromApiKey(String apiKey) {
if (apiKey == null || apiKey.isEmpty()) {
return FREE;
} else if (apiKey.startsWith("BA-")) {
return BASIC;
} else if (apiKey.startsWith("PX-")) {
return PROFESSIONAL;
}
return FREE;
}
}
Service
@Service
@RequiredArgsConstructor
public class PricingPlanService {
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
public Bucket resolveBucket(String apiKey) {
return cache.computeIfAbsent(apiKey, this::newBucket);
}
private Bucket newBucket(String apiKey) {
PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey);
return Bucket.builder()
.addLimit(pricingPlan.getLimit())
.build();
}
}
Controller
@GetMapping("/{itemId}")
public ResponseEntity<Body> get(@RequestHeader(value = "X-api-key") String apiKey, @PathVariable Long itemId) {
Bucket bucket = pricingPlanService.resolveBucket(apiKey);
ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
long saveToekn = probe.getRemainingTokens();
if (probe.isConsumed()) {
ItemResponse itemResponse = itemService.get(itemId);
return response.success(Code.SEARCH_SUCCESS, itemResponse);
}
long waitForRefill = probe.getNanosToWaitForRefill();
System.out.println("TOO MANY REQUEST");
System.out.println("Available Toekn : " + saveToekn);
System.out.println("Wait Time " + waitForRefill + "Second");
return response.fail(ErrorCode.TOO_MANY_REQUESTS);
}
참고