본문 바로가기

Programming/Java & JSP & Spring

[Spring] Guava RateLimiter 사용법 (spring throttling)

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

- 참조 : https://www.baeldung.com/guava-rate-limiter