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

# Lottery 클래스

> Illuminate\Support\Lottery를 사용해 확률적인 조작을 우아하게 구현하는 방법을 해설합니다. 패키지 개발에서의 유스케이스나 테스트 시의 고정화도 소개합니다.

## Lottery 클래스란

`Illuminate\Support\Lottery`는 확률 기반의 조작을 유창한 API로 표현할 수 있는 유틸리티 클래스입니다. "100 요청에 1번만 처리를 실행한다", "일부 요청만 상세 로그를 기록한다"와 같은 패턴을 심플하게 기술할 수 있습니다.

<Info>
  구현은 `src/Illuminate/Support/Lottery.php`에 있습니다. Laravel은 이 클래스를 Session GC나 캐시 락의 prune 등, 프레임워크 내부에서도 활용하고 있습니다.
</Info>

```mermaid theme={null}
flowchart LR
  A["Lottery::odds(1, 100)"] --> B{"랜덤 판정"}
  B -->|"당첨 (1%)"| C["winner 콜백 실행"]
  B -->|"낙첨 (99%)"| D["loser 콜백 실행"]
```

## 기본적인 사용법

### 정수 비율로 확률 지정하기

`Lottery::odds($chances, $outOf)`로 "$outOf 회 중 $chances 회 당첨"이라는 확률을 지정합니다.

```php theme={null}
use Illuminate\Support\Lottery;

Lottery::odds(1, 100)           // 100回に1回
    ->winner(fn () => $this->runMaintenance())
    ->loser(fn () => null)
    ->choose();
```

### 소수로 확률 지정하기

`$outOf`를 생략하고 `0.0`\~`1.0`의 소수를 전달하면, 그대로 확률로 사용됩니다.

```php theme={null}
Lottery::odds(0.01)             // 1% の確率
    ->winner(fn () => $this->sample())
    ->choose();
```

<Warning>
  소수 지정 시 값이 `1.0`을 넘으면 `RuntimeException`이 던져집니다.
</Warning>

### 콜백 없이 진위값 반환하기

winner / loser를 설정하지 않은 경우, `choose()`는 당첨이면 `true`, 낙첨이면 `false`를 반환합니다.

```php theme={null}
$shouldSample = Lottery::odds(1, 50)->choose(); // bool
```

### 여러 번 실행하기

`choose($times)`에 횟수를 전달하면 결과의 배열이 반환됩니다.

```php theme={null}
$results = Lottery::odds(1, 2)
    ->winner(fn () => 'win')
    ->loser(fn () => 'lose')
    ->choose(10);

// 例: ['win', 'lose', 'win', 'win', 'lose', ...]
```

### Callable로서 전달하기

`Lottery` 인스턴스는 `__invoke`를 구현하고 있으므로, callable을 받는 API에 직접 전달할 수 있습니다.

```php theme={null}
// DB::whenQueryingForLongerThan の第二引数として渡す例
DB::whenQueryingForLongerThan(
    Interval::seconds(5),
    Lottery::odds(1, 5)->winner(function ($connection) {
        // スロークエリを検知したとき 1/5 の確率でアラート送信
        Alert::send("Slow query on {$connection->getName()}");
    })
);
```

## 실전적인 유스케이스

### 1. 캐시 prune(100회에 1번만 실행)

기한이 지난 레코드 삭제 등, 매번 실행할 필요가 없는 유지보수 처리에 최적입니다.

```php theme={null}
Lottery::odds(1, 100)
    ->winner(fn () => Cache::store('database')->flush())
    ->choose();
```

### 2. 텔레메트리·샘플링(일부 요청만 상세 로그)

전체 요청을 로그에 남기면 비용이 높은 경우, 샘플링에 사용할 수 있습니다.

```php theme={null}
Lottery::odds(1, 20)
    ->winner(function () use ($request) {
        Log::channel('telemetry')->info('Request sampled', [
            'url'      => $request->url(),
            'duration' => microtime(true) - LARAVEL_START,
            'memory'   => memory_get_peak_usage(true),
        ]);
    })
    ->choose();
```

### 3. A/B 테스트적인 동작

사용자를 확률적으로 2개의 코드 패스로 나눕니다.

```php theme={null}
$result = Lottery::odds(1, 2)
    ->winner(fn ($user) => $this->newCheckoutFlow($user))
    ->loser(fn ($user) => $this->legacyCheckoutFlow($user))
    ->choose();
```

