Jamie the programmer

Go에서 Rate Limiting으로 공격 방어하기: Token Bucket vs Leaky Bucket 본문

programming/golang

Go에서 Rate Limiting으로 공격 방어하기: Token Bucket vs Leaky Bucket

jamie91 2025. 3. 10. 11:10
Contents 접기
반응형

 
 
대규모 트래픽을 다루는 서버나 API에서는 Rate Limiting(속도 제한)이 매우 중요하다.
보안 관점에서도, 무차별 암호 대입 공격(Brute Force)이나 Dos/DDoS 공격과 같은 악의적 트래픽을 효과적으로 제한하기 위해서는 Rate Limiting이 필수적이다.
또한, 계정 도용 방지API Key 남용 방지자원(네트워크, CPU, DB 등) 보호 등 다양한 측면에서 보안 강화를 기대할 수 있다.
Go 언어에서는 표준 라이브러리에 포함된 확장 패키지 golang.org/x/time/rate와, 오픈소스로 제공되는 go.uber.org/ratelimit 라이브러리가 대표적이다. 이 글에서는 이 두 라이브러리가 구현하고 있는 대표적인 알고리즘인 Token Bucket과 Leaky Bucket에 대해 간단히 살펴보며, 보안적으로 어떤 이점을 얻을 수 있는지 알아보자.


1. Go에서의 Rate Limit

1.1 golang.org/x/time/rate

  • Token Bucket 알고리즘을 기반으로 구현된 Rate Limiting 라이브러리
  • API 요청 횟수를 초당 몇 건으로 제한하고, 동시에 몇 건의 버스트(순간적인 폭주)를 허용할지 쉽게 설정 가능
  • 보안 측면: 공격자가 짧은 시간에 대량의 요청을 보내도, Token Bucket에 의해 초당 처리 가능한 요청 수가 제한되므로 Brute Force, Dos 공격을 어느 정도 방어 가능

1.2 go.uber.org/ratelimit

  • Leaky Bucket 알고리즘을 기반으로 구현된 Rate Limiting 라이브러리
  • 물이 샌다고 가정한 양동이(버킷)에 트래픽을 넣고 일정 속도로 빠져나가도록 하여, 갑작스러운 대규모 트래픽(공격 트래픽 포함)을 제한
  • 보안 측면: 안정적으로 일정 속도로만 요청을 처리하므로, 분산된 Dos/DDoS 공격에도 서버가 급격히 과부하되지 않도록 보호

참고


2. Token Bucket Algorithm

Token Bucket 알고리즘은 시간에 따라 일정 속도로 토큰을 충전해두고, 요청 시 토큰을 소모하는 방식으로 동작한다. 아래 그림처럼, 일정 간격으로 버킷에 토큰이 추가되고, 요청이 들어오면 토큰을 하나씩 사용한다. 버킷이 비어 있으면 토큰이 재충전될 때까지 대기하거나, 요청이 거부된다.
 

Token Bucket의 핵심 개념

  1. 토큰 버킷(Token Bucket)
    • 버킷의 깊이(용량)를 b라고 할 때, 버킷은 최대 b개의 토큰을 보유
    • 버킷이 가득 차 있을 때 새 토큰이 들어오면 버려짐
  2. 토큰 충전 속도(r)
    • 1초에 r개씩 토큰을 충전한다고 가정
    • 예: 초당 5개 토큰 = 초당 5회 요청 허용
  3. 요청 처리
    • 리소스에 접근하려면 토큰 1개가 필요
    • 버킷에 남아 있는 토큰이 있으면 즉시 요청을 처리(토큰 제거)
    • 버킷이 비어 있으면 요청을 대기열에 두거나 거부
  4. 버스트 처리
    • 짧은 시간에 몰리는 트래픽(버스트)을 어느 정도 흡수 가능
    • 버킷에 토큰이 남아 있으면, 순간적으로 많은 요청이 와도 빠르게 처리

