서버 개발을 하다보면, 특정 행동의 주기를 제한해야 할 때가 있다.
일례로 외부 서비스의 API를 호출한다거나, 내부 서버의 외부의 공격으로 인해 부하가 너무 커지는 상황을 방지하기 위해 요청 수를 제한한다던가 하는 예가 있을 것이다.
이런 경우에 대개 사용할 수 있는 방법은 time.Sleep()
을 사용하는 것이다.
예를 들어 API를 1초 주기로 반복적으로 호출해야 한다고 하면 다음과 같은 식으로 코드를 작성할 수 있다.
for {
// ...
someAPICall()
time.Sleep(1 * time.Second)
// ...
}
다만 이런 방법은 항상 1초를 기다리게 만들기 때문에, 혼잡하지 않은 경우에도 API를 호출하는데 1초가 걸리게 된다.
rate
패키지 사용하기
좀 더 스마트하게 사용하는 방법으로는 golang.org/x/time
의 rate
패키지를 사용하는 것이다.
x
패키지는 Go가 만든 공식 라이브러리이긴 하나, 실험적인 패키지들, Go core와는 다르게 느슨한 호환성 요구사항으로 개발되는 패키지가 포함된다.
일례로 지금은 공식 패키지이지만, context.Context
도 이전엔 x
에 존재했으나, go 1.7부터 추가된 패키지이다.
rate
패키지는 Rate Limiter를 구현하기 위한 패키지로, 기본적으로 token bucket 알고리즘을 사용한다.
token bucket 알고리즘은 일정한 속도로 토큰을 생성하고, 요청이 들어올 때마다 토큰을 소비하는 방식으로 동작한다. 예를 들어 token 생성량이 1초에 2개이고, 버킷 용량이 30개라면, 처음 30개는 빠르게 사용하지만, 30개가 가득 차는 순간부터는 1초에 2개씩만 사용하게 되는 것이다.
백문이 불여일견! 먼저 x
패키지를 임포트 한다
import "golang.org/x/time/rate"
그리고 rate.NewLimiter()
를 통해 Rate Limiter를 생성한다.
var limiter = rate.NewLimiter(2, 1)
전역 변수로 생성하기도하고, 함수 내부에 선언해도 되지만, 함수 내부에서 사용할 경우 서버와 같은 경우라면 주입받아 사용하는 식으로 싱글톤으로 인스턴스를 유지해야 제대로 작동한다.
필자는 위의 예시에서 편의를 위해 전역 변수로 선언했다.
그리고 제한하는 곳 앞에 limiter.Wait()
를 호출하면 된다.
var limiter = rate.NewLimiter(1, 5)
func main() {
ctx := context.Background()
for range 10 {
if err := limiter.Wait(ctx); err != nil {
log.Fatal(err)
}
slog.InfoContext(ctx, "Requesting...")
}
}
결과는 아래와 같다.
2025/04/02 16:19:44 INFO Requesting...
2025/04/02 16:19:44 INFO Requesting...
2025/04/02 16:19:44 INFO Requesting...
2025/04/02 16:19:44 INFO Requesting...
2025/04/02 16:19:44 INFO Requesting...
2025/04/02 16:19:45 INFO Requesting...
2025/04/02 16:19:46 INFO Requesting...
2025/04/02 16:19:47 INFO Requesting...
2025/04/02 16:19:48 INFO Requesting...
2025/04/02 16:19:49 INFO Requesting...
로그를 살펴보면 처음 5개까지는 버킷에 용량이 있으니(5개) 빠르게 소비하지만, 이후 5개는 1초에 1개씩 차근차근 소비되는 것을 확인할 수 있다.