Skip to content
Server Plugin

속도 제한(Rate limiting

[//]: # (title: 속도 제한(Rate limiting))

필수 의존성: io.ktor:ktor-server-rate-limit

코드 예제: rate-limit

Native 서버
Ktor는 Kotlin/Native를 지원하며 추가 런타임이나 가상 머신 없이 서버를 실행할 수 있게 해줍니다.
지원: ✅

RateLimit 플러그인을 사용하면 클라이언트가 일정 시간 동안 보낼 수 있는 요청 수를 제한할 수 있습니다. Ktor는 속도 제한을 구성하기 위한 다양한 수단을 제공합니다. 예를 들어:

  • 애플리케이션 전체에 전역적으로 속도 제한을 활성화하거나, 서로 다른 리소스에 대해 각기 다른 속도 제한을 구성할 수 있습니다.
  • IP 주소, API 키 또는 액세스 토큰 등 특정 요청 파라미터를 기반으로 속도 제한을 구성할 수 있습니다.

의존성 추가

RateLimit을 사용하려면 빌드 스크립트에 ktor-server-rate-limit 아티팩트를 포함해야 합니다:

Kotlin
Groovy
XML

RateLimit 설치

RateLimit 플러그인을 애플리케이션에 설치하려면, 지정된

모듈
모듈을 사용하면 경로를 그룹화하여 애플리케이션을 구조화할 수 있습니다.
install 함수에 전달하세요. 아래의 코드 스니펫은 RateLimit을 설치하는 방법을 보여줍니다...

  • ... embeddedServer 함수 호출 내부에서 설치하는 방법.
  • ... Application 클래스의 확장 함수인 명시적으로 정의된 module 내부에서 설치하는 방법.
kotlin
kotlin

RateLimit 구성

개요

Ktor는 속도 제한을 위해 토큰 버킷(token bucket) 알고리즘을 사용하며, 다음과 같이 작동합니다:

  1. 처음에는 용량(토큰 수)으로 정의된 버킷이 있습니다.
  2. 들어오는 각 요청은 버킷에서 토큰 하나를 소비하려고 시도합니다:
    • 용량이 충분하면 서버는 요청을 처리하고 다음 헤더와 함께 응답을 보냅니다:
      • X-RateLimit-Limit: 지정된 버킷 용량.
      • X-RateLimit-Remaining: 버킷에 남아 있는 토큰 수.
      • X-RateLimit-Reset: 버킷이 다시 채워지는 시간을 지정하는 UTC 타임스탬프(초 단위).
    • 용량이 부족하면 서버는 429 Too Many Requests 응답을 사용하여 요청을 거부하고, 클라이언트가 다음 요청을 보내기까지 기다려야 하는 시간(초 단위)을 나타내는 Retry-After 헤더를 추가합니다.
  3. 지정된 시간이 지나면 버킷 용량이 다시 채워집니다.

속도 제한기 등록

Ktor를 사용하면 애플리케이션 전체에 전역적으로 속도 제한을 적용하거나 특정 경로에 적용할 수 있습니다:

  • 애플리케이션 전체에 속도 제한을 적용하려면 global 메서드를 호출하고 구성된 속도 제한기를 전달하세요.

    kotlin
    install(RateLimit) {
        global {
            rateLimiter(limit = 5, refillPeriod = 60.seconds)
        }
    }
  • register 메서드는 특정 경로에 적용할 수 있는 속도 제한기를 등록합니다.

    kotlin
    install(RateLimit) {
        register {
            rateLimiter(limit = 5, refillPeriod = 60.seconds)
        }
    }

위의 코드 샘플은 RateLimit 플러그인에 대한 최소한의 구성을 보여주지만, register 메서드를 사용하여 등록된 속도 제한기의 경우 특정 경로에도 이를 적용해야 합니다.

속도 제한 구성

이 섹션에서는 속도 제한을 구성하는 방법을 살펴보겠습니다:

  1. (선택 사항) register 메서드를 사용하면 특정 경로에 속도 제한 규칙을 적용하는 데 사용할 수 있는 속도 제한기 이름을 지정할 수 있습니다:

    kotlin
        install(RateLimit) {
            register(RateLimitName("protected")) {
                // ...
            }
        }
  2. rateLimiter 메서드는 두 개의 파라미터로 속도 제한기를 생성합니다: limit은 버킷 용량을 정의하고, refillPeriod는 이 버킷의 리필 주기를 지정합니다. 아래 예제의 속도 제한기는 분당 30개의 요청을 처리할 수 있도록 허용합니다:

    kotlin
    register(RateLimitName("protected")) {
        rateLimiter(limit = 30, refillPeriod = 60.seconds)
    }
  3. (선택 사항) requestKey를 사용하면 요청에 대한 키를 반환하는 함수를 지정할 수 있습니다. 키가 서로 다른 요청은 독립적인 속도 제한을 갖습니다. 아래 예제에서는 login 쿼리 파라미터를 키로 사용하여 서로 다른 사용자를 구분합니다:

    kotlin
    register(RateLimitName("protected")) {
        requestKey { applicationCall ->
            applicationCall.request.queryParameters["login"]!!
        }
    }

    키는 equalshashCode가 잘 구현되어 있어야 합니다.

  4. (선택 사항) requestWeight는 요청에 의해 소비되는 토큰 수를 반환하는 함수를 설정합니다. 아래 예제에서는 요청 키를 사용하여 요청 가중치를 구성합니다:

    kotlin
    register(RateLimitName("protected")) {
        requestKey { applicationCall ->
            applicationCall.request.queryParameters["login"]!!
        }
        requestWeight { applicationCall, key ->
            when(key) {
                "jetbrains" -> 1
                else -> 2
            }
        }
    }
  5. (선택 사항) modifyResponse를 사용하면 각 요청과 함께 전송되는 기본 X-RateLimit-* 헤더를 오버라이드할 수 있습니다:

    kotlin
    register(RateLimitName("protected")) {
        modifyResponse { applicationCall, state ->
            applicationCall.response.header("X-RateLimit-Custom-Header", "Some value")
        }
    }

속도 제한 범위 정의

속도 제한기를 구성한 후에는 rateLimit 메서드를 사용하여 특정 경로에 해당 규칙을 적용할 수 있습니다:

kotlin
routing {
    rateLimit {
        get("/") {
            val requestsLeft = call.response.headers["X-RateLimit-Remaining"]
            call.respondText("Welcome to the home page! $requestsLeft requests left.")
        }
    }
}

이 메서드는 속도 제한기 이름을 인자로 받을 수도 있습니다:

kotlin
routing {
    rateLimit(RateLimitName("protected")) {
        get("/protected-api") {
            val requestsLeft = call.response.headers["X-RateLimit-Remaining"]
            val login = call.request.queryParameters["login"]
            call.respondText("Welcome to protected API, $login! $requestsLeft requests left.")
        }
    }
}

예제

아래 코드 샘플은 RateLimit 플러그인을 사용하여 서로 다른 리소스에 각기 다른 속도 제한기를 적용하는 방법을 보여줍니다. StatusPages 플러그인은 429 Too Many Requests 응답이 전송된 거부된 요청을 처리하는 데 사용됩니다.

kotlin
package com.example

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.ratelimit.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlin.time.Duration.Companion.seconds

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module() {
    install(RateLimit) {
        register {
            rateLimiter(limit = 5, refillPeriod = 60.seconds)
        }
        register(RateLimitName("public")) {
            rateLimiter(limit = 10, refillPeriod = 60.seconds)
        }
        register(RateLimitName("protected")) {
            rateLimiter(limit = 30, refillPeriod = 60.seconds)
            requestKey { applicationCall ->
                applicationCall.request.queryParameters["login"]!!
            }
            requestWeight { applicationCall, key ->
                when(key) {
                    "jetbrains" -> 1
                    else -> 2
                }
            }
        }
    }
    install(StatusPages) {
        status(HttpStatusCode.TooManyRequests) { call, status ->
            val retryAfter = call.response.headers["Retry-After"]
            call.respondText(text = "429: Too many requests. Wait for $retryAfter seconds.", status = status)
        }
    }
    routing {
        rateLimit {
            get("/") {
                val requestsLeft = call.response.headers["X-RateLimit-Remaining"]
                call.respondText("Welcome to the home page! $requestsLeft requests left.")
            }
        }
        rateLimit(RateLimitName("public")) {
            get("/public-api") {
                val requestsLeft = call.response.headers["X-RateLimit-Remaining"]
                call.respondText("Welcome to public API! $requestsLeft requests left.")
            }
        }
        rateLimit(RateLimitName("protected")) {
            get("/protected-api") {
                val requestsLeft = call.response.headers["X-RateLimit-Remaining"]
                val login = call.request.queryParameters["login"]
                call.respondText("Welcome to protected API, $login! $requestsLeft requests left.")
            }
        }
    }
}

전체 예제는 여기에서 확인할 수 있습니다: rate-limit.