### 4. Scheduler의 보조로 정기 태스크를 랜덤 실행

여러 서버에서 중복 실행을 피하면서, 어떤 태스크를 랜덤하게 실행하고 싶을 때 사용할 수 있습니다.

```php theme={null}
// app/Console/Kernel.php
$schedule->call(function () {
    Lottery::odds(1, 3)
        ->winner(fn () => Artisan::call('cache:prune-stale-tags'))
        ->choose();
})->everyMinute();
```

## Laravel 프레임워크 내에서의 확률적 패턴

Laravel은 프레임워크 내부에서도 확률적인 유지보수 처리를 널리 사용하고 있습니다. 일부 구현은 `Lottery` 클래스가 추가되기 전에 작성되었기 때문에 `random_int()`를 직접 사용하고 있지만, 같은 사상에 기반합니다.

<Steps>
  <Step title="Session: 가비지 컬렉션">
    `Illuminate\Session\Middleware\StartSession::configHitsLottery()`는 `config/session.php`의 `lottery` 설정을 사용해 `random_int`로 확률 판정하여 GC를 실행합니다.

    ```php theme={null}
    // config/session.php
    'lottery' => [2, 100], // 100リクエストに2回

    // StartSession 内部 (random_int を直接使用)
    protected function configHitsLottery(array $config): bool
    {
        return random_int(1, $config['lottery'][1]) <= $config['lottery'][0];
    }
    ```
  </Step>

  <Step title="DatabaseLock: 기한 만료 락 prune">
    `Illuminate\Cache\DatabaseLock::acquire()`는 락 획득 때마다 동일한 비율 패턴으로 기한 만료 락을 삭제합니다.

    ```php theme={null}
    // config/cache.php (database ドライバー)
    'lock_lottery' => [2, 100], // 100回に2回プルーン実行

    // DatabaseLock::acquire() 内部 (random_int を直接使用)
    if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
        $this->pruneExpiredLocks();
    }
    ```
  </Step>

  <Step title="DB::whenQueryingForLongerThan — Lottery 클래스를 전달하는 예">
    `Lottery` 인스턴스는 callable로 전달할 수 있으므로, 슬로우 쿼리 감지 콜백에 직접 사용할 수 있습니다.

    ```php theme={null}
    DB::whenQueryingForLongerThan(
        Interval::seconds(5),
        Lottery::odds(1, 5)->winner(function ($connection) {
            Log::warning("Slow query on {$connection->getName()}");
        })
    );
    ```
  </Step>
</Steps>

<Tip>
  Session이나 DatabaseLock이 `random_int()`를 직접 사용하는 것에 비해, `Lottery` 클래스를 사용하면 `alwaysWin()` / `alwaysLose()` / `fix()`로 테스트 시에 결과를 제어할 수 있는 이점이 있습니다. 패키지 개발에서는 `Lottery` 클래스를 선택하면 테스트 용이성이 향상됩니다.
</Tip>

## 테스트 시의 이용

랜덤성이 있는 코드의 테스트에는 `Lottery`가 제공하는 테스트용 API를 사용합니다.

### `Lottery::alwaysWin()` — 항상 당첨시키기

```php theme={null}
public function test_maintenance_runs_on_win(): void
{
    $ranMaintenance = false;

    Lottery::alwaysWin(function () use (&$ranMaintenance) {
        Lottery::odds(1, 100)
            ->winner(function () use (&$ranMaintenance) {
                $ranMaintenance = true;
            })
            ->choose();
    });

    $this->assertTrue($ranMaintenance);
}
```

### `Lottery::alwaysLose()` — 항상 낙첨시키기

```php theme={null}
public function test_maintenance_skipped_on_lose(): void
{
    $ranMaintenance = false;

    Lottery::alwaysLose(function () use (&$ranMaintenance) {
        Lottery::odds(1, 100)
            ->winner(function () use (&$ranMaintenance) {
                $ranMaintenance = true;
            })
            ->choose();
    });

    $this->assertFalse($ranMaintenance);
}
```

### `Lottery::fix()` — 결과를 시퀀스로 고정하기

여러 번의 호출 결과를 `true`/`false`의 배열로 제어할 수 있습니다.

