RateLimiter 파사드의 구조
Laravel의 레이트 리밋은 Illuminate\Cache\RateLimiting\Limit 클래스와 RateLimiter 파사드로 구성되어 있습니다. 내부적으로는 캐시 드라이버(기본은 파일 또는 Redis)에 카운터를 보관하고, 요청 수를 추적합니다.
throttle 미들웨어가 받은 요청에 대해 RateLimiter::for()에서 정의한 클로저를 실행하고, 제한에 도달했다면 429 Too Many Requests를 반환합니다.
AppServiceProvider에서의 커스텀 리미터 정의
레이트 리밋의 설정은 App\Providers\AppServiceProvider의 boot() 메서드에서 합니다.
<? php
namespace App\Providers ;
use Illuminate\Cache\RateLimiting\ Limit ;
use Illuminate\Http\ Request ;
use Illuminate\Support\Facades\ RateLimiter ;
use Illuminate\Support\ ServiceProvider ;
class AppServiceProvider extends ServiceProvider
{
public function boot () : void
{
RateLimiter :: for ( 'api' , function ( Request $request ) {
return Limit :: perMinute ( 60 ) -> by ( $request -> user () ?-> id ?: $request -> ip ());
});
}
}
RateLimiter::for()의 제1인수는 리미터명으로, throttle 미들웨어에서 참조할 때 사용합니다. 제2인수의 클로저는 Illuminate\Cache\RateLimiting\Limit 인스턴스를 반환해야 합니다.
사용자별·IP 주소별·플랜별 레이트 리밋
인증된 사용자와 게스트로 제한을 다르게 함
RateLimiter :: for ( 'uploads' , function ( Request $request ) {
return $request -> user ()
? Limit :: perHour ( 100 ) -> by ( $request -> user () -> id )
: Limit :: perHour ( 10 ) -> by ( $request -> ip ());
});
사용자의 플랜에 따른 제한
RateLimiter :: for ( 'api' , function ( Request $request ) {
$user = $request -> user ();
if ( ! $user ) {
return Limit :: perMinute ( 30 ) -> by ( $request -> ip ());
}
return match ( $user -> plan ) {
'enterprise' => Limit :: none (),
'pro' => Limit :: perMinute ( 500 ) -> by ( $user -> id ),
default => Limit :: perMinute ( 60 ) -> by ( $user -> id ),
};
});
IP 주소에 의한 글로벌 제한
특정 엔드포인트와 관계없이, IP 주소 단위로 스로틀링합니다.
RateLimiter :: for ( 'global' , function ( Request $request ) {
return Limit :: perMinute ( 1000 ) -> by ( $request -> ip ());
});
여러 제한을 조합함
배열로 반환하면, 모든 제한이 평가됩니다. 어느 하나에 도달한 시점에서 429를 반환합니다.
RateLimiter :: for ( 'login' , function ( Request $request ) {
return [
Limit :: perMinute ( 10 ) -> by ( $request -> ip ()),
Limit :: perMinute ( 5 ) -> by ( $request -> input ( 'email' )),
];
});
같은 by 값을 갖는 여러 제한을 정의하는 경우에는, 키가 충돌하지 않도록 프리픽스를 붙여 주세요.
RateLimiter :: for ( 'uploads' , function ( Request $request ) {
return [
Limit :: perMinute ( 10 ) -> by ( 'minute:' . $request -> user () -> id ),
Limit :: perDay ( 1000 ) -> by ( 'day:' . $request -> user () -> id ),
];
});
throttle 미들웨어와 커스텀 리미터명의 지정
throttle 미들웨어에 정의한 리미터명을 전달합니다.
use Illuminate\Support\Facades\ Route ;
Route :: middleware ([ 'throttle:api' ]) -> group ( function () {
Route :: get ( '/user' , function () { /* ... */ });
Route :: post ( '/posts' , function () { /* ... */ });
});
bootstrap/app.php에서의 등록
Laravel 11 이후, 미들웨어는 bootstrap/app.php에서 관리합니다.
use Illuminate\Foundation\ Application ;
return Application :: configure ( basePath : dirname ( __DIR__ ))
-> withRouting (
web : __DIR__ . '/../routes/web.php' ,
api : __DIR__ . '/../routes/api.php' ,
apiPrefix : 'api' ,
)
-> withMiddleware ( function ( \Illuminate\Foundation\Configuration\ Middleware $middleware ) : void {
$middleware -> throttleApi ( 'api' );
})
-> create ();
API 라우트에의 적용 예
리미터를 정의한다
AppServiceProvider에 여러 리미터를 정의합니다.public function boot () : void
{
// 一般APIアクセス
RateLimiter :: for ( 'api' , function ( Request $request ) {
return Limit :: perMinute ( 60 ) -> by ( $request -> user () ?-> id ?: $request -> ip ());
});
// ファイルアップロード
RateLimiter :: for ( 'uploads' , function ( Request $request ) {
return $request -> user () ?-> isPro ()
? Limit :: perHour ( 500 ) -> by ( $request -> user () -> id )
: Limit :: perHour ( 50 ) -> by ( $request -> user () ?-> id ?: $request -> ip ());
});
// ログイン試行
RateLimiter :: for ( 'login' , function ( Request $request ) {
return [
Limit :: perMinute ( 10 ) -> by ( $request -> ip ()),
Limit :: perMinute ( 5 ) -> by ( $request -> input ( 'email' )),
];
});
}
라우트에 미들웨어를 적용한다
// routes/api.php
use Illuminate\Support\Facades\ Route ;
Route :: middleware ([ 'auth:sanctum' , 'throttle:api' ]) -> group ( function () {
Route :: get ( '/user' , [ \App\Http\Controllers\ UserController :: class , 'show' ]);
Route :: get ( '/posts' , [ \App\Http\Controllers\ PostController :: class , 'index' ]);
});
Route :: middleware ([ 'auth:sanctum' , 'throttle:uploads' ]) -> group ( function () {
Route :: post ( '/uploads' , [ \App\Http\Controllers\ UploadController :: class , 'store' ]);
});
Route :: middleware ([ 'throttle:login' ]) -> group ( function () {
Route :: post ( '/login' , [ \App\Http\Controllers\ AuthController :: class , 'login' ]);
});
응답 헤더(X-RateLimit-*)의 구조
throttle 미들웨어는 제한 정보를 응답 헤더에 자동 부여합니다.
헤더 설명 X-RateLimit-Limit허가되어 있는 요청 수 X-RateLimit-Remaining남은 요청 수 Retry-After다음 요청이 가능해질 때까지의 초 수(429 시에만) X-RateLimit-Reset제한이 리셋되는 UNIX 타임스탬프
HTTP / 1.1 429 Too Many Requests
X-RateLimit-Limit : 60
X-RateLimit-Remaining : 0
Retry-After : 45
X-RateLimit-Reset : 1717000000
Content-Type : application/json
{
"message" : "Too Many Requests."
}
커스텀 응답을 반환한다
RateLimiter :: for ( 'api' , function ( Request $request ) {
return Limit :: perMinute ( 60 )
-> by ( $request -> user () ?-> id ?: $request -> ip ())
-> response ( function ( Request $request , array $headers ) {
return response () -> json ([
'message' => 'リクエスト制限を超えました。しばらくしてから再試行してください。' ,
'retry_after' => $headers [ 'Retry-After' ],
], 429 , $headers );
});
});
RateLimiter::attempt()를 사용한 수동 체크
throttle 미들웨어를 사용하지 않고, 코드 안에서 임의의 타이밍에 레이트 리밋을 확인하고 싶은 경우에는 RateLimiter::attempt()를 사용합니다.
use Illuminate\Support\Facades\ RateLimiter ;
class SmsController extends Controller
{
public function send ( Request $request ) : \Illuminate\Http\ JsonResponse
{
$key = 'sms:' . $request -> user () -> id ;
$executed = RateLimiter :: attempt (
key : $key ,
maxAttempts : 5 ,
callback : function () use ( $request ) {
app ( SmsService :: class ) -> send (
$request -> user () -> phone ,
$request -> input ( 'message' )
);
},
decaySeconds : 3600 , // 1時間
);
if ( ! $executed ) {
$seconds = RateLimiter :: availableIn ( $key );
return response () -> json ([
'message' => "SMS送信の制限を超えました。{ $seconds }秒後に再試行してください。" ,
], 429 );
}
return response () -> json ([ 'message' => 'SMSを送信しました。' ]);
}
}
시도 횟수의 확인과 리셋
// 現在の試行回数を取得
$hits = RateLimiter :: attempts ( $key );
// 次のリセットまでの秒数
$seconds = RateLimiter :: availableIn ( $key );
// 制限に達しているか確認
$tooMany = RateLimiter :: tooManyAttempts ( $key , $maxAttempts = 5 );
// カウンターを手動でリセット(ログアウト後など)
RateLimiter :: clear ( $key );
로그인 스로틀링의 예
public function login ( Request $request ) : mixed
{
$key = 'login:' . $request -> input ( 'email' );
if ( RateLimiter :: tooManyAttempts ( $key , 5 )) {
$seconds = RateLimiter :: availableIn ( $key );
throw ValidationException :: withMessages ([
'email' => "ログイン試行回数が多すぎます。{ $seconds }秒後に再試行してください。" ,
]);
}
if ( ! Auth :: attempt ( $request -> only ( 'email' , 'password' ))) {
RateLimiter :: hit ( $key , 300 ); // 5分間カウント
throw ValidationException :: withMessages ([
'email' => 'メールアドレスまたはパスワードが正しくありません。' ,
]);
}
RateLimiter :: clear ( $key );
return redirect () -> intended ( '/dashboard' );
}
응답 기반의 레이트 리밋
특정 응답만 카운트하고 싶은 경우에는 after()를 사용합니다. 404 응답만 카운트함으로써 리소스 열거 공격을 방지하는 예:
use Symfony\Component\HttpFoundation\ Response ;
RateLimiter :: for ( 'resource-lookup' , function ( Request $request ) {
return Limit :: perMinute ( 10 )
-> by ( $request -> user () ?-> id ?: $request -> ip ())
-> after ( function ( Response $response ) {
return $response -> getStatusCode () === 404 ;
});
});
Redis를 사용한 레이트 리밋
기본 캐시 드라이버를 Redis로 변경하는 것만으로, throttle 미들웨어도 자동적으로 Redis를 사용합니다.
Redis 드라이버의 설정
// config/cache.php
'default' => env ( 'CACHE_DRIVER' , 'redis' ),
# .env
CACHE_DRIVER =redis
REDIS_HOST =127.0.0.1
REDIS_PORT =6379
throttleWithRedis를 사용한다
Redis 전용의 최적화된 스로틀링 미들웨어를 사용하려면 bootstrap/app.php에서 throttleWithRedis()를 호출합니다.
use Illuminate\Foundation\ Application ;
return Application :: configure ( basePath : dirname ( __DIR__ ))
-> withRouting (
web : __DIR__ . '/../routes/web.php' ,
api : __DIR__ . '/../routes/api.php' ,
apiPrefix : 'api' ,
)
-> withMiddleware ( function ( \Illuminate\Foundation\Configuration\ Middleware $middleware ) : void {
$middleware -> throttleWithRedis ();
})
-> create ();
이에 의해 throttle 미들웨어가 ThrottleRequestsWithRedis 클래스로 매핑되어, Redis의 아토믹 조작을 사용해 정확한 카운트가 이루어집니다.
throttleWithRedis()를 사용하는 경우에는 반드시 Redis가 이용 가능한 상태로 해 주세요. Redis에의 접속이 실패하면, 요청이 모두 거부될 가능성이 있습니다.
Redis를 사용하는 이점
수평 스케일링 대응 — 여러 서버 인스턴스 간에서 카운터를 공유할 수 있음
고정밀도 — 아토믹 조작으로 레이스 컨디션을 방지
TTL 관리 — Redis의 네이티브한 유효 기한 기능으로 카운터를 자동 삭제
관련 페이지
캐시 Redis를 포함한 Laravel의 캐시 드라이버의 설정과 사용법을 확인합니다.