Bucket4j

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);
        }
    }