> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 레이트 리밋 커스터마이즈

> Laravel의 RateLimiter 파사드를 심층 분석하고, 사용자별·플랜별·IP 주소별 커스텀 레이트 리밋의 구현부터, Redis를 사용한 고가용성 설정까지 해설합니다.

## `RateLimiter` 파사드의 구조

Laravel의 레이트 리밋은 `Illuminate\Cache\RateLimiting\Limit` 클래스와 `RateLimiter` 파사드로 구성되어 있습니다. 내부적으로는 캐시 드라이버(기본은 파일 또는 Redis)에 카운터를 보관하고, 요청 수를 추적합니다.

`throttle` 미들웨어가 받은 요청에 대해 `RateLimiter::for()`에서 정의한 클로저를 실행하고, 제한에 도달했다면 `429 Too Many Requests`를 반환합니다.

## `AppServiceProvider`에서의 커스텀 리미터 정의

레이트 리밋의 설정은 `App\Providers\AppServiceProvider`의 `boot()` 메서드에서 합니다.

```php theme={null}
<?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 주소별·플랜별 레이트 리밋

### 인증된 사용자와 게스트로 제한을 다르게 함

```php theme={null}
RateLimiter::for('uploads', function (Request $request) {
    return $request->user()
        ? Limit::perHour(100)->by($request->user()->id)
        : Limit::perHour(10)->by($request->ip());
});
```

### 사용자의 플랜에 따른 제한

```php theme={null}
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 주소 단위로 스로틀링합니다.

```php theme={null}
RateLimiter::for('global', function (Request $request) {
    return Limit::perMinute(1000)->by($request->ip());
});
```

### 여러 제한을 조합함

배열로 반환하면, 모든 제한이 평가됩니다. 어느 하나에 도달한 시점에서 `429`를 반환합니다.

```php theme={null}
RateLimiter::for('login', function (Request $request) {
    return [
        Limit::perMinute(10)->by($request->ip()),
        Limit::perMinute(5)->by($request->input('email')),
    ];
});
```

<Info>
  같은 `by` 값을 갖는 여러 제한을 정의하는 경우에는, 키가 충돌하지 않도록 프리픽스를 붙여 주세요.
</Info>

```php theme={null}
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` 미들웨어에 정의한 리미터명을 전달합니다.

```php theme={null}
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`에서 관리합니다.

```php theme={null}
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 라우트에의 적용 예

<Steps>
  <Step title="리미터를 정의한다">
    `AppServiceProvider`에 여러 리미터를 정의합니다.

    ```php theme={null}
    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')),
            ];
        });
    }
    ```
  </Step>

  <Step title="라우트에 미들웨어를 적용한다">
    ```php theme={null}
    // 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']);
    });
    ```
  </Step>
</Steps>

## 응답 헤더(`X-RateLimit-*`)의 구조

`throttle` 미들웨어는 제한 정보를 응답 헤더에 자동 부여합니다.

| 헤더                      | 설명                            |
| ----------------------- | ----------------------------- |
| `X-RateLimit-Limit`     | 허가되어 있는 요청 수                  |
| `X-RateLimit-Remaining` | 남은 요청 수                       |
| `Retry-After`           | 다음 요청이 가능해질 때까지의 초 수(429 시에만) |
| `X-RateLimit-Reset`     | 제한이 리셋되는 UNIX 타임스탬프           |

```http theme={null}
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."
}
```

### 커스텀 응답을 반환한다

```php theme={null}
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()`를 사용합니다.

```php theme={null}
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を送信しました。']);
    }
}
```

### 시도 횟수의 확인과 리셋

```php theme={null}
// 現在の試行回数を取得
$hits = RateLimiter::attempts($key);

// 次のリセットまでの秒数
$seconds = RateLimiter::availableIn($key);

// 制限に達しているか確認
$tooMany = RateLimiter::tooManyAttempts($key, $maxAttempts = 5);

// カウンターを手動でリセット（ログアウト後など）
RateLimiter::clear($key);
```

### 로그인 스로틀링의 예

```php theme={null}
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 응답만 카운트함으로써 리소스 열거 공격을 방지하는 예:

```php theme={null}
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 드라이버의 설정

```php theme={null}
// config/cache.php
'default' => env('CACHE_DRIVER', 'redis'),
```

```ini theme={null}
# .env
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
```

### `throttleWithRedis`를 사용한다

Redis 전용의 최적화된 스로틀링 미들웨어를 사용하려면 `bootstrap/app.php`에서 `throttleWithRedis()`를 호출합니다.

```php theme={null}
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의 아토믹 조작을 사용해 정확한 카운트가 이루어집니다.

<Warning>
  `throttleWithRedis()`를 사용하는 경우에는 반드시 Redis가 이용 가능한 상태로 해 주세요. Redis에의 접속이 실패하면, 요청이 모두 거부될 가능성이 있습니다.
</Warning>

### Redis를 사용하는 이점

* **수평 스케일링 대응** — 여러 서버 인스턴스 간에서 카운터를 공유할 수 있음
* **고정밀도** — 아토믹 조작으로 레이스 컨디션을 방지
* **TTL 관리** — Redis의 네이티브한 유효 기한 기능으로 카운터를 자동 삭제

## 관련 페이지

<Card title="캐시" icon="database" href="/ko/cache">
  Redis를 포함한 Laravel의 캐시 드라이버의 설정과 사용법을 확인합니다.
</Card>


## Related topics

- [Laravel Pulse](/ko/pulse.md)
- [HTTP 테스트](/ko/http-tests.md)
- [Laravel Pennant](/ko/pennant.md)
- [Laravel Sail](/ko/sail.md)
- [Socialite - Laravel Bluesky](/ko/packages/laravel-bluesky/socialite.md)
