Lottery 클래스란
Illuminate\Support\Lottery는 확률 기반의 조작을 유창한 API로 표현할 수 있는 유틸리티 클래스입니다. “100 요청에 1번만 처리를 실행한다”, “일부 요청만 상세 로그를 기록한다”와 같은 패턴을 심플하게 기술할 수 있습니다.
구현은 src/Illuminate/Support/Lottery.php에 있습니다. Laravel은 이 클래스를 Session GC나 캐시 락의 prune 등, 프레임워크 내부에서도 활용하고 있습니다.
기본적인 사용법
정수 비율로 확률 지정하기
Lottery::odds($chances, $outOf)로 “o u t O f 회중 outOf 회 중 o u tO f 회중 chances 회 당첨”이라는 확률을 지정합니다.
use Illuminate\Support\ Lottery ;
Lottery :: odds ( 1 , 100 ) // 100回に1回
-> winner ( fn () => $this -> runMaintenance ())
-> loser ( fn () => null )
-> choose ();
소수로 확률 지정하기
$outOf를 생략하고 0.0~1.0의 소수를 전달하면, 그대로 확률로 사용됩니다.
Lottery :: odds ( 0.01 ) // 1% の確率
-> winner ( fn () => $this -> sample ())
-> choose ();
소수 지정 시 값이 1.0을 넘으면 RuntimeException이 던져집니다.
콜백 없이 진위값 반환하기
winner / loser를 설정하지 않은 경우, choose()는 당첨이면 true, 낙첨이면 false를 반환합니다.
$shouldSample = Lottery :: odds ( 1 , 50 ) -> choose (); // bool
여러 번 실행하기
choose($times)에 횟수를 전달하면 결과의 배열이 반환됩니다.
$results = Lottery :: odds ( 1 , 2 )
-> winner ( fn () => 'win' )
-> loser ( fn () => 'lose' )
-> choose ( 10 );
// 例: ['win', 'lose', 'win', 'win', 'lose', ...]
Callable로서 전달하기
Lottery 인스턴스는 __invoke를 구현하고 있으므로, callable을 받는 API에 직접 전달할 수 있습니다.
// 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번만 실행)
기한이 지난 레코드 삭제 등, 매번 실행할 필요가 없는 유지보수 처리에 최적입니다.
Lottery :: odds ( 1 , 100 )
-> winner ( fn () => Cache :: store ( 'database' ) -> flush ())
-> choose ();
2. 텔레메트리·샘플링(일부 요청만 상세 로그)
전체 요청을 로그에 남기면 비용이 높은 경우, 샘플링에 사용할 수 있습니다.
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개의 코드 패스로 나눕니다.
$result = Lottery :: odds ( 1 , 2 )
-> winner ( fn ( $user ) => $this -> newCheckoutFlow ( $user ))
-> loser ( fn ( $user ) => $this -> legacyCheckoutFlow ( $user ))
-> choose ();
4. Scheduler의 보조로 정기 태스크를 랜덤 실행
여러 서버에서 중복 실행을 피하면서, 어떤 태스크를 랜덤하게 실행하고 싶을 때 사용할 수 있습니다.
// 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()를 직접 사용하고 있지만, 같은 사상에 기반합니다.
Session: 가비지 컬렉션
Illuminate\Session\Middleware\StartSession::configHitsLottery()는 config/session.php의 lottery 설정을 사용해 random_int로 확률 판정하여 GC를 실행합니다.// 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 ];
}
DatabaseLock: 기한 만료 락 prune
Illuminate\Cache\DatabaseLock::acquire()는 락 획득 때마다 동일한 비율 패턴으로 기한 만료 락을 삭제합니다.// 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 ();
}
DB::whenQueryingForLongerThan — Lottery 클래스를 전달하는 예
Lottery 인스턴스는 callable로 전달할 수 있으므로, 슬로우 쿼리 감지 콜백에 직접 사용할 수 있습니다.DB :: whenQueryingForLongerThan (
Interval :: seconds ( 5 ),
Lottery :: odds ( 1 , 5 ) -> winner ( function ( $connection ) {
Log :: warning ( "Slow query on { $connection -> getName ()}" );
})
);
Session이나 DatabaseLock이 random_int()를 직접 사용하는 것에 비해, Lottery 클래스를 사용하면 alwaysWin() / alwaysLose() / fix()로 테스트 시에 결과를 제어할 수 있는 이점이 있습니다. 패키지 개발에서는 Lottery 클래스를 선택하면 테스트 용이성이 향상됩니다.
테스트 시의 이용
랜덤성이 있는 코드의 테스트에는 Lottery가 제공하는 테스트용 API를 사용합니다.
Lottery::alwaysWin() — 항상 당첨시키기
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() — 항상 낙첨시키기
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의 배열로 제어할 수 있습니다.
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 (); // テスト後は必ず元に戻す
}
alwaysWin() / alwaysLose() / fix()는 전역 정적 프로퍼티를 변경합니다. 테스트의 tearDown()에서 반드시 Lottery::determineResultNormally()를 호출해 주세요.
protected function tearDown () : void
{
Lottery :: determineResultNormally ();
parent :: tearDown ();
}
Lottery::setResultFactory() — 커스텀 팩토리 주입하기
더 세밀한 제어가 필요한 경우에는 커스텀 팩토리를 사용합니다.
Lottery :: setResultFactory ( function ( $chances , $outOf ) {
// 常に当選させるカスタムロジック
return true ;
});
// テスト後はリセット
Lottery :: determineResultNormally ();
패키지 개발에서의 활용
서비스 프로바이더에의 등록
패키지의 서비스 프로바이더에 유지보수 처리를 넣는 경우, Lottery를 사용해 부하를 분산시킵니다.
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 읽어들이기
확률을 설정 파일에서 변경 가능하게 하면, 사용자가 조정하기 쉬워집니다.
$lottery = config ( 'acme.prune_lottery' , [ 1 , 100 ]);
Lottery :: odds ( ... $lottery )
-> winner ( fn () => $this -> pruneExpiredRecords ())
-> choose ();
// config/acme.php
return [
// [当選数, 試行数] = 100リクエストに1回プルーン実行
'prune_lottery' => [ 1 , 100 ],
];
Middleware에서의 샘플링
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)커스텀 판정 로직을 주입
관련 페이지
Macroable 트레이트 기존 클래스에 새로운 메서드를 추가하는 확장 패턴을 배웁니다.
Conditionable 트레이트 when() / unless()를 사용한 조건 분기 체인의 설계를 배웁니다.