보안적 이점

  • Brute Force 방어: 초당 N회 이상의 시도가 불가능하므로, 대규모 비밀번호 시도 등 무차별 대입 공격 완화
  • Dos/DDoS 완화: 순간 폭주 트래픽을 제한하여 서버가 급격히 다운되는 상황 방지
  • 자원 보호: 토큰이 없으면 더 이상 요청을 허용하지 않으므로, 데이터베이스나 내부 서비스 과부하를 줄임

3. Leaky Bucket Algorithm

Token Bucket과 자주 비교되는 알고리즘으로, go.uber.org/ratelimit가 채택한 Leaky Bucket 방식이 있다.

  • Leaky Bucket은 버킷에 물(요청)이 담기면 일정한 속도로 물이 빠져나가는 구조를 가정
  • Token Bucket이 “토큰이 충전되어야 요청을 처리할 수 있음”이라면, Leaky Bucket은 “들어오는 요청(물)은 일단 버킷에 쌓이고, 일정 속도로 빠져나가므로 그 속도를 초과하면 버킷이 넘쳐 요청이 버려짐”에 가까움

보안적 이점

  • 일정 처리 속도 보장: 트래픽이 한꺼번에 몰려와도 일정 속도로만 빠져나가므로, 서버 자원이 고갈되지 않도록 방어
  • 분산 공격 방어: 대규모로 분산된 Dos/DDoS 공격에도, 버킷이 제한된 양만 처리하고 나머지는 버려서 시스템 다운을 예방

4. 간단 예시: golang.org/x/time/rate로 Token Bucket 구현

package main

import (
    "fmt"
    "time"

    "golang.org/x/time/rate"
)

func main() {
    // 초당 2개 요청, 버스트는 3개까지 허용
    limiter := rate.NewLimiter(2, 3)

    for i := 1; i <= 10; i++ {
        // Wait는 토큰이 생길 때까지 대기 (Context 없이 사용하면 Background context)
        err := limiter.Wait(nil)
        if err != nil {
            fmt.Println("Error:", err)
            continue
        }
        fmt.Printf("Request #%d allowed at %v\\\\n", i, time.Now())
    }
}

  • rate.NewLimiter(r, b)에서 r은 초당 토큰 충전 개수, b는 버킷 용량(버스트 허용량)을 의미
  • Wait() 함수는 토큰이 없으면 자동으로 대기 후 처리
  • 보안적으로도, 초당 요청 횟수를 엄격히 제한하므로 API Key 오남용이나 대규모 트래픽 유입 시에도 서버가 급격히 과부하되지 않는다.

5. Rate Limiting 구현 시 주의사항

  1. 정확한 시간 측정
    • OS 스케줄링이나 고루틴 스위칭으로 인해 완벽히 초당 N회 처리만 보장하기는 어려울 수 있다.
    • 약간의 오차 보정 또는 버퍼를 두어 운영하는 것이 안정적이다.
  2. 오류(429 Too Many Requests) 처리
    • 클라이언트가 429 오류를 받을 때 재시도 정책을 안내하거나, 쿨다운(cooldown) 시간을 명시하는 등 사용자 경험도 고려
  3. 추가적인 보안 로직
    • Rate Limiting만으로 모든 공격을 차단할 수는 없다.
    • IP 블랙리스트, WAF(Web Application Firewall), 모니터링 & 로깅, 알림 시스템 등과 연계해 다층 보안을 구축하는 것이 좋다.

6. 마무리

  • Rate Limiting은 서버 안정성과 보안을 위해 필수적인 기법이다.
  • Go 언어에서는 표준 라이브러리 확장 모듈인 golang.org/x/time/rate로 Token Bucket 방식을 간단히 적용할 수 있으며, go.uber.org/ratelimit를 통해 Leaky Bucket 알고리즘을 활용할 수도 있다.
  • Brute Force나 Dos/DDoS 공격, API Key 남용 등을 막기 위해서는 Rate Limiting이 강력한 1차 방어 수단이 된다.
  • 실제 운영 환경에서는 트래픽 패턴, 서버 자원, 사용자 요구사항 등을 종합적으로 고려해 버킷 크기(b), 토큰 충전 속도(r) 등의 파라미터를 설정해야 하며, 추가 보안 레이어와 함께 사용하면 더 효과적이다.

참고 자료

728x90
반응형