```php theme={null}
public function test_alternating_results(): void
{
    Lottery::fix([true, false, true, false]);

    $results = Lottery::odds(1, 100)
        ->winner(fn () => 'winner')
        ->loser(fn () => 'loser')
        ->choose(4);

    $this->assertSame(['winner', 'loser', 'winner', 'loser'], $results);

    Lottery::determineResultNormally(); // テスト後は必ず元に戻す
}
```

<Warning>
  `alwaysWin()` / `alwaysLose()` / `fix()`는 전역 정적 프로퍼티를 변경합니다. 테스트의 `tearDown()`에서 반드시 `Lottery::determineResultNormally()`를 호출해 주세요.
</Warning>

```php theme={null}
protected function tearDown(): void
{
    Lottery::determineResultNormally();

    parent::tearDown();
}
```

### `Lottery::setResultFactory()` — 커스텀 팩토리 주입하기

더 세밀한 제어가 필요한 경우에는 커스텀 팩토리를 사용합니다.

```php theme={null}
Lottery::setResultFactory(function ($chances, $outOf) {
    // 常に当選させるカスタムロジック
    return true;
});

// テスト後はリセット
Lottery::determineResultNormally();
```

## 패키지 개발에서의 활용

### 서비스 프로바이더에의 등록

패키지의 서비스 프로바이더에 유지보수 처리를 넣는 경우, `Lottery`를 사용해 부하를 분산시킵니다.

```php theme={null}
use Illuminate\Support\Lottery;
use Illuminate\Support\ServiceProvider;

class AcmeServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->app->booted(function () {
            Lottery::odds(1, 100)
                ->winner(fn () => $this->pruneExpiredRecords())
                ->choose();
        });
    }

    protected function pruneExpiredRecords(): void
    {
        $this->app['db']->table('acme_logs')
            ->where('expires_at', '<', now())
            ->delete();
    }
}
```

### 설정값에서 odds 읽어들이기

확률을 설정 파일에서 변경 가능하게 하면, 사용자가 조정하기 쉬워집니다.

```php theme={null}
$lottery = config('acme.prune_lottery', [1, 100]);

Lottery::odds(...$lottery)
    ->winner(fn () => $this->pruneExpiredRecords())
    ->choose();
```

```php theme={null}
// config/acme.php
return [
    // [当選数, 試行数] = 100リクエストに1回プルーン実行
    'prune_lottery' => [1, 100],
];
```

### Middleware에서의 샘플링

```php theme={null}
use Illuminate\Support\Lottery;

class SampleTelemetryMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        Lottery::odds(1, 50)
            ->winner(fn () => $this->recordTelemetry($request, $response))
            ->choose();

        return $response;
    }
}
```

## API 레퍼런스

| 메서드                                   | 설명                                 |
| ------------------------------------- | ---------------------------------- |
| `Lottery::odds($chances, $outOf)`     | Lottery 인스턴스를 생성하는 정적 팩토리          |
| `->winner(callable $callback)`        | 당첨 시의 콜백을 설정                       |
| `->loser(callable $callback)`         | 낙첨 시의 콜백을 설정                       |
| `->choose($times = null)`             | Lottery를 실행. `$times`를 지정하면 배열을 반환 |
| `Lottery::alwaysWin($callback)`       | 테스트용: 항상 당첨                        |
| `Lottery::alwaysLose($callback)`      | 테스트용: 항상 낙첨                        |
| `Lottery::fix($sequence)`             | 테스트용: 결과를 시퀀스로 고정                  |
| `Lottery::determineResultNormally()`  | 테스트용 고정을 리셋                        |
| `Lottery::setResultFactory(callable)` | 커스텀 판정 로직을 주입                      |

## 관련 페이지

<Columns cols={2}>
  <Card title="Macroable 트레이트" icon="puzzle-piece" href="/ko/advanced/macroable">
    기존 클래스에 새로운 메서드를 추가하는 확장 패턴을 배웁니다.
  </Card>

  <Card title="Conditionable 트레이트" icon="git-branch" href="/ko/advanced/conditionable">
    `when()` / `unless()`를 사용한 조건 분기 체인의 설계를 배웁니다.
  </Card>
</Columns>


## Related topics

- [Fluent 클래스](/ko/advanced/fluent.md)
- [문자열 조작 (Str 클래스)](/ko/strings.md)
- [알림(Notifications)](/ko/notifications.md)
- [메일 전송](/ko/mail.md)
- [Laravel Pennant](/ko/pennant.md)
