bucket4j 란 API 요청을 컨트롤 할 수 있는 라이브러리다.
쉽게 말해 DDOS 공격 같이 짧은 시간 내에 많은 요청을 보내는 것을 제한할 수 있게 하는 라이브러리다.
동작 방법은 토큰을 만들고 api 요청이 올 때 마다 토큰을 소비하고 일정 시간마다 토큰을 재 충전하여 요청을 제한할 수 있다.
이 방법으로 사용자 개개인의 api요청을 제한할 수 있고 서버 전체에서 제한할 수 있다.
필자는 문자 전송 API에 서버 만의 제한을 뒀다.
개인마다 요청 제한 + 서버 전체 제한 두 가지를 같이 적용해야 하지만 필자가 만드는 서비스는 곧 사라질 서비스라 서버 전체에서만 제한을 두기로 했다.
bucket4j를 사용하려면 의존성을 추가해야한다.
jdk 11이상은 위 jdk8은 밑을 붙여넣는다.
<!-- For java 11+ -->
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.7.1</version>
</dependency>
<!-- For java 8 -->
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j_jdk8-core</artifactId>
<version>8.7.1</version>
</dependency>
import io.github.bucket4j.Bucket;
...
// bucket with capacity 20 tokens and with refilling speed 1 token per each 6 second
private static Bucket bucket = Bucket.builder()
.addLimit(limit -> limit.capacity(20).refillGreedy(10, Duration.ofMinutes(1)))
.build();
private void doSomethingProtected() {
if (bucket.tryConsume(1)) {
doSomething();
} else {
throw new SomeRateLimitingException();
}
}
공식 문서에 나와있는 예제이다. builder로 초기 값에 최대 값과 분당 몇 개가 리필 되는지 설정할 수 있다.
필자는 아래와 같이 bean으로 등록해 사용했다.
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class BucketConfig {
@Bean
public Bucket bucket() {
//60초에 100개의 토큰씩 충전
final Refill refill = Refill.intervally(100, Duration.ofSeconds(60));
//버킷의 크기는 100개
final Bandwidth limit = Bandwidth.classic(100, refill);
return Bucket.builder()
.addLimit(limit)
.build();
}
}
---
@RequestMapping(value="/sendMsg", method = RequestMethod.POST , produces = "application/json; charset=utf8")
public ResponseEntity<ResCodeDto> send(HttpServletRequest request, @RequestBody SmsRequestDto dto) {
if (bucket.tryConsume(1)) {
ResCodeDto resDto = ResCodeDto.builder()
.code(HttpStatus.ACCEPTED.value())
.data(zSer.smsSend(dto))
.build();
return ResponseEntity.status(HttpStatus.OK).body(resDto);
} else {
ResCodeDto resDto = ResCodeDto.builder()
.code(HttpStatus.TOO_MANY_REQUESTS.value())
.data("잠시 후 다시 시도해 주세요")
.build();
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(resDto);
}
}