최근에 대규모 시스템 설계에 대해 학습하며, 처리율 제한 장치에 대해 알게 되었다.
개발자라면 Open API를 한번 쯤은 써봤을 것을 것이고, 아직이라면 써보려고 생각이라도 해봤을 것이다.
구름톤에서는 "거인의 어깨 위에 올라타라" 라고 표현을 하니 중요성은 충분하다고 본다.
여기서 내가 강조하고 싶은 건, 오픈 소스라고 해서 모두 무료라고 생각하지 말라는 것이다.
요즘 핫한 ChatGPT API를 제공하는 Open AI만 봐도, 카드 등록을 해놓지 않으면 해당 소스를 사용할 수 없다.
더군다나 사용량이 일정 기준을 넘어가면, 요금이 청구된다.
실제 운용되는 큰 서비스가 아닌, 작고.. 소중한.. 우리 프로젝트에서도 돈이 소중한 건 모두 마찬가지가 아니겠나?
이러한 상황들을 막기 위해 요청은 1분에 최대 3번만 보내도록 하기..
등의 조치를 취할 수 있도록
우리는 "처리율 제한 장치"를 도입해야 한다.
앞에서는 그나마 모두가 걱정했을 법 한 이야기로 예시를 들어 이해하기 쉽게 설명한 것이다.
처리율 제한 장치에 대한 자세한 내용은 맨 처음에 첨부한 링크를 클릭해 학습해보길 바란다.
아무쪼록 이론에 대해 학습했으면, 직접 실습까지 해봐야 제대로 이해가 되지 않을까? 일단 해보자!
Spring Boot에서 사용할 수 있는 처리율 제한 구현 라이브러리는 대표적으로 4가지가 있다.
토큰 버킷 알고리즘을 기반으로 하는 Java 속도 제한 라이브러리이다.
특징으로는 동시에 lock-free한 구현으로 멀티 스레딩 환경에서의 확장성이 우수하며, 추가적인 동시성 전략도 제공한다.
토큰 버킷 알고리즘을 기반으로 구현된 라이브러리, 구글에서 개발한 것이다.
사실 guava 자체는 처리율 제한 목적으로 나온 라이브러리가 아니지만,
guava가 제공하는 기능 중 처리율 제한 기능이 존재한다.
이동 윈도 알고리즘을 기반으로 하는 Java 속도 제한 라이브러리이다.
Github ReadMe에 첫 번째 줄부터 아래와 같이 적혀있는 게 눈에 먼저 들어왔다.
"더 이상 지원하지 않는 프로젝트이니 Bucket4j를 사용하시오."
중요한 프로젝트에서는 사용하지 않는 것이 좋겠다.
함수형 프로그래밍을 위해 설계된 Java 라이브러리로, 서킷 브레이커, Rate Limiter, Retry 등을 사용한 고차 함수를 제공한다.
여기서 말하는 서킷 브레이커는 "한 모듈이 장애가 나도 다른 모듈은 멀쩡해야 한다"라는 MSA 회복성 패턴 중 하나인데,
아직 필자는 MSA에 대해 무지하여 해당 라이브러리는 시도하지 못했다.
특징으로는 java 17 이상( = Spring Boot 3 이상)부터만 사용할 수 있다고 하니 참고하자!
위에 나온 4가지 선택지 중, 필자는 첫 번째로 나온 Bucket4j를 사용하기로 했다.
처리율 제한을 목적으로 두고 나온 라이브러리이기도 하며, 현재 적용할 프로젝트에 가장 적합하다고 판단했다.
implementation 'com.bucket4j:bucket4j-core:8.3.0'
이 라이브러리에는 크게 3가지 클래스가 존재한다.
Refill
: 일정 시간마다 충전할 Token의 개수 지정Bandwidth
: Bucket의 총 크기를 지정Bucket
: 실제 트래픽 제어에 사용이를 코드로 작성하면? baeldung에서 제시한 예제 코드는 아래와 같다.
@RestController
public class Controller {
private final Bucket bucket;
public Controller() {
// 충전 간격을 10초로 지정하며, 한 번 충전할 때마다 2개의 토큰을 충전한다.
final Refill refill = Refill.intervally(2, Duration.ofSeconds(10));
// Bucket의 총 크기는 3으로 지정
final Bandwidth limit = Bandwidth.classic(3, refill);
// 총 크기는 3이며 10초마다 2개의 토큰을 충전하는 Bucket
this.bucket = Bucket.builder()
.addLimit(limit)
.build();
}
@GetMapping(value = "/api/test")
public ResponseEntity<String> test() {
// API 호출시 토큰 1개를 소비
if (bucket.tryConsume(1)) {
return ResponseEntity.ok("success!");
}
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
}
미들웨어에 처리율 제한 장치를 도입하는 것이 아닌, 서버에 직접 도입하기에는 난이도가 높지 않은 것을 볼 수 있다.
다만, 사실 위 코드에서 Controller 생성 시점에 Bucket을 build하는 과정이 지저분하게만 보인다.
또한 따지고 보면, 해당 도메인과 관련된 로직이 아니라면? 코드를 분리해 줄 필요가 있다.
아래와 같이 특정 도메인의 특정 API에만 적용할 설정 파일을 작성하고, 직접 빈으로 등록을 해주자.
@Configuration
public class BucketConfig {
@Bean
public Bucket bucket() {
//60초에 3개의 토큰씩 충전
final Refill refill = Refill.intervally(3, Duration.ofSeconds(60));
//버킷의 크기는 3개
final Bandwidth limit = Bandwidth.classic(3, refill);
return Bucket.builder()
.addLimit(limit)
.build();
}
}
이후에는 스프링 컨테이너가 애플리케이션 런타임 시에, 자동으로 의존성 주입을 해준다.
아래와 같이 깔끔해진 Controller를 확인할 수 있다!
@RestController
public class Controller {
private final Bucket bucket;
@GetMapping(value = "/api/test")
public ResponseEntity<String> test() {
// API 호출시 토큰 1개를 소비
if (bucket.tryConsume(1)) {
return ResponseEntity.ok("success!");
}
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
}
전체 코드는 링크를 통해 확인하실 수 있습니다.
현재 예제 코드로 작성한 설정 값들이 프로젝트에 최적화 된 값들이 아님에 주의하자.
이는 프로젝트를 진행하는 팀원들과 어떤 정책으로 제한을 둘 지도 함께 정해야 할 필요가 있다.잦은 제 3자의 서비스를 호출하여 과금이 나올까봐 무섭다면? 처리율 제한 장치를 구축할 것!
잘 봤습니다. 좋은 글 감사합니다.