Spring에서 throttling을 하기 위해 Guava의 RateLimiter가 주로 사용된다.
사용법과 예제를 정리해본다.
Guava Rate Limiter
Rate Limiter는 초당 실행횟수를 제어해서 throttling을 도와주는 클래스이다.
라이브러리 추가
dependencies {
implementation("com.google.guava:guava:31.1-jre")
}
1초에 한번 실행
describe("1초에 1번 실행") {
val rateLimiter = RateLimiter.create(1.0) // RateLimiter 생성
repeat(10) {
val startTime = ZonedDateTime.now().second
rateLimiter.acquire() // = rateLimiter.acquire(1)
doSomething()
val elapsedTimeSeconds = ZonedDateTime.now().second - startTime
println("elapsedTimeSeconds: $elapsedTimeSeconds")
}
}
- RateLimiter.create(N) : 1초에 N번 실행되도록 RateLimiter를 생성한다.
- acquire() : acquire()는 acquire(1)과 같은 뜻으로, 1번 실행의 허가를 얻는다.
위 코드의 실행 결과는 아래와 같다.
doSomething
elapsedTimeSeconds: 0
doSomething
elapsedTimeSeconds: 1
doSomething
elapsedTimeSeconds: 1
doSomething
elapsedTimeSeconds: 1
doSomething
elapsedTimeSeconds: 1
doSomething
elapsedTimeSeconds: 1
doSomething
elapsedTimeSeconds: 1
doSomething
elapsedTimeSeconds: 1
doSomething
elapsedTimeSeconds: 1
doSomething
elapsedTimeSeconds: 1
실행 결과를 보면, 첫번째 허가의 경우 지연시간없이 바로 호출되는 것을 볼 수 있다.
2초에 1번 실행
describe("2초에 1번 실행 (1)") {
val rateLimiter = RateLimiter.create(1.0)
repeat(10) {
val startTime = ZonedDateTime.now().second
rateLimiter.acquire(2)
doSomething()
val elapsedTimeSeconds = ZonedDateTime.now().second - startTime
println("elapsedTimeSeconds: $elapsedTimeSeconds")
}
}
describe("2초에 1번 실행 (2)") {
val rateLimiter = RateLimiter.create(0.5)
repeat(10) {
val startTime = ZonedDateTime.now().second
rateLimiter.acquire(1)
doSomething()
val elapsedTimeSeconds = ZonedDateTime.now().second - startTime
println("elapsedTimeSeconds: $elapsedTimeSeconds")
}
}
위 예제처럼 2가지 표현으로 2초에 1번 실행되도록 설정할 수 있다.
tryAcquire()
describe("acquire()와 tryAcquire()의 동작 차이") {
val rateLimiter = RateLimiter.create(1.0)
val startTime = ZonedDateTime.now().second
rateLimiter.acquire(1)
val result = rateLimiter.tryAcquire()
val elapsedTimeSeconds = ZonedDateTime.now().second - startTime
// Then
result shouldBe false
println("elapsedTimeSeconds: $elapsedTimeSeconds")
}
- acquire()는 허가를 받을 때까지 blocking하며 기다린다.
- tryAcquire()는 허가를 요청하지만, 현재 허가를 받지 못하는 상황이면 false를 리턴한다.
tryAcquire()는 호출을 분산시킨다.
describe("tryAcquire()가 예상대로 작동안되는 경우") {
val rateLimiter = RateLimiter.create(100.0) // 1초에 100번 실행
repeat(10) {
val startTime = ZonedDateTime.now().second
if (rateLimiter.tryAcquire()) {
doSomething()
}
val elapsedTimeSeconds = ZonedDateTime.now().second - startTime
println("elapsedTimeSeconds: $elapsedTimeSeconds")
}
}
1초에 100번 실행하도록 설정했으므로, 10번 모두 허가를 얻고 doSomething()이 호출되기를 기대했다.
하지만 결과는 아래와 같았다.
doSomething
elapsedTimeSeconds: 0
elapsedTimeSeconds: 0
elapsedTimeSeconds: 0
elapsedTimeSeconds: 0
elapsedTimeSeconds: 0
elapsedTimeSeconds: 0
elapsedTimeSeconds: 0
elapsedTimeSeconds: 0
elapsedTimeSeconds: 0
elapsedTimeSeconds: 0
첫번째 호출 외에 나머지는 모두 tryAcquire()가 false를 리턴해서, doSomething()이 실행되지 않았다.
tryAcquire()는 1초에 100번 실행되도록 설정했음에도 불구하고, 연속적으로 호출하면 실행이 안되는 것을 확인할 수 있다.
물론, 실제 코드 적용 시엔 doSomething()처럼 가벼운 메소드를 호출할 것이 아니므로 위의 결과와 다를 수 있다.
위 코드에서 tryAcquire()의 대기시간을 주면 기대했던 것처럼 작동한다.
describe("tryAcquire() wait time 추가") {
val rateLimiter = RateLimiter.create(100.0) // 1초에 100번 실행
repeat(10) {
val startTime = ZonedDateTime.now().second
if (rateLimiter.tryAcquire(1, 100, TimeUnit.MILLISECONDS)) {
doSomething()
}
val elapsedTimeSeconds = ZonedDateTime.now().second - startTime
println("elapsedTimeSeconds: $elapsedTimeSeconds")
}
}
100ms 대기 시간을 주고 허가를 얻어오도록 설정했다.
결과는 아래와 같다.
doSomething
elapsedTimeSeconds: 0
doSomething
elapsedTimeSeconds: 0
doSomething
elapsedTimeSeconds: 0
doSomething
elapsedTimeSeconds: 0
doSomething
elapsedTimeSeconds: 0
doSomething
elapsedTimeSeconds: 0
doSomething
elapsedTimeSeconds: 0
doSomething
elapsedTimeSeconds: 0
doSomething
elapsedTimeSeconds: 0
doSomething
elapsedTimeSeconds: 0
이제 기대했던 것처럼 doSomething()이 10번 모두 실행되는 것을 확인할 수 있다.
마무리
위 예제처럼 간단하게 throttling할 수 있었지만, 한가지 아쉬운 점은 1초에 N번 실행되는 것을 미리 정해놔야 한다는 것이다.
서버 상황에 따라 유동적으로 throttling하는게 가장 좋은 방법일 것 같은데, 리서치를 좀 더 해봐야겠다.
- 예제 전체코드 : https://github.com/henry-jo/spring-guava-ratelimiter
'Programming > Java & JSP & Spring' 카테고리의 다른 글
JVM 메모리 누수 트러블슈팅 (Native memory leak) (1) | 2024.11.16 |
---|---|
[Spring] 외부 API 호출 서비스 테스트코드 작성하기 (WebClient & MockWebServer) (0) | 2022.03.13 |
[Spring boot] Hikari CP 적용 & 커넥션 누수 이슈 (0) | 2022.02.01 |
[Spring] REQUIRES_NEW과 Exception (REQUIRES_NEW는 정말 독립적인가?) (0) | 2020.10.17 |
나만의 코딩컨벤션 작성하기(Spring, Java, Naming, 구조, 코드 작성법 등) (2) | 2020.04.04 |