
GDG마켓 플랫폼의 리스크관리 팀의 일원으로 api가 무방비하게 호출되는 상황을 막아야 한다.

Bucket4j 는 주로 토큰 버킷 알고리즘을 기반으로 하는 Java 속도 제한 라이브러리로, IT 산업에서 속도 제한을 위한 사실상의 표준입니다.
[참고자료]
https://github.com/bucket4j/bucket4j
https://github.com/MarcGiffing/bucket4j-spring-boot-starter

Google의 Guava 라이브러리에서 제공하는 레이트 리미터로, 주로 간단한 경우에 사용하는 "토큰 버킷" 기반의 라이브러리입니다.
[참고자료]
https://github.com/google/guava
함수형 프로그래밍을 위해 설계된 Java 라이브러리로, 서킷 브레이커, Rate Limiter, Retry 등을 사용한 고차 함수를 제공한다.

[참고자료]
https://resilience4j.readme.io/
레디스에 탑재된 Lua Script Engine을 통해서 서버에서 Lua Script를 실행하여 atomic 연산을 통해 api rate limiter에서 발생하는 race condition에 대해서 해결할 수 있다.
이를 통해서 MSA 환경에서도 자주 사용됩니다.

[참고자료]
https://dev.gmarket.com/69
implementation 'com.bucket4j:bucket4j-core:8.3.0'
Refill : 일정 시간마다 충전할 Token의 개수 지정
Bandwidth : Bucket의 총 크기를 지정
Bucket : 실제 트래픽 제어에 사용
// 공통된 Bucket 생성 메서드
private Bucket createBucket(int capacity, Duration refillDuration) {
Bandwidth limit = Bandwidth.classic(capacity, Refill.intervally(capacity, refillDuration));
return Bucket.builder()
.addLimit(limit)
.build();
}
[참고자료] https://www.baeldung.com/spring-bucket4j
@RestController
public class HelloController {
@RateLimited(type = RequestType.LOGIN)
@GetMapping(value = "/api/test")
public ResponseEntity<String> test() {
// API 호출시 토큰 1개를 소비
return ResponseEntity.ok("Request successful!1");
}
@RateLimited(type = RequestType.QUERY)
@GetMapping(value = "/api/test2")
public ResponseEntity<String> test2() {
// API 호출시 토큰 1개를 소비
return ResponseEntity.ok("Request successful!2");
}
}
@Configuration
public class BucketConfig {
private final ConcurrentHashMap<String, Bucket> loginBuckets = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Bucket> queryBuckets = new ConcurrentHashMap<>();
public Bucket getBucketForLogin(String ip) {
System.out.println("로그인 관련 요청 IP : " + ip);
return loginBuckets.computeIfAbsent(ip, k -> createBucket(5, Duration.ofHours(1)));
}
public Bucket getBucketForQuery(String ip) {
System.out.println("조회 관련 요청 IP : " + ip);
return queryBuckets.computeIfAbsent(ip, k -> createBucket(5, Duration.ofSeconds(5)));
}
// 공통된 Bucket 생성 메서드
private Bucket createBucket(int capacity, Duration refillDuration) {
Bandwidth limit = Bandwidth.classic(capacity, Refill.intervally(capacity, refillDuration));
return Bucket.builder()
.addLimit(limit)
.build();
}
}
@Aspect
@Component
@RequiredArgsConstructor
public class RateLimitAspect {
private final BucketConfig bucketConfig;
private final HttpServletRequest request;
// @RateLimited 어노테이션이 붙은 메서드를 처리
@Around("@annotation(rateLimited)")
public Object applyRateLimit(ProceedingJoinPoint joinPoint, RateLimited rateLimited) throws Throwable {
try {
String ip = request.getRemoteAddr();
Bucket bucket;
if (rateLimited.type() == RequestType.LOGIN) {
bucket = bucketConfig.getBucketForLogin(ip);
} else {
bucket = bucketConfig.getBucketForQuery(ip);
}
// rate limiting 처리
if (bucket.tryConsume(1)) { // 요청을 1개 토큰만 소비
return joinPoint.proceed(); // 메서드 실행
} else {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Too many requests");
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Server error");
}
}
}
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimited {
RequestType type();
}
public enum RequestType {
LOGIN,
QUERY
}
get요청 : http://127.0.0.1:8080/api/test (정상요청)

get요청 : http://127.0.0.1:8080/api/test (요청거부)

get요청 : http://127.0.0.1:8080/api/test2 (정상요청)
test에서는 거부가 되었지만 다른 api는 정상적으로 접근되도록 처리

get요청 : http://127.0.0.1:8080/api/test (정상요청)
ip를 192.168.1.1 로 변경시 테스트시 정상적인 접근되도록 처리
ip당 처리가 가능할 수 있게 했다.
