큐
업데이트됨번역일: 2026년 10월 8일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 10월 7일
- 번역 갱신
- 2026년 10월 8일
큐
- 소개
- Job 생성
- Job 미들웨어
- Job 전달
- Job 배치
- 클로저 큐 처리
- 큐 워커 실행
- Supervisor 설정
- 실패한 Job 처리
- 큐의 Job 비우기
- 큐 모니터링
- 테스트
- Job 이벤트
소개
업로드된 CSV 파일을 분석해 저장하는 것처럼 일반 웹 요청 안에서 처리하기에는 오래 걸리는 작업이 있습니다. Laravel은 백그라운드에서 처리할 큐 Job을 쉽게 생성할 수 있게 합니다. 시간이 많이 드는 작업을 큐로 옮기면 웹 요청에 빠르게 응답하여 사용자 경험을 개선할 수 있습니다.
Laravel 큐는 Amazon SQS, Redis, 관계형 데이터베이스 등 다양한 백엔드에 통일된 큐 API를 제공합니다.
설정은 config/queue.php에 있습니다. database, Amazon SQS, Redis, Beanstalkd 등의 연결 설정과 개발·테스트용으로 Job을 즉시 실행하는 동기 드라이버가 포함됩니다. Job을 버리는 null 드라이버도 있습니다.
NOTE
Laravel Horizon은 Redis 큐를 위한 대시보드와 설정 시스템입니다. 자세한 내용은 Horizon 문서를 참고하세요.
연결과 큐
Laravel 큐를 사용하기 전에 연결과 큐의 차이를 이해해야 합니다. config/queue.php의 connections는 Amazon SQS, Beanstalk, Redis 등의 백엔드 서비스 연결을 정의합니다. 하나의 연결 안에는 Job을 쌓는 별도 공간으로 볼 수 있는 여러 큐가 존재할 수 있습니다.
각 연결 설정의 queue 속성은 해당 연결로 전달하는 Job의 기본 큐입니다. 큐를 명시하지 않고 전달하면 연결 설정의 queue에 지정한 큐에 들어갑니다:
use App\Jobs\ProcessPodcast;
// This job is sent to the default connection's default queue...
ProcessPodcast::dispatch();
// This job is sent to the default connection's "emails" queue...
ProcessPodcast::dispatch()->onQueue('emails');하나의 큐만으로 충분한 애플리케이션도 있습니다. 처리 우선순위를 나누거나 작업을 구분하고 싶다면 여러 큐가 유용합니다. Laravel 큐 워커는 우선순위대로 처리할 큐를 지정할 수 있습니다. 예를 들어 high 큐의 Job을 우선 처리하려면 다음 워커를 실행합니다:
php artisan queue:work --queue=high,default드라이버 참고 사항과 사전 준비
데이터베이스
database 드라이버에는 Job 저장용 테이블이 필요합니다. 보통 기본 데이터베이스 마이그레이션인 0001_01_01_000002_create_jobs_table.php에 포함됩니다. 없다면 make:queue-table로 생성할 수 있습니다:
php artisan make:queue-tablephp artisan migrateRedis
redis 드라이버를 사용하려면 config/database.php에 Redis 연결을 설정해야 합니다.
WARNING
redis 큐 드라이버는 Redis의 serializer와 compression 옵션을 지원하지 않습니다.
Redis Cluster
연결이 Redis Cluster를 사용한다면 큐 이름에 키 해시 태그가 있어야 합니다. 같은 큐의 모든 Redis 키를 동일한 해시 슬롯에 배치하기 위한 조건입니다:
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', '{default}'),
'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],블로킹
Redis 큐의 block_for는 Job이 준비되기를 기다리는 시간을 지정합니다. 이 시간이 지나면 워커 루프를 다음으로 진행하며 Redis를 다시 폴링합니다.
큐 부하에 맞게 값을 조절하면 새 Job을 계속 폴링하는 것보다 효율적일 수 있습니다. 예를 들어 5로 설정하면 Job이 준비될 때까지 최대 5초 동안 블로킹합니다:
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => 5,
'after_commit' => false,
],WARNING
block_for가 0이면 Job이 준비될 때까지 무기한 블로킹합니다. 다음 Job을 처리할 때까지 SIGTERM 같은 신호도 처리하지 못합니다.
SQS 초과 페이로드 저장
Amazon SQS에는 메시지 페이로드 최대 크기 제한이 있습니다. 한도를 넘을 수 있는 Job을 전달해야 한다면 큰 페이로드를 캐시에 저장하고 SQS에는 참조만 보내도록 설정할 수 있습니다. SQS 연결 설정에 overflow 배열을 추가합니다:
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
'overflow' => [
'enabled' => env('SQS_OVERFLOW_ENABLED', false),
'store' => env('SQS_OVERFLOW_STORE'),
'always' => false,
'delete_after_processing' => true,
'flush_on_clear' => env('SQS_OVERFLOW_FLUSH_ON_CLEAR', false),
],
],활성화하면 Laravel은 1MB 이상인 페이로드를 지정한 캐시에 저장합니다. always가 true이면 크기와 관계없이 모든 페이로드를 저장합니다. 처리 시 캐시에서 가져와야 하므로 워커가 처리할 때까지 보관할 수 있는 저장소를 선택해야 합니다. 기본적으로 Job이 성공적으로 처리되고 SQS에서 삭제되면 저장된 페이로드도 삭제됩니다.
flush_on_clear가 true이면 queue:clear로 SQS 큐를 비울 때 초과 페이로드 캐시 저장소도 비웁니다. 모든 항목이 제거될 수 있으므로 이 옵션을 활성화할 때는 전용 캐시 저장소를 사용해야 합니다.
기타 드라이버 사전 준비
다음 드라이버에는 아래 의존성이 필요합니다. Composer로 설치할 수 있습니다:
- Amazon SQS:
aws/aws-sdk-php ~3.0 - Beanstalkd:
pda/pheanstalk ^7.0|^8.0 - Redis:
predis/predis ~3.0또는 phpredis PHP 확장 - MongoDB:
mongodb/laravel-mongodb
Job 생성
Job 클래스 생성
큐로 실행할 Job은 기본적으로 app/Jobs에 저장합니다. 디렉터리가 없으면 make:job을 실행할 때 생성됩니다:
php artisan make:job ProcessPodcast생성한 클래스는 Illuminate\Contracts\Queue\ShouldQueue를 구현하여 비동기 실행을 위해 큐에 넣어야 함을 Laravel에 알립니다.
NOTE
스텁 게시로 Job 스텁을 수정할 수 있습니다.
클래스 구조
Job 클래스는 보통 큐에서 처리할 때 호출하는 handle만 있는 간단한 구조입니다. 팟캐스트 게시 서비스에서 업로드한 파일을 게시 전에 처리하는 예제를 살펴보겠습니다:
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* Execute the job.
*/
public function handle(AudioProcessor $processor): void
{
// Process uploaded podcast...
}
}예제에서는 Eloquent 모델을 큐 Job 생성자에 직접 전달했습니다. Queueable 트레이트가 모델과 로드된 관계를 적절히 직렬화하고 처리 시 역직렬화합니다.
생성자로 모델을 받으면 큐에는 모델 식별자만 직렬화됩니다. 실제 처리할 때 큐 시스템이 데이터베이스에서 모델 전체와 로드된 관계를 다시 조회합니다. 이 방식은 Job 페이로드를 크게 줄입니다.
`handle` 메서드 의존성 주입
큐가 Job을 처리할 때 handle을 호출합니다. 여기에 의존성 타입 힌트를 지정하면 Laravel 서비스 컨테이너가 자동 주입합니다.
주입 방식을 직접 제어하려면 컨테이너의 bindMethod를 사용합니다. Job과 컨테이너를 받는 콜백에서 원하는 방식으로 handle을 호출합니다. 보통 App\Providers\AppServiceProvider 서비스 프로바이더의 boot에서 설정합니다:
use App\Jobs\ProcessPodcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Foundation\Application;
$this->app->bindMethod([ProcessPodcast::class, 'handle'], function (ProcessPodcast $job, Application $app) {
return $job->handle($app->make(AudioProcessor::class));
});WARNING
원시 이미지 같은 바이너리 데이터는 큐 Job에 전달하기 전에 base64_encode로 인코딩해야 합니다. 그렇지 않으면 큐에 넣을 때 JSON 직렬화가 제대로 되지 않을 수 있습니다.
큐에 포함되는 관계
모델에 로드된 관계도 직렬화되므로 Job 문자열이 커질 수 있습니다. 역직렬화 후 데이터베이스에서 관계를 다시 조회할 때는 전체를 가져오며, 큐에 넣기 전의 관계 제약은 적용되지 않습니다. 관계 일부만 사용하려면 Job 안에서 제약을 다시 적용해야 합니다.
관계를 직렬화하지 않으려면 속성에 모델을 할당할 때 withoutRelations를 호출합니다. 로드된 관계가 없는 모델 인스턴스를 반환합니다:
/**
* Create a new job instance.
*/
public function __construct(
Podcast $podcast,
) {
$this->podcast = $podcast->withoutRelations();
}다른 관계는 유지하면서 특정 관계만 제거하려면 withoutRelation을 사용합니다:
$this->podcast = $podcast->withoutRelation('comments');PHP 생성자 속성 승격을 사용할 때 Eloquent 모델의 관계를 직렬화하지 않으려면 WithoutRelations 속성을 사용합니다:
use Illuminate\Queue\Attributes\WithoutRelations;
/**
* Create a new job instance.
*/
public function __construct(
#[WithoutRelations]
public Podcast $podcast,
) {}모든 모델을 관계 없이 직렬화하려면 개별 모델 대신 클래스 전체에 WithoutRelations를 적용할 수 있습니다:
<?php
namespace App\Jobs;
use App\Models\DistributionPlatform;
use App\Models\Podcast;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\WithoutRelations;
#[WithoutRelations]
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(
public Podcast $podcast,
public DistributionPlatform $platform,
) {}
}단일 모델 대신 Eloquent 모델 컬렉션이나 배열을 Job이 받으면 역직렬화 및 실행 시 관계가 복원되지 않습니다. 많은 모델을 다루는 Job의 과도한 리소스 사용을 막기 위한 동작입니다.
고유 Job
WARNING
고유 Job에는 잠금을 지원하는 캐시 드라이버가 필요합니다. 현재 memcached, redis, dynamodb, database, file, array가 원자적 잠금을 지원합니다.
WARNING
배치 안의 Job에는 고유성 제약이 적용되지 않습니다.
특정 Job 인스턴스가 큐에 하나만 존재하게 하려면 ShouldBeUnique를 구현합니다. 추가 메서드를 정의할 필요는 없습니다:
<?php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
// ...
}위 예제의 UpdateSearchIndex는 고유 Job입니다. 다른 인스턴스가 큐에 있고 처리를 마치지 않았다면 새 Job을 전달하지 않습니다.
고유 키나 고유성을 유지할 제한 시간을 지정하려면 UniqueFor 속성과 uniqueId 메서드를 사용합니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Queue\Attributes\UniqueFor;
#[UniqueFor(3600)]
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
/**
* The product instance.
*
* @var \App\Models\Product
*/
public $product;
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return $this->product->id;
}
}위 예제는 제품 ID로 고유성을 구분합니다. 같은 ID의 새 Job 전달은 기존 처리가 완료될 때까지 무시합니다. 기존 Job을 한 시간 내에 처리하지 못하면 잠금이 해제되어 같은 키의 다른 Job을 큐에 전달할 수 있습니다.
WARNING
여러 웹 서버나 컨테이너에서 Job을 전달한다면 모든 서버가 같은 중앙 캐시 서버를 사용해야 고유성을 정확히 판단할 수 있습니다.
처리 시작 전까지 고유성 유지
기본적으로 고유 Job은 처리를 완료하거나 모든 재시도에 실패한 뒤 잠금이 해제됩니다. 처리 직전에 해제하려면 ShouldBeUnique 대신 ShouldBeUniqueUntilProcessing 계약을 구현합니다:
<?php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
// ...
}고유 Job 잠금
ShouldBeUnique Job을 전달할 때 Laravel은 uniqueId 키의 잠금을 획득하려고 시도합니다. 이미 잠겨 있으면 전달하지 않습니다. 처리 완료 또는 모든 재시도 실패 시 해제됩니다. 기본 캐시 드라이버 대신 다른 드라이버를 사용하려면 해당 드라이버를 반환하는 uniqueVia를 정의합니다:
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
// ...
/**
* Get the cache driver for the unique job lock.
*/
public function uniqueVia(): Repository
{
return Cache::driver('redis');
}
}NOTE
Job의 동시 처리만 제한하려면 WithoutOverlapping 미들웨어를 사용하세요.
디바운스 Job
같은 Job이 짧은 시간 동안 반복 전달될 때 최신 전달만 실행하려면 DebounceFor 속성을 추가합니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\DebounceFor;
#[DebounceFor(30)]
class UpdateSearchIndex implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(public int $productId)
{
}
/**
* Get the debounce ID for the job.
*/
public function debounceId(): string
{
return (string) $this->productId;
}
}위 예제에서는 같은 제품의 UpdateSearchIndex를 30초 안에 반복 전달하면 최신 전달만 실행됩니다.
반복 전달이 Job을 지연시킬 수 있는 최대 시간을 제한하려면 maxWait를 지정합니다:
#[DebounceFor(30, maxWait: 120)]
class UpdateSearchIndex implements ShouldQueue
{
use Queueable;
// ...
}디바운스 추적용 캐시 저장소를 변경하려면 debounceVia를 정의합니다:
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
public function debounceVia(): Repository
{
return Cache::driver('redis');
}디바운스 Job이 더 최신 전달로 대체되면 Laravel은 Illuminate\Queue\Events\JobDebounced를 전달하고 대체된 Job을 큐에서 제거합니다.
WARNING
디바운스와 고유 Job은 함께 사용할 수 없습니다. DebounceFor를 사용하는 Job은 ShouldBeUnique를 구현하면 안 됩니다.
WARNING
여러 서버나 컨테이너에서 디바운스 Job을 전달한다면 같은 중앙 캐시 서버를 사용해야 합니다.
암호화된 Job
암호화로 Job 데이터의 기밀성과 무결성을 보호할 수 있습니다. ShouldBeEncrypted를 클래스에 추가하면 Laravel이 큐에 넣기 전에 Job을 자동 암호화합니다:
<?php
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
{
// ...
}Job 미들웨어
Job 미들웨어는 실행 전후를 사용자 정의 로직으로 감싸 반복 코드를 줄입니다. 예를 들어 다음 handle은 Redis 요청 제한으로 5초마다 하나의 Job만 처리합니다:
use Illuminate\Support\Facades\Redis;
/**
* Execute the job.
*/
public function handle(): void
{
Redis::throttle('key')->block(0)->allow(1)->every(5)->then(function () {
info('Lock obtained...');
// Handle job...
}, function () {
// Could not obtain lock...
return $this->release(5);
});
}코드는 유효하지만 handle에 Redis 제한 로직이 섞여 복잡해지고, 다른 Job에도 같은 로직을 반복해야 합니다. 이를 Job 미들웨어로 분리할 수 있습니다:
<?php
namespace App\Jobs\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class RateLimited
{
/**
* Process the queued job.
*
* @param \Closure(object): void $next
*/
public function handle(object $job, Closure $next): void
{
Redis::throttle('key')
->block(0)->allow(1)->every(5)
->then(function () use ($job, $next) {
// Lock obtained...
$next($job);
}, function () use ($job) {
// Could not obtain lock...
$job->release(5);
});
}
}라우트 미들웨어처럼 Job 미들웨어도 처리할 Job과 처리를 계속하기 위한 콜백을 받습니다.
make:job-middleware로 클래스를 생성한 뒤 Job의 middleware에서 반환하여 연결합니다. make:job이 생성한 클래스에는 이 메서드가 없으므로 직접 추가해야 합니다:
use App\Jobs\Middleware\RateLimited;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited];
}요청 제한
Laravel에는 직접 구현하지 않고 사용할 수 있는 요청 제한 미들웨어가 있습니다. 라우트 요청 제한기처럼 RateLimiter 파사드의 for로 정의합니다.
예를 들어 일반 사용자는 시간당 한 번만 백업하고 프리미엄 고객은 제한 없이 사용하게 하려면 AppServiceProvider의 boot에 다음 제한기를 정의합니다:
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
RateLimiter::for('backups', function (object $job) {
return $job->user->vipCustomer()
? Limit::none()
: Limit::perHour(1)->by($job->user->id);
});
}위 예제는 시간 단위지만 perMinute로 분 단위 제한도 정의할 수 있습니다. by에는 원하는 값을 전달할 수 있으며 보통 고객별로 제한을 구분합니다:
return Limit::perMinute(50)->by($job->user->id);정의한 제한기는 Illuminate\Queue\Middleware\RateLimited로 Job에 연결합니다. 한도를 초과할 때마다 제한 시간에 맞는 지연을 적용해 Job을 큐로 반환합니다:
use Illuminate\Queue\Middleware\RateLimited;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited('backups')];
}제한된 Job을 큐로 반환해도 전체 attempts는 증가합니다. 이에 맞게 Tries와 MaxExceptions를 조절하거나 retryUntil로 재시도 기한을 정의할 수 있습니다.
releaseAfter로 반환된 Job을 다시 시도하기 전 대기할 초 단위 시간을 지정할 수도 있습니다:
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new RateLimited('backups'))->releaseAfter(60)];
}제한에 걸린 Job을 재시도하지 않으려면 dontRelease를 사용합니다:
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new RateLimited('backups'))->dontRelease()];
}Redis 요청 제한
Redis를 사용한다면 Redis에 맞게 조정되어 기본 미들웨어보다 효율적인 Illuminate\Queue\Middleware\RateLimitedWithRedis를 사용할 수 있습니다:
use Illuminate\Queue\Middleware\RateLimitedWithRedis;
public function middleware(): array
{
return [new RateLimitedWithRedis('backups')];
}connection으로 사용할 Redis 연결을 지정할 수 있습니다:
return [(new RateLimitedWithRedis('backups'))->connection('limiter')];Job 중복 실행 방지
Illuminate\Queue\Middleware\WithoutOverlapping은 임의의 키를 기준으로 Job 중복 실행을 방지합니다. 한 번에 하나의 Job만 수정해야 하는 리소스를 처리할 때 유용합니다.
예를 들어 같은 사용자 ID의 신용 점수 갱신 Job이 겹치지 않게 하려면 middleware에서 WithoutOverlapping을 반환합니다:
use Illuminate\Queue\Middleware\WithoutOverlapping;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new WithoutOverlapping($this->user->id)];
}겹친 Job을 큐로 반환해도 전체 시도 횟수가 증가하므로 Tries와 MaxExceptions를 조절해야 할 수 있습니다. 예를 들어 기본값처럼 Tries가 1이면 겹친 Job을 나중에 재시도할 수 없습니다.
같은 유형의 겹친 Job은 큐로 반환됩니다. 다시 시도하기 전 대기할 초 단위 시간도 지정할 수 있습니다:
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->releaseAfter(60)];
}겹친 Job을 즉시 삭제하여 재시도하지 않으려면 dontRelease를 사용합니다:
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->dontRelease()];
}WithoutOverlapping은 Laravel의 원자적 잠금을 사용합니다. 예상치 못한 실패나 타임아웃으로 잠금이 해제되지 않을 수 있어 expireAfter로 만료 시간을 지정할 수 있습니다. 다음 예제는 처리 시작 3분 후 잠금을 해제하도록 합니다:
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->expireAfter(180)];
}WARNING
WithoutOverlapping에는 잠금을 지원하는 캐시 드라이버가 필요합니다. 현재 memcached, redis, dynamodb, database, file, array가 원자적 잠금을 지원합니다.
Job 클래스 간 잠금 키 공유
기본적으로 WithoutOverlapping은 같은 클래스의 중복 실행만 방지합니다. 다른 클래스가 같은 키를 써도 서로의 중복 실행을 막지 않습니다. shared로 클래스 간에 키를 공유하도록 할 수 있습니다:
use Illuminate\Queue\Middleware\WithoutOverlapping;
class ProviderIsDown
{
// ...
public function middleware(): array
{
return [
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
];
}
}
class ProviderIsUp
{
// ...
public function middleware(): array
{
return [
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
];
}
}예외 발생 제한
Illuminate\Queue\Middleware\ThrottlesExceptions는 일정 횟수의 예외가 발생하면 이후 Job 실행 시도를 지정한 시간 동안 지연합니다. 불안정한 외부 서비스와 상호작용하는 Job에 유용합니다.
외부 API에서 예외가 발생하기 시작한다면 Job의 middleware에서 이 미들웨어를 반환할 수 있습니다. 일반적으로 시간 기반 시도를 구현한 Job과 함께 사용해야 합니다:
use DateTime;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new ThrottlesExceptions(10, 5 * 60)];
}
/**
* Determine the time at which the job should timeout.
*/
public function retryUntil(): DateTime
{
return now()->plus(minutes: 30);
}첫 생성자 인수는 제한이 시작되기 전 허용할 예외 수이며, 두 번째는 제한된 Job을 다시 시도하기 전 기다릴 초 단위 시간입니다. 위 예제는 연속 10번 예외가 발생하면 5분을 기다리며 전체 30분 기한 안에서만 시도합니다.
임계값에 도달하지 않은 예외는 보통 즉시 재시도합니다. 미들웨어의 backoff로 이때 기다릴 분 단위 시간을 지정할 수 있습니다:
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 5 * 60))->backoff(5)];
}backoff에 발생한 예외를 받는 클로저를 전달하여 대기 시간을 동적으로 결정할 수도 있습니다:
use App\Exceptions\RateLimitedException;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
use Throwable;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 5 * 60))->backoff(
fn (Throwable $throwable) => $throwable instanceof RateLimitedException
? $throwable->retryAfterMinutes()
: 5
)];
}미들웨어는 Laravel 캐시를 이용하며 기본 키는 Job 클래스 이름입니다. by로 바꿀 수 있습니다. 같은 외부 서비스를 사용하는 여러 Job이 하나의 제한을 공유하도록 할 때 유용합니다:
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 10 * 60))->by('key')];
}기본적으로 모든 예외를 제한 대상으로 삼습니다. when을 사용하면 클로저가 true를 반환하는 예외만 제한합니다:
use Illuminate\Http\Client\HttpClientException;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 10 * 60))->when(
fn (Throwable $throwable) => $throwable instanceof HttpClientException
)];
}Job을 큐로 반환하거나 예외를 던지는 when과 달리, deleteWhen은 특정 예외 발생 시 Job을 완전히 삭제할 수 있습니다:
use App\Exceptions\CustomerDeletedException;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(2, 10 * 60))->deleteWhen(CustomerDeletedException::class)];
}제한한 예외를 애플리케이션 예외 처리기에 보고하려면 report를 호출합니다. 선택적으로 클로저를 전달하면 true를 반환할 때만 보고합니다:
use Illuminate\Http\Client\HttpClientException;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 10 * 60))->report(
fn (Throwable $throwable) => $throwable instanceof HttpClientException
)];
}Redis 예외 발생 제한
Redis를 사용한다면 Redis에 맞게 조정되어 기본 미들웨어보다 효율적인 Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis를 사용할 수 있습니다:
use Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis;
public function middleware(): array
{
return [new ThrottlesExceptionsWithRedis(10, 10 * 60)];
}connection으로 사용할 Redis 연결을 지정할 수 있습니다:
return [(new ThrottlesExceptionsWithRedis(10, 10 * 60))->connection('limiter')];Job 반환
Release 미들웨어는 Job을 실행하지 않고 큐로 반환합니다. Release::when은 조건이 true일 때, Release::unless는 false일 때 반환합니다:
use Illuminate\Queue\Middleware\Release;
/**
* Get the middleware the job should pass through.
*/
public function middleware(): array
{
return [
Release::when($condition, releaseAfter: 60),
];
}큐로 반환해도 전체 시도 횟수는 증가합니다. 이에 맞게 Job의 Tries와 MaxExceptions를 조절해야 할 수 있습니다.
복잡한 조건은 when과 unless에 Closure로 전달할 수 있습니다:
use Illuminate\Queue\Middleware\Release;
/**
* Get the middleware the job should pass through.
*/
public function middleware(): array
{
return [
Release::when(function (): bool {
return ! $this->order->isPaid();
}, releaseAfter: 60),
];
}Job 건너뛰기
Skip 미들웨어로 Job 내부 로직을 바꾸지 않고 건너뛰거나 삭제할 수 있습니다. Skip::when은 조건이 true일 때, Skip::unless는 false일 때 삭제합니다:
use Illuminate\Queue\Middleware\Skip;
/**
* Get the middleware the job should pass through.
*/
public function middleware(): array
{
return [
Skip::when($condition),
];
}복잡한 조건은 when과 unless에 Closure로 전달할 수 있습니다:
use Illuminate\Queue\Middleware\Skip;
/**
* Get the middleware the job should pass through.
*/
public function middleware(): array
{
return [
Skip::when(function (): bool {
return $this->shouldSkip();
}),
];
}Job 전달
Job 클래스를 작성한 뒤 Job의 dispatch로 전달합니다. 인수는 Job 생성자로 넘어갑니다:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Store a new podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// ...
ProcessPodcast::dispatch($podcast);
return redirect('/podcasts');
}
}조건부로 전달하려면 dispatchIf와 dispatchUnless를 사용합니다:
ProcessPodcast::dispatchIf($accountActive, $podcast);
ProcessPodcast::dispatchUnless($accountSuspended, $podcast);새 Laravel 애플리케이션의 기본 큐 연결은 database입니다. .env의 QUEUE_CONNECTION으로 변경할 수 있습니다.
지연 전달
Job을 즉시 처리할 수 없게 하려면 전달 시 delay를 사용합니다. 예를 들어 전달 후 10분이 지나야 처리할 수 있도록 지정합니다:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Store a new podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// ...
ProcessPodcast::dispatch($podcast)
->delay(now()->plus(minutes: 10));
return redirect('/podcasts');
}
}기본 지연 시간이 설정된 Job을 즉시 처리 대상으로 만들려면 withoutDelay로 지연을 건너뛸 수 있습니다:
ProcessPodcast::dispatch($podcast)->withoutDelay();WARNING
Amazon SQS의 최대 지연 시간은 15분입니다.
동기 전달
dispatchSync로 Job을 즉시 동기 실행할 수 있습니다. 큐에 넣지 않고 현재 프로세스 안에서 바로 실행합니다:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Store a new podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// Create podcast...
ProcessPodcast::dispatchSync($podcast);
return redirect('/podcasts');
}
}지연된 동기 전달
동기 Job을 현재 프로세스에서 처리하되 HTTP 응답을 보낸 뒤 실행할 수 있습니다. 사용자에게 응답하는 시간을 늦추지 않고 처리하려면 deferred 연결로 전달합니다:
RecordDelivery::dispatch($order)->onConnection('deferred');deferred는 기본 장애 조치 큐이기도 합니다.
background 역시 HTTP 응답 후 처리하지만, 별도로 생성한 PHP 프로세스에서 실행합니다. 따라서 PHP-FPM / 애플리케이션 워커는 다른 HTTP 요청을 받을 수 있습니다:
RecordDelivery::dispatch($order)->onConnection('background');일괄 전달
독립적인 Job을 한 번에 많이 전달하고 배치 추적이나 콜백이 필요하지 않다면 Bus 파사드의 bulk를 사용합니다. Laravel이 설정된 연결과 큐 이름으로 묶어 각 그룹을 해당 큐에 일괄 전달합니다:
use App\Jobs\ProcessUser;
use Illuminate\Support\Facades\Bus;
Bus::bulk(
$users->map(fn ($user) => new ProcessUser($user))
);전달 전 Job 준비
큐에 넣기 전에 상태를 준비하거나 검사하려면 Illuminate\Contracts\Queue\PreparesForDispatch를 구현합니다. Laravel이 전달 전에 prepareForDispatch를 호출하며 false를 반환하면 Job을 전달하지 않습니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\PreparesForDispatch;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Cache;
class SyncPodcasts implements PreparesForDispatch, ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(
public array $podcastIds,
) {}
/**
* Prepare the job before dispatching.
*/
public function prepareForDispatch(): bool
{
return collect($this->podcastIds)
->reject(fn (int $id) => Cache::has("podcast-syncing:{$id}"))
->isNotEmpty();
}
}Job과 데이터베이스 트랜잭션
트랜잭션 안에서 Job을 전달할 수 있지만 성공적으로 실행할 수 있는지 주의해야 합니다. 부모 트랜잭션 커밋 전에 워커가 Job을 처리할 수 있어, 트랜잭션 중 수정한 모델·레코드가 아직 반영되지 않았거나 새로 만든 레코드가 존재하지 않을 수 있습니다.
Laravel에는 이 문제를 해결할 방법이 여러 가지 있습니다. 먼저 큐 연결 설정 배열의 after_commit을 지정할 수 있습니다:
'redis' => [
'driver' => 'redis',
// ...
'after_commit' => true,
],after_commit이 true이면 트랜잭션 안에서 Job을 전달해도 열려 있는 부모 트랜잭션이 커밋될 때까지 실제 전달을 기다립니다. 진행 중인 트랜잭션이 없으면 즉시 전달합니다.
예외로 트랜잭션이 롤백되면 그 안에서 전달한 Job들은 버려집니다.
NOTE
after_commit을 true로 설정하면 큐 이벤트 리스너, Mailable, 알림, 브로드캐스트 이벤트도 열린 모든 트랜잭션이 커밋된 뒤 전달됩니다.
전달 시 커밋 동작 지정
연결의 after_commit이 true가 아니어도 특정 Job은 모든 트랜잭션 커밋 이후 전달하도록 지정할 수 있습니다. 전달 작업에 afterCommit을 연결합니다:
use App\Jobs\ProcessPodcast;
ProcessPodcast::dispatch($podcast)->afterCommit();반대로 after_commit이 true일 때 특정 Job을 커밋까지 기다리지 않고 즉시 전달하도록 지정할 수도 있습니다:
ProcessPodcast::dispatch($podcast)->beforeCommit();Job 체이닝
체이닝은 첫 Job이 성공한 뒤 순서대로 실행할 큐 Job 목록을 지정합니다. 하나가 실패하면 나머지는 실행되지 않습니다. Bus 파사드의 chain을 사용합니다. Laravel 명령 버스는 큐 Job 전달의 기반이 되는 하위 구성 요소입니다:
use App\Jobs\OptimizePodcast;
use App\Jobs\ProcessPodcast;
use App\Jobs\ReleasePodcast;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->dispatch();Job 클래스 인스턴스 외에 클로저도 연결할 수 있습니다:
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
function () {
Podcast::update(/* ... */);
},
])->dispatch();WARNING
Job 내부에서 $this->delete()를 호출해도 이후 체인 Job의 처리는 중단되지 않습니다. 체인의 Job이 실패해야 실행이 중단됩니다.
체인의 연결과 큐
onConnection과 onQueue로 체인에 사용할 연결과 큐를 지정합니다. 개별 Job에 다른 연결이나 큐를 명시하지 않았다면 이 설정을 사용합니다:
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->onConnection('redis')->onQueue('podcasts')->dispatch();체인에 Job 추가
체인 안의 Job에서 기존 체인의 앞이나 뒤에 Job을 추가하려면 prependToChain과 appendToChain을 사용합니다:
/**
* Execute the job.
*/
public function handle(): void
{
// ...
// Prepend to the current chain, run job immediately after current job...
$this->prependToChain(new TranscribePodcast);
// Append to the current chain, run job at end of chain...
$this->appendToChain(new TranscribePodcast);
}체인 실패
catch로 체인의 Job이 실패했을 때 호출할 클로저를 지정합니다. 실패 원인인 Throwable 인스턴스를 받습니다:
use Illuminate\Support\Facades\Bus;
use Throwable;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->catch(function (Throwable $e) {
// A job within the chain has failed...
})->dispatch();WARNING
체인 콜백은 직렬화되어 나중에 큐에서 실행되므로 콜백 안에서 $this를 사용하면 안 됩니다.
큐와 연결 변경
특정 큐로 전달
Job을 여러 큐로 나누면 분류와 워커 배분 우선순위를 조정할 수 있습니다. 이는 설정 파일의 서로 다른 연결로 보내는 것이 아니라 하나의 연결 안에서 특정 큐로 보내는 것입니다. 전달 시 onQueue로 지정합니다:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Store a new podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// Create podcast...
ProcessPodcast::dispatch($podcast)->onQueue('processing');
return redirect('/podcasts');
}
}Job 생성자에서 onQueue로 지정할 수도 있습니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct()
{
$this->onQueue('processing');
}
}특정 연결로 전달
여러 큐 연결을 사용한다면 onConnection으로 Job을 보낼 연결을 지정합니다:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Store a new podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// Create podcast...
ProcessPodcast::dispatch($podcast)->onConnection('sqs');
return redirect('/podcasts');
}
}onConnection과 onQueue를 함께 연결하여 연결과 큐를 모두 지정할 수 있습니다:
ProcessPodcast::dispatch($podcast)
->onConnection('sqs')
->onQueue('processing');Job 생성자에서 onConnection으로 지정할 수도 있습니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct()
{
$this->onConnection('sqs');
}
}큐 라우팅
Queue 파사드의 route로 특정 Job 클래스의 기본 연결과 큐를 정의할 수 있습니다. Job 자체에 설정하지 않고도 특정 큐를 사용하게 할 때 유용합니다.
클래스 외에 인터페이스, 트레이트, 부모 클래스를 전달할 수도 있습니다. 그러면 해당 인터페이스를 구현하거나 트레이트를 사용하거나 부모 클래스를 상속하는 Job이 설정한 연결과 큐를 자동 사용합니다.
보통 서비스 프로바이더의 boot에서 route를 호출합니다:
use App\Concerns\RequiresVideo;
use App\Jobs\ProcessPodcast;
use App\Jobs\ProcessVideo;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Support\Facades\Queue;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts');
Queue::route(RequiresVideo::class, queue: 'video');
Queue::route(ShouldBroadcast::class, queue: 'events');
}연결만 지정하고 큐를 생략하면 기본 큐로 전달됩니다:
Queue::route(ProcessPodcast::class, connection: 'redis');배열로 여러 Job 클래스를 한 번에 라우팅할 수도 있습니다:
Queue::route([
ProcessPodcast::class => ['redis', 'podcasts'], // Connection and queue
ProcessVideo::class => 'videos', // Queue only (uses default connection)
]);NOTE
각 Job에서 라우팅 설정을 개별적으로 재정의할 수 있습니다.
forward는 한 큐의 Job을 다른 큐나 연결로 전달합니다. 개별 Job이나 전달 위치를 수정하지 않고 큐 인프라를 바꿀 때 유용합니다:
Queue::forward('reports', 'reports.fifo', 'sqs');
Queue::forward('payments', connection: 'sqs');
Queue::forward('updates', 'notifications');배열로 여러 큐를 한 번에 전달할 수도 있습니다:
Queue::forward([
'reports' => 'reports.fifo',
'emails' => 'emails.fifo',
], connection: 'sqs');Job에 명시한 연결이 전달 설정의 연결보다 우선합니다.
최대 시도 횟수와 타임아웃 지정
최대 시도 횟수
Job의 시도는 큐 시스템의 핵심 개념이며 여러 고급 기능의 기반입니다. 기본 설정을 변경하기 전에 동작을 이해해야 합니다.
Job을 큐에 넣으면 워커가 가져와 실행을 시도합니다. 이것이 한 번의 Job 시도입니다.
시도했다고 해서 반드시 handle이 실행된 것은 아닙니다. 다음 경우에도 시도가 소비됩니다:
- 실행 중 처리되지 않은 예외가 발생한 경우
$this->release()로 직접 큐에 반환한 경우WithoutOverlapping이나RateLimited등이 잠금을 획득하지 못해 반환한 경우- Job이 타임아웃된 경우
handle이 예외 없이 완료된 경우
무한히 시도하지 않도록 Laravel은 횟수나 시도 가능 시간을 지정하는 방법을 제공합니다.
NOTE
기본 시도 횟수는 한 번입니다. 미들웨어를 사용하거나 직접 Job을 반환한다면 tries로 허용 횟수를 늘려야 할 수 있습니다.
Artisan 명령어의 --tries로 워커 전체의 최대 횟수를 설정할 수 있습니다. Job 자체에 횟수를 지정하면 해당 설정이 우선합니다:
php artisan queue:work --tries=3최대 횟수를 초과하면 실패한 Job으로 간주합니다. 실패한 Job 문서를 참고하세요. queue:work에 --tries=0을 전달하면 무한히 재시도합니다.
클래스의 Tries 속성으로 Job별 최대 횟수를 지정할 수도 있습니다. 명령어의 --tries보다 우선합니다:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Tries;
#[Tries(5)]
class ProcessPodcast implements ShouldQueue
{
// ...
}동적으로 제어하려면 Job에 tries 메서드를 정의합니다:
/**
* Determine number of times the job may be attempted.
*/
public function tries(): int
{
return 5;
}시간 기반 시도
횟수 대신 더 이상 시도하지 않을 시각을 지정할 수도 있습니다. 지정한 시간 안에서는 횟수 제한 없이 시도할 수 있습니다. Job에 retryUntil을 정의하여 DateTime 인스턴스를 반환합니다:
use DateTime;
/**
* Determine the time at which the job should timeout.
*/
public function retryUntil(): DateTime
{
return now()->plus(minutes: 10);
}retryUntil과 tries를 모두 정의하면 retryUntil이 우선합니다.
최대 예외 수
많은 시도를 허용하되 직접 release한 경우가 아니라 처리되지 않은 예외로 재시도가 발생한 횟수가 일정 수에 이르면 실패하게 하려면 Tries와 MaxExceptions를 사용합니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Support\Facades\Redis;
#[Tries(25)]
#[MaxExceptions(3)]
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Execute the job.
*/
public function handle(): void
{
Redis::throttle('key')->allow(10)->every(60)->then(function () {
// Lock obtained, process the podcast...
}, function () {
// Unable to obtain lock...
return $this->release(10);
});
}
}예제는 Redis 잠금을 얻지 못하면 10초 후 재시도하도록 반환하며 최대 25회까지 시도합니다. 다만 처리되지 않은 예외가 세 번 발생하면 실패합니다.
기본적으로 메모리 부족 등 워커가 충돌하거나 종료되어 끝난 시도는 최대 예외 수에 포함되지 않습니다. 포함하려면 CountCrashesAsExceptions를 추가합니다:
use Illuminate\Queue\Attributes\CountCrashesAsExceptions;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Tries;
#[Tries(25)]
#[MaxExceptions(3)]
#[CountCrashesAsExceptions]
class ProcessPodcast implements ShouldQueue
{
// ...
}이 속성이 있으면 워커는 처리 중 캐시에 표식을 저장합니다. 다음 시도에서 표식이 남아 있으면 이전 시도를 예외로 계산합니다.
예외 유형에 따라 재시도 중지
다시 시도하기보다 즉시 실패해야 하는 예외도 있습니다. bootstrap/app.php의 예외 설정 dontRetry로 재시도를 중단할 유형을 지정합니다:
use App\Exceptions\InvalidPodcastSourceException;
use Illuminate\Foundation\Configuration\Exceptions;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->dontRetry([
InvalidPodcastSourceException::class,
]);
})재시도 중단 조건을 세밀하게 제어하려면 dontRetryWhen에 클로저를 전달합니다. true를 반환하면 Job을 실패 상태로 표시하고 재시도하지 않습니다:
use App\Exceptions\PodcastProcessingException;
use Illuminate\Foundation\Configuration\Exceptions;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->dontRetryWhen(function (PodcastProcessingException $e) {
return $e->reason() === 'Subscription expired';
});
})타임아웃
Job 실행 시간을 예상할 수 있다면 타임아웃을 지정할 수 있습니다. 기본값은 60초입니다. 지정한 시간보다 오래 처리하면 워커가 오류와 함께 종료되며, 보통 서버의 프로세스 관리자가 자동으로 재시작합니다.
Artisan의 --timeout으로 최대 실행 시간을 초 단위로 지정합니다:
php artisan queue:work --timeout=30계속 타임아웃되어 최대 시도 횟수를 초과하면 실패 상태로 표시됩니다.
클래스의 Timeout 속성으로 Job별 시간을 지정할 수도 있습니다. 명령어의 타임아웃보다 우선합니다:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Timeout;
#[Timeout(120)]
class ProcessPodcast implements ShouldQueue
{
// ...
}소켓이나 외부 HTTP 연결처럼 I/O가 블로킹되는 작업은 지정한 타임아웃을 따르지 않을 수 있습니다. 이런 API 자체에도 타임아웃을 지정해야 합니다. 예를 들어 Guzzle에서는 연결과 요청 타임아웃을 모두 지정하세요.
WARNING
Job 타임아웃에는 PCNTL PHP 확장이 필요합니다. timeout은 항상 retry after보다 작아야 합니다. 그렇지 않으면 실행이나 타임아웃이 끝나기 전에 재시도될 수 있습니다. queue:work --once에서는 --timeout이 적용되지 않습니다.
타임아웃 시 실패 처리
타임아웃에 도달하면 Job을 실패 상태로 표시하려면 FailOnTimeout을 사용합니다:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\FailOnTimeout;
#[FailOnTimeout]
class ProcessPodcast implements ShouldQueue
{
// ...
}NOTE
기본적으로 타임아웃은 한 번의 시도를 소비하고 재시도가 허용되면 큐로 반환됩니다. 타임아웃 시 실패하도록 설정하면 tries와 관계없이 재시도하지 않습니다.
SQS FIFO와 공정 큐
Laravel은 Amazon SQS FIFO와 공정 큐를 지원합니다. FIFO는 전달된 순서대로 처리하며 메시지 중복 제거를 통해 정확히 한 번 처리하도록 합니다.
FIFO에는 병렬 처리를 구분할 메시지 그룹 ID가 필요합니다. 같은 ID의 Job은 순서대로 처리하고 다른 ID는 동시에 처리할 수 있습니다.
전달 시 onGroup으로 그룹 ID를 지정합니다:
ProcessOrder::dispatch($order)
->onGroup("customer-{$order->customer_id}");FIFO 큐에 그룹을 지정하지 않으면 큐 이름을 메시지 그룹 ID로 사용합니다.
FIFO의 메시지 중복 제거에 사용할 ID를 변경하려면 Job 클래스에 deduplicationId를 구현합니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessSubscriptionRenewal implements ShouldQueue
{
use Queueable;
// ...
/**
* Get the job's deduplication ID.
*/
public function deduplicationId(): string
{
return "renewal-{$this->subscription->id}";
}
}공정 큐
SQS 표준 큐에서는 메시지 그룹을 지정하면 공정 큐 처리가 활성화됩니다. SQS가 그룹을 사용해 테넌트나 워크로드 간 공정한 전달을 유지합니다. Laravel의 추가 설정은 필요하지 않습니다.
전달 시 onGroup을 호출하는 대신 Job 자체에 messageGroup을 정의할 수도 있습니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessOrder implements ShouldQueue
{
use Queueable;
// ...
/**
* Get the job's message group.
*/
public function messageGroup(): string
{
return "customer-{$this->order->customer_id}";
}
}FIFO 리스너, 메일, 알림
FIFO를 사용하는 리스너, 메일, 알림에도 메시지 그룹이 필요합니다. 또는 해당 객체의 큐 인스턴스를 FIFO가 아닌 큐로 보내도 됩니다.
큐 이벤트 리스너에는 messageGroup을 정의합니다. 선택적으로 이벤트를 받아 중복 제거 ID 생성 클로저를 반환하는 deduplicator도 정의할 수 있습니다:
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Closure;
class SendShipmentNotification
{
// ...
/**
* Get the job's message group.
*/
public function messageGroup(): string
{
return 'shipments';
}
/**
* Get the job's deduplicator.
*/
public function deduplicator(OrderShipped $event): Closure
{
return fn () => "shipment-notification-{$event->order->id}";
}
}FIFO 큐에서 처리할 메일 메시지는 전송 시 onGroup을 호출하고 필요하면 withDeduplicator도 사용합니다:
use App\Mail\InvoicePaid;
use Illuminate\Support\Facades\Mail;
$invoicePaid = (new InvoicePaid($invoice))
->onGroup('invoices')
->withDeduplicator(fn () => 'invoices-'.$invoice->id);
Mail::to($request->user())->send($invoicePaid);FIFO 큐에서 처리할 알림도 전송 시 onGroup을 호출하고 필요하면 withDeduplicator를 사용합니다:
use App\Notifications\InvoicePaid;
$invoicePaid = (new InvoicePaid($invoice))
->onGroup('invoices')
->withDeduplicator(fn () => 'invoices-'.$invoice->id);
$user->notify($invoicePaid);큐 장애 조치
failover 드라이버는 Job을 큐에 넣을 때 자동 장애 조치를 제공합니다. 첫 연결이 어떤 이유로든 실패하면 목록의 다음 연결로 전달을 시도합니다. 큐 신뢰성이 중요한 운영 환경에서 가용성을 높이는 데 유용합니다.
설정하려면 failover 드라이버와 순서대로 시도할 연결 이름 배열을 지정합니다. 기본 config/queue.php에는 다음 예제가 포함됩니다:
'failover' => [
'driver' => 'failover',
'connections' => [
'redis',
'database',
'sync',
],
],failover 연결을 설정한 뒤 .env에서 기본 큐 연결로 지정해야 장애 조치를 사용할 수 있습니다:
QUEUE_CONNECTION=failover목록의 각 연결에 대해 최소 하나의 워커를 시작합니다:
php artisan queue:work redisphp artisan queue:work databaseNOTE
원문에서 설명하는 sync, background, deferred 드라이버는 별도의 큐 워커를 실행할 필요가 없습니다.
연결 작업이 실패하여 장애 조치가 활성화되면 Illuminate\Queue\Events\QueueFailedOver가 전달됩니다. 이를 통해 실패를 보고하거나 기록할 수 있습니다.
NOTE
Horizon은 Redis 큐만 관리합니다. 장애 조치 목록에 database가 있다면 Horizon과 함께 일반 php artisan queue:work database 프로세스도 실행해야 합니다.
오류 처리
처리 중 예외가 발생하면 Job이 자동으로 큐에 반환되어 다시 시도할 수 있습니다. 허용된 최대 횟수에 이를 때까지 반복됩니다. 최대 횟수는 queue:work의 --tries나 Job 클래스 자체에 정의합니다. 워커 실행 방법은 아래 문서를 참고하세요.
Job 수동 반환
나중에 다시 시도하도록 큐로 직접 반환하려면 release를 호출합니다:
/**
* Execute the job.
*/
public function handle(): void
{
// ...
$this->release();
}기본적으로 즉시 다시 처리할 수 있게 반환합니다. 정수나 날짜 인스턴스를 전달하면 지정한 시간이 지난 후 처리 가능하도록 할 수 있습니다:
$this->release(10);
$this->release(now()->plus(seconds: 10));Job 수동 실패 처리
Job을 직접 실패 상태로 표시하려면 fail을 호출합니다:
/**
* Execute the job.
*/
public function handle(): void
{
// ...
$this->fail();
}잡은 예외 때문에 실패 처리하려면 해당 예외를 fail에 전달합니다. 문자열 메시지를 전달하면 예외로 변환됩니다:
$this->fail($exception);
$this->fail('Something went wrong.');NOTE
자세한 내용은 실패한 Job 처리를 참고하세요.
특정 예외에서 Job 실패 처리
FailOnException Job 미들웨어는 특정 예외가 발생하면 재시도를 중단합니다. 외부 API 오류 같은 일시적인 예외는 재시도하되 사용자 권한 취소처럼 지속적인 예외는 영구 실패하게 할 수 있습니다:
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Middleware\FailOnException;
use Illuminate\Support\Facades\Http;
#[Tries(3)]
class SyncChatHistory implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(
public User $user,
) {}
/**
* Execute the job.
*/
public function handle(): void
{
$this->user->authorize('sync-chat-history');
$response = Http::throw()->get(
"https://chat.laravel.test/?user={$this->user->uuid}"
);
// ...
}
/**
* Get the middleware the job should pass through.
*/
public function middleware(): array
{
return [
new FailOnException([AuthorizationException::class])
];
}
}Job 배치
배치는 Job 그룹을 병렬 실행하고 모두 끝난 후 후속 작업을 수행할 수 있게 합니다.
먼저 완료율 등 배치 메타데이터를 저장할 테이블의 마이그레이션을 생성해야 합니다. make:queue-batches-table을 사용합니다:
php artisan make:queue-batches-tablephp artisan migrate배치 가능 Job 정의
일반적인 큐 Job을 생성하고 Illuminate\Bus\Batchable을 추가합니다. 이 트레이트의 batch로 현재 Job이 속한 배치를 조회할 수 있습니다:
<?php
namespace App\Jobs;
use Illuminate\Bus\Batchable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ImportCsv implements ShouldQueue
{
use Batchable, Queueable;
/**
* Execute the job.
*/
public function handle(): void
{
if ($this->batch()->cancelled()) {
// Determine if the batch has been cancelled...
return;
}
// Import a portion of the CSV file...
}
}배치 전달
Bus 파사드의 batch로 전달합니다. 완료 콜백과 함께 사용할 때 특히 유용하며, then, catch, finally로 정의합니다. 각 콜백은 Illuminate\Bus\Batch 인스턴스를 받습니다.
여러 워커가 실행되면 배치 Job을 병렬 처리하므로 완료 순서가 추가 순서와 다를 수 있습니다. 순차 실행 방법은 Job 체인과 배치를 참고하세요.
다음 예제는 CSV 파일의 일정 행 범위를 각각 처리하는 Job을 배치로 만듭니다:
use App\Jobs\ImportCsv;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Throwable;
$batch = Bus::batch([
new ImportCsv(1, 100),
new ImportCsv(101, 200),
new ImportCsv(201, 300),
new ImportCsv(301, 400),
new ImportCsv(401, 500),
])->before(function (Batch $batch) {
// The batch has been created but no jobs have been added...
})->progress(function (Batch $batch) {
// A single job has completed successfully...
})->then(function (Batch $batch) {
// All jobs completed successfully...
})->catch(function (Batch $batch, Throwable $e) {
// Batch job failure detected...
})->finally(function (Batch $batch) {
// The batch has finished executing...
})->dispatch();
return $batch->id;$batch->id의 ID는 전달 후 Laravel 명령 버스 조회를 통해 배치 정보를 확인하는 데 사용할 수 있습니다.
WARNING
배치 콜백은 직렬화되어 나중에 큐에서 실행되므로 $this를 사용하면 안 됩니다. 배치 Job은 데이터베이스 트랜잭션으로 감싸므로 암묵적 커밋을 발생시키는 DB 문장도 실행하면 안 됩니다.
배치 이름 지정
Laravel Horizon과 Laravel Telescope 등은 이름이 있는 배치에 더 읽기 쉬운 디버그 정보를 제공할 수 있습니다. 배치 정의 시 name으로 이름을 지정합니다:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// All jobs completed successfully...
})->name('Import CSV')->dispatch();배치 연결과 큐
onConnection과 onQueue로 지정합니다. 배치의 모든 Job은 같은 연결과 큐에서 실행해야 합니다:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// All jobs completed successfully...
})->onConnection('redis')->onQueue('imports')->dispatch();체인과 배치
배치 안에 배열을 넣어 Job 체인을 정의할 수 있습니다. 예를 들어 두 체인을 병렬로 실행하고 둘 다 완료되면 콜백을 실행할 수 있습니다:
use App\Jobs\ReleasePodcast;
use App\Jobs\SendPodcastReleaseNotification;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
Bus::batch([
[
new ReleasePodcast(1),
new SendPodcastReleaseNotification(1),
],
[
new ReleasePodcast(2),
new SendPodcastReleaseNotification(2),
],
])->then(function (Batch $batch) {
// All jobs completed successfully...
})->dispatch();반대로 체인 안에 배치를 정의할 수도 있습니다. 예를 들어 여러 팟캐스트를 게시하는 배치를 먼저 실행하고 알림 전송 배치를 나중에 실행할 수 있습니다:
use App\Jobs\FlushPodcastCache;
use App\Jobs\ReleasePodcast;
use App\Jobs\SendPodcastReleaseNotification;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new FlushPodcastCache,
Bus::batch([
new ReleasePodcast(1),
new ReleasePodcast(2),
]),
Bus::batch([
new SendPodcastReleaseNotification(1),
new SendPodcastReleaseNotification(2),
]),
])->dispatch();배치에 Job 추가
배치 Job 안에서 추가 Job을 넣을 수 있습니다. 웹 요청에서 전달하기에 너무 오래 걸리는 수천 개의 Job을 처리할 때 유용합니다. 먼저 로더 Job의 배치를 전달하고 로더가 나머지 Job을 추가하게 합니다:
$batch = Bus::batch([
new LoadImportBatch,
new LoadImportBatch,
new LoadImportBatch,
])->then(function (Batch $batch) {
// All jobs completed successfully...
})->name('Import Contacts')->dispatch();예제에서는 LoadImportBatch가 추가 Job을 채웁니다. Job의 batch로 얻은 배치 인스턴스의 add를 사용합니다:
use App\Jobs\ImportContacts;
use Illuminate\Support\Collection;
/**
* Execute the job.
*/
public function handle(): void
{
if ($this->batch()->cancelled()) {
return;
}
$this->batch()->add(Collection::times(1000, function () {
return new ImportContacts;
}));
}WARNING
같은 배치에 속한 Job 안에서만 해당 배치에 Job을 추가할 수 있습니다.
배치 확인
완료 콜백에 전달되는 Illuminate\Bus\Batch에는 배치 상태를 확인하고 조작할 다양한 속성과 메서드가 있습니다:
// The UUID of the batch...
$batch->id;
// The name of the batch (if applicable)...
$batch->name;
// The number of jobs assigned to the batch...
$batch->totalJobs;
// The number of jobs that have not been processed by the queue...
$batch->pendingJobs;
// The number of jobs that have failed...
$batch->failedJobs;
// The number of jobs that have been processed thus far...
$batch->processedJobs();
// The completion percentage of the batch (0-100)...
$batch->progress();
// Indicates if the batch has finished executing...
$batch->finished();
// Cancel the execution of the batch...
$batch->cancel();
// Indicates if the batch has been cancelled...
$batch->cancelled();라우트에서 배치 반환
Illuminate\Bus\Batch는 JSON 직렬화가 가능하므로 라우트에서 직접 반환하면 완료 진행률 등 배치 정보를 담은 JSON을 얻을 수 있습니다. UI에 진행 상황을 표시하기 편리합니다.
ID로 조회하려면 Bus 파사드의 findBatch를 사용합니다:
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Route;
Route::get('/batch/{batchId}', function (string $batchId) {
return Bus::findBatch($batchId);
});배치 취소
배치 실행을 취소하려면 Illuminate\Bus\Batch의 cancel을 호출합니다:
/**
* Execute the job.
*/
public function handle(): void
{
if ($this->user->exceedsImportLimit()) {
$this->batch()->cancel();
return;
}
if ($this->batch()->cancelled()) {
return;
}
}앞 예제처럼 배치 Job은 보통 실행 전에 취소 여부를 확인해야 합니다. 대신 SkipIfBatchCancelled 미들웨어를 지정하면 취소된 배치의 Job을 처리하지 않도록 할 수 있습니다:
use Illuminate\Queue\Middleware\SkipIfBatchCancelled;
/**
* Get the middleware the job should pass through.
*/
public function middleware(): array
{
return [new SkipIfBatchCancelled];
}배치 실패
배치 Job이 실패하면 지정한 catch가 호출됩니다. 배치에서 처음 실패한 Job에 대해서만 호출됩니다.
실패 허용
배치 Job 하나가 실패하면 Laravel은 배치를 자동으로 취소 표시합니다. 이를 막으려면 전달 시 allowFailures를 호출합니다:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// All jobs completed successfully...
})->allowFailures()->dispatch();선택적으로 allowFailures에 클로저를 전달하면 각 Job 실패마다 실행합니다:
$batch = Bus::batch([
// ...
])->allowFailures(function (Batch $batch, $exception) {
// Handle individual job failures...
})->dispatch();실패한 배치 Job 재시도
queue:retry-batch로 특정 배치의 실패한 모든 Job을 재시도할 수 있습니다. 배치 UUID를 전달합니다:
php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5배치 정리
정리하지 않으면 job_batches에 레코드가 빠르게 쌓입니다. 매일 queue:prune-batches를 실행하도록 스케줄을 등록해야 합니다:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches')->daily();기본적으로 완료 후 24시간이 지난 배치를 정리합니다. hours로 보관 기간을 조정할 수 있습니다. 다음은 완료 후 48시간이 지난 배치를 삭제합니다:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches --hours=48')->daily();Job이 실패한 뒤 성공적으로 재시도되지 않은 경우처럼 끝내 완료되지 않은 배치가 쌓일 수도 있습니다. unfinished로 미완료 배치 레코드를 정리하도록 지정합니다:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches --hours=48 --unfinished=72')->daily();취소한 배치도 쌓일 수 있습니다. cancelled로 정리할 수 있습니다:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches --hours=48 --cancelled=72')->daily();DynamoDB에 배치 저장
관계형 데이터베이스 대신 DynamoDB에 배치 메타데이터를 저장할 수도 있습니다. 레코드를 저장할 테이블은 직접 생성해야 합니다.
일반적으로 job_batches를 사용하지만 실제 이름은 queue 설정의 queue.batching.table 값에 맞춰야 합니다.
DynamoDB 배치 테이블 설정
job_batches에는 문자열 파티션 키 application과 문자열 정렬 키 id가 있어야 합니다. application에는 app 설정의 name에 정의한 애플리케이션 이름이 들어갑니다. 이름이 키의 일부이므로 여러 Laravel 애플리케이션이 같은 테이블을 사용할 수 있습니다.
자동 배치 정리를 사용하려면 ttl 속성도 정의할 수 있습니다.
DynamoDB 설정
먼저 AWS SDK를 설치하여 DynamoDB와 통신할 수 있게 합니다:
composer require aws/aws-sdk-phpqueue.batching.driver를 dynamodb로 설정하고 batching 배열에 AWS 인증용 key, secret, region을 정의합니다. 이 드라이버에서는 queue.batching.database가 필요하지 않습니다:
'batching' => [
'driver' => env('QUEUE_BATCHING_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'job_batches',
],DynamoDB 배치 정리
DynamoDB에 저장한 배치는 관계형 데이터베이스용 정리 명령어로 제거할 수 없습니다. 대신 DynamoDB 자체 TTL로 오래된 레코드를 자동 제거할 수 있습니다.
테이블에 ttl을 정의했다면 Laravel 설정으로 정리 방법을 지정합니다. queue.batching.ttl_attribute는 TTL을 저장할 속성 이름이고, queue.batching.ttl은 레코드의 마지막 갱신 시점부터 제거 가능해질 때까지의 초 단위 기간입니다:
'batching' => [
'driver' => env('QUEUE_BATCHING_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'job_batches',
'ttl_attribute' => 'ttl',
'ttl' => 60 * 60 * 24 * 7, // 7 days...
],클로저 큐 처리
Job 클래스 대신 클로저도 큐에 전달할 수 있습니다. 현재 요청 주기 밖에서 실행할 간단한 작업에 유용합니다. 큐에 전달하는 클로저의 코드는 전송 중 변조되지 않도록 암호학적으로 서명됩니다:
use App\Models\Podcast;
$podcast = Podcast::find(1);
dispatch(function () use ($podcast) {
$podcast->publish();
});큐 대시보드와 queue:work 출력에 사용할 이름은 name으로 지정합니다:
dispatch(function () {
// ...
})->name('Publish Podcast');catch에 클로저를 전달하면 설정된 재시도를 모두 소진해도 성공하지 못했을 때 실행합니다:
use Throwable;
dispatch(function () use ($podcast) {
$podcast->publish();
})->catch(function (Throwable $e) {
// This job has failed...
});WARNING
catch 콜백은 직렬화되어 나중에 큐에서 실행되므로 안에서 $this를 사용하면 안 됩니다.
큐 워커 실행
`queue:work` 명령어
Laravel의 queue:work Artisan 명령어는 워커를 시작하여 큐에 들어오는 새 Job을 처리합니다. 시작 후에는 직접 멈추거나 터미널을 닫을 때까지 계속 실행됩니다:
php artisan queue:workNOTE
백그라운드에서 계속 실행하려면 Supervisor 같은 프로세스 모니터로 워커가 중단되지 않게 관리해야 합니다.
-v를 지정하면 처리된 Job ID, 연결 이름, 큐 이름을 출력에 포함합니다:
php artisan queue:work -v워커는 시작된 애플리케이션 상태를 메모리에 보관하는 장기 실행 프로세스입니다. 시작 후 코드 변경을 감지하지 않으므로 배포할 때 워커를 재시작해야 합니다. 애플리케이션이 만든 정적 상태도 Job 사이에서 자동 초기화되지 않습니다.
queue:listen을 사용하면 코드 갱신이나 상태 초기화를 위해 직접 재시작할 필요가 없지만 queue:work보다 효율이 크게 떨어집니다:
php artisan queue:listen여러 큐 워커 실행
여러 queue:work 프로세스를 시작하면 한 큐를 여러 워커가 동시에 처리합니다. 로컬에서는 여러 터미널 탭을, 운영 환경에서는 프로세스 관리자 설정을 사용할 수 있습니다. Supervisor에서는 numprocs를 사용합니다.
연결과 큐 지정
워커가 사용할 연결도 지정할 수 있습니다. work에 전달하는 이름은 config/queue.php에 정의한 연결 중 하나여야 합니다:
php artisan queue:work redis기본적으로 해당 연결의 기본 큐만 처리합니다. 특정 큐만 처리하도록 지정할 수도 있습니다. 예를 들어 Redis의 emails 큐만 처리하려면 다음과 같이 실행합니다:
php artisan queue:work redis --queue=emails지정한 수의 Job 처리
--once는 Job 하나만 처리하도록 합니다:
php artisan queue:work --once--max-jobs는 지정한 수를 처리한 뒤 종료합니다. Supervisor와 함께 사용하면 자동으로 재시작하여 누적된 메모리를 해제할 수 있습니다:
php artisan queue:work --max-jobs=1000큐를 모두 처리한 뒤 종료
--stop-when-empty는 모든 Job을 처리한 뒤 정상 종료합니다. 큐가 비면 Docker 컨테이너를 종료하려는 경우 등에 유용합니다:
php artisan queue:work --stop-when-empty지정한 시간 동안 처리
--max-time은 지정한 초 동안 처리한 뒤 종료합니다. Supervisor로 자동 재시작하면 누적 메모리를 해제할 수 있습니다:
# Process jobs for one hour and then exit...php artisan queue:work --max-time=3600워커 대기 시간
Job이 있으면 워커는 작업 사이에 지연 없이 계속 처리합니다. sleep은 Job이 없을 때 대기할 초 단위 시간입니다. 대기 중에는 새 Job을 처리하지 않습니다:
php artisan queue:work --sleep=3유지보수 모드와 큐
유지보수 모드에서는 Job을 처리하지 않으며 모드를 해제하면 다시 처리합니다.
유지보수 모드에서도 처리하려면 --force를 사용합니다:
php artisan queue:work --force리소스 고려 사항
--memory는 워커가 종료할 메모리 한도를 MB로 지정합니다. 기본값은 128MB입니다. PHP의 memory_limit에 대한 비율로도 지정할 수 있습니다:
php artisan queue:work --memory=60%비율을 사용하려면 PHP의 메모리 한도가 유한해야 합니다. 무제한(-1)이면 MB 단위로 지정하세요. Supervisor 같은 모니터가 종료 후 자동 재시작할 수 있습니다.
데몬 워커는 각 Job 전에 프레임워크를 다시 시작하지 않으므로 작업 후 무거운 리소스를 해제해야 합니다. 예를 들어 이미지 처리에 GD 라이브러리를 사용했다면 imagedestroy로 메모리를 해제합니다:
큐 우선순위
큐 처리 순서에 우선순위를 줄 수 있습니다. 예를 들어 Redis 기본 큐가 low여도 특정 Job은 다음처럼 high에 넣을 수 있습니다:
dispatch((new Job)->onQueue('high'));high의 모든 Job을 먼저 처리한 뒤 low로 넘어가는 워커를 실행하려면 쉼표로 구분한 큐 이름 목록을 전달합니다:
php artisan queue:work --queue=high,low큐 워커와 배포
장기 실행 워커는 재시작하지 않으면 코드 변경을 감지하지 못합니다. 배포 과정에서 queue:restart로 정상 재시작하도록 하는 방법이 간단합니다:
php artisan queue:restart이 명령어는 현재 Job을 마친 뒤 워커가 종료하도록 하여 기존 작업을 잃지 않게 합니다. 워커가 종료되므로 Supervisor 등으로 자동 재시작해야 합니다.
NOTE
재시작 신호는 캐시에 저장됩니다. 먼저 캐시 드라이버 설정을 확인하세요.
워커 신호 처리
워커가 Job 처리 중 SIGQUIT, SIGTERM, SIGINT 같은 종료 신호를 받으면 현재 작업을 마친 뒤 종료합니다. 다만 서버나 컨테이너 오케스트레이터가 프로세스를 멈추기 전에 Job이 신호에 반응해야 할 수 있습니다. 예를 들어 긴 가져오기 작업은 새 레코드 처리를 멈추고 진행 상황을 저장해야 합니다.
Job에 Illuminate\Contracts\Queue\Interruptible을 구현하고 interrupted를 정의하면 수신한 신호 번호를 받을 수 있습니다:
<?php
namespace App\Jobs;
use App\Models\Import;
use Illuminate\Contracts\Queue\Interruptible;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ImportProducts implements ShouldQueue, Interruptible
{
use Queueable;
protected bool $shouldStop = false;
/**
* Create a new job instance.
*/
public function __construct(
public Import $import,
) {}
/**
* Execute the job.
*/
public function handle(): void
{
foreach ($this->import->pendingRows() as $row) {
if ($this->shouldStop) {
break;
}
// Import the product row...
}
$this->import->saveProgress();
}
/**
* Handle a signal received by the queue worker.
*/
public function interrupted(int $signal): void
{
$this->shouldStop = true;
}
}interrupted는 Job 실행 중 워커가 프로세스 신호를 받았을 때만 호출됩니다. 타임아웃이나 failed 메서드를 대체하지 않습니다.
Job 만료와 타임아웃
Job 만료
config/queue.php의 각 연결에 있는 retry_after는 처리 중인 Job을 재시도하기까지 기다릴 초 단위 시간입니다. 예를 들어 90이면 반환되거나 삭제되지 않은 채 90초 동안 처리된 Job을 큐로 반환합니다. 보통 Job 처리에 합리적으로 필요한 최대 시간으로 설정해야 합니다.
WARNING
retry_after가 없는 유일한 연결은 Amazon SQS입니다. AWS 콘솔에서 관리하는 기본 가시성 타임아웃을 기준으로 재시도합니다.
워커 타임아웃
queue:work의 --timeout 기본값은 60초입니다. 처리 시간이 이를 초과하면 워커가 오류와 함께 종료됩니다. 보통 서버의 프로세스 관리자가 자동 재시작합니다:
php artisan queue:work --timeout=60retry_after와 CLI의 --timeout은 서로 다른 설정이지만, 작업 유실을 막고 성공적으로 한 번만 처리되도록 함께 작동합니다.
WARNING
--timeout은 retry_after보다 최소 몇 초 짧아야 합니다. 멈춘 Job의 워커가 재시도 전에 종료되도록 하기 위함입니다. 더 길면 같은 Job을 두 번 처리할 수 있습니다.
큐 워커 일시 중지와 재개
워커를 종료하지 않고 새 Job 처리만 일시 중지할 수 있습니다. 시스템 유지보수 등에 유용하며 queue:pause와 queue:continue를 사용합니다.
특정 큐를 중지하려면 연결 이름과 큐 이름을 전달합니다:
php artisan queue:pause database:default예제에서 database는 연결, default는 큐입니다. 워커는 현재 Job을 완료하지만 재개 전까지 새 Job은 가져오지 않습니다.
모든 연결의 모든 큐를 일시 중지하려면 --all을 사용합니다:
php artisan queue:pause --all특정 큐를 재개하려면 queue:continue를 사용합니다:
php artisan queue:continue database:default원문의 전체 재개 예제는 queue:resume과 --all을 사용합니다:
php artisan queue:resume --all재개하면 새 Job을 즉시 처리합니다. 전체 재개는 개별적으로 일시 중지한 큐를 재개하지 않습니다. 일시 중지는 프로세스를 종료하는 것이 아니라 지정한 큐의 새 작업 처리를 막습니다.
워커 재시작과 일시 중지 신호
기본적으로 워커는 매 Job 반복에서 캐시의 재시작·일시 중지 신호를 폴링합니다. 명령에 반응하기 위해 필요하지만 작은 성능 비용이 있습니다.
해당 기능이 필요하지 않고 성능을 최적화하려면 Queue 파사드의 withoutInterruptionPolling으로 전역 비활성화할 수 있습니다. 보통 AppServiceProvider의 boot에서 호출합니다:
use Illuminate\Support\Facades\Queue;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Queue::withoutInterruptionPolling();
}Illuminate\Queue\Worker의 정적 $restartable 또는 $pausable로 각각 비활성화할 수도 있습니다:
use Illuminate\Queue\Worker;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Worker::$restartable = false;
Worker::$pausable = false;
}WARNING
폴링을 비활성화하면 해당 기능에 따라 queue:restart 또는 queue:pause에 반응하지 않습니다.
Supervisor 설정
운영 환경에서는 queue:work가 계속 실행되게 해야 합니다. 타임아웃이나 queue:restart 등 여러 이유로 종료될 수 있습니다.
종료를 감지하여 자동 재시작하는 프로세스 모니터가 필요하며, 동시 워커 수를 지정할 수도 있습니다. Linux에서 자주 사용하는 Supervisor 설정을 살펴보겠습니다.
Supervisor 설치
Supervisor는 실패한 워커를 자동 재시작하는 Linux 프로세스 모니터입니다. Ubuntu에서는 다음 명령어로 설치합니다:
sudo apt-get install supervisorNOTE
직접 설정과 관리가 부담스럽다면 워커 실행을 완전 관리하는 Laravel Cloud를 고려할 수 있습니다.
Supervisor 설정
설정 파일은 보통 /etc/supervisor/conf.d에 있습니다. 여러 파일을 만들어 프로세스 감시 방식을 지정할 수 있습니다. 예를 들어 laravel-worker.conf를 다음처럼 작성합니다:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/app.com/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/home/forge/app.com/worker.log
stopwaitsecs=3600numprocs는 워커 여덟 개를 실행하고 감시하며 실패 시 재시작하도록 합니다. 원하는 연결과 옵션에 맞게 command를 수정해야 합니다.
WARNING
stopwaitsecs는 가장 오래 실행되는 Job보다 길어야 합니다. 그렇지 않으면 완료 전에 Supervisor가 종료할 수 있습니다.
Supervisor 시작
설정 파일 작성 후 다음 명령어로 갱신하고 프로세스를 시작합니다:
sudo supervisorctl rereadsudo supervisorctl updatesudo supervisorctl start "laravel-worker:*"자세한 내용은 Supervisor 문서를 참고하세요.
실패한 Job 처리
Laravel은 최대 시도 횟수를 지정할 수 있게 합니다. 비동기 Job이 이 횟수를 초과하면 failed_jobs에 저장됩니다. 실패한 동기 Job은 이 테이블에 저장되지 않고 예외를 애플리케이션에서 즉시 처리합니다.
새 애플리케이션에는 보통 해당 테이블의 마이그레이션이 있습니다. 없다면 make:queue-failed-table로 생성합니다:
php artisan make:queue-failed-tablephp artisan migrate큐 워커의 --tries로 최대 시도 횟수를 지정합니다. 생략하면 한 번 또는 클래스의 Tries에 지정한 횟수만큼 시도합니다:
php artisan queue:work redis --tries=3--backoff는 예외 후 재시도 전 기다릴 초 단위 시간입니다. 기본적으로는 즉시 큐에 반환하여 재시도할 수 있게 합니다:
php artisan queue:work redis --tries=3 --backoff=3Job별 대기 시간은 클래스의 Backoff 속성으로 지정합니다:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Backoff;
#[Backoff(3)]
class ProcessPodcast implements ShouldQueue
{
// ...
}더 복잡한 계산은 backoff 메서드로 정의합니다:
/**
* Calculate the number of seconds to wait before retrying the job.
*/
public function backoff(): int
{
return 3;
}대기 시간 배열로 점차 증가하는 대기 시간을 설정할 수 있습니다. 예제는 첫 재시도에 1초, 두 번째에 5초, 세 번째와 이후에는 10초씩 기다립니다:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Backoff;
#[Backoff([1, 5, 10])]
class ProcessPodcast implements ShouldQueue
{
// ...
}실패 후 정리
실패 시 알림을 보내거나 부분 완료한 작업을 되돌리려면 Job에 failed를 정의합니다. 실패 원인인 Throwable이 전달됩니다:
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Throwable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* Execute the job.
*/
public function handle(AudioProcessor $processor): void
{
// Process uploaded podcast...
}
/**
* Handle a job failure.
*/
public function failed(?Throwable $exception): void
{
// Send user notification of failure, etc...
}
}WARNING
failed 호출 전에 새 Job 인스턴스를 생성하므로 handle에서 수정한 클래스 속성 값은 사라집니다.
실패는 처리되지 않은 예외뿐 아니라 허용된 시도를 모두 소진한 경우에도 발생합니다. 시도는 다음과 같이 소비될 수 있습니다:
- 타임아웃
- 실행 중 처리되지 않은 예외
- 직접 또는 미들웨어에 의한 큐 반환
최종 시도의 실행 중 예외로 실패하면 그 예외가 failed에 전달됩니다. 최대 시도 횟수 때문이면 Illuminate\Queue\MaxAttemptsExceededException, 타임아웃 때문이면 Illuminate\Queue\TimeoutExceededException이 전달됩니다.
실패한 Job 재시도
failed_jobs에 저장한 모든 실패 Job은 queue:failed로 조회합니다:
php artisan queue:failedID, 연결, 큐, 실패 시각 등의 정보를 표시합니다. ID로 재시도할 수 있습니다. 예를 들어 다음 ID의 Job을 재시도합니다:
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece여러 ID를 전달할 수도 있습니다:
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 91401d2c-0784-4f43-824c-34f94a33c24d특정 큐의 모든 실패 Job도 재시도할 수 있습니다:
php artisan queue:retry --queue=name전체 실패 Job을 재시도하려면 ID로 all을 전달합니다:
php artisan queue:retry all실패 Job을 삭제하려면 queue:forget을 사용합니다:
php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24dNOTE
Horizon에서는 queue:forget 대신 horizon:forget을 사용해야 합니다.
failed_jobs를 모두 비우려면 queue:flush를 사용합니다:
php artisan queue:flushqueue:flush는 오래된 정도와 관계없이 모든 실패 기록을 제거합니다. --hours로 특정 시간 이상 지난 기록만 제거할 수 있습니다:
php artisan queue:flush --hours=48누락된 모델 무시
Job에 전달한 Eloquent 모델은 직렬화되고 처리 시 데이터베이스에서 다시 조회합니다. 기다리는 동안 모델이 삭제되었다면 ModelNotFoundException으로 실패할 수 있습니다.
DeleteWhenMissingModels를 클래스에 적용하면 모델이 없는 Job을 예외 없이 조용히 삭제합니다:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\DeleteWhenMissingModels;
#[DeleteWhenMissingModels]
class ProcessPodcast implements ShouldQueue
{
// ...
}실패한 Job 정리
queue:prune-failed로 failed_jobs 기록을 정리할 수 있습니다:
php artisan queue:prune-failed기본적으로 24시간이 지난 기록을 제거합니다. --hours를 지정하면 최근 N시간 이내에 저장한 기록만 남깁니다. 다음은 48시간이 지난 기록을 삭제합니다:
php artisan queue:prune-failed --hours=48DynamoDB에 실패한 Job 저장
관계형 테이블 대신 DynamoDB에 저장할 수도 있습니다. 테이블은 직접 생성해야 하며 보통 failed_jobs를 쓰지만 실제 이름은 queue 설정의 queue.failed.table에 맞춥니다.
문자열 파티션 키 application과 문자열 정렬 키 uuid가 필요합니다. application에는 app 설정의 name이 들어갑니다. 이름이 키의 일부이므로 여러 Laravel 애플리케이션이 같은 테이블을 사용할 수 있습니다.
DynamoDB와 통신할 수 있도록 AWS SDK도 설치해야 합니다:
composer require aws/aws-sdk-phpqueue.failed.driver를 dynamodb로 설정하고 실패 Job 설정 배열에 AWS 인증용 key, secret, region을 정의합니다. 이 드라이버에서는 queue.failed.database가 필요하지 않습니다:
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'failed_jobs',
],실패한 Job 저장 비활성화
queue.failed.driver를 null로 설정하면 실패 Job을 저장하지 않고 버립니다. 보통 QUEUE_FAILED_DRIVER 환경 변수로 지정합니다:
QUEUE_FAILED_DRIVER=null실패한 Job 이벤트
Job 실패 시 호출할 리스너는 Queue 파사드의 failing으로 등록할 수 있습니다. 예를 들어 AppServiceProvider의 boot에서 클로저를 연결합니다:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;
use Illuminate\Queue\Events\JobFailed;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// ...
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Queue::failing(function (JobFailed $event) {
// $event->connectionName
// $event->job
// $event->exception
});
}
}큐의 Job 비우기
NOTE
Horizon에서는 queue:clear 대신 horizon:clear를 사용해야 합니다.
기본 연결의 기본 큐에서 모든 Job을 삭제하려면 queue:clear를 사용합니다:
php artisan queue:clearconnection 인수와 queue 옵션으로 특정 연결과 큐를 지정할 수도 있습니다:
php artisan queue:clear redis --queue=emailsWARNING
SQS, Redis, database 드라이버에서만 지원합니다. SQS 삭제는 최대 60초가 걸리므로 비운 뒤 60초 안에 보낸 Job도 삭제될 수 있습니다.
큐 모니터링
Job이 갑자기 몰리면 처리 대기가 길어질 수 있습니다. Laravel은 Job 수가 지정한 임계값을 초과할 때 알릴 수 있습니다.
먼저 queue:monitor를 매분 실행하도록 설정합니다. 감시할 큐 이름과 Job 수 임계값을 전달합니다:
php artisan queue:monitor redis:default,redis:deployments --max=100명령어 스케줄만으로 알림이 전송되지는 않습니다. 임계값을 넘으면 Illuminate\Queue\Events\QueueBusy가 전달됩니다. AppServiceProvider에서 이 이벤트를 받아 자신이나 개발팀에 알림을 보낼 수 있습니다:
use App\Notifications\QueueHasLongWaitTime;
use Illuminate\Queue\Events\QueueBusy;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Event::listen(function (QueueBusy $event) {
Notification::route('mail', 'dev@example.com')
->notify(new QueueHasLongWaitTime(
$event->connectionName,
$event->queue,
$event->size
));
});
}테스트
Job 전달 코드를 테스트할 때는 실제 Job을 실행하지 않게 할 수 있습니다. Job 코드는 전달 코드와 분리해 직접 테스트할 수 있기 때문입니다. Job 자체는 인스턴스를 생성하고 테스트에서 handle을 직접 호출합니다.
Queue 파사드의 fake로 실제 큐 전달을 막고 애플리케이션이 Job을 넣으려고 했는지 검증할 수 있습니다:
Pest
<?php
use App\Jobs\AnotherJob;
use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Queue;
test('orders can be shipped', function () {
Queue::fake();
// Perform order shipping...
// Assert that no jobs were pushed...
Queue::assertNothingPushed();
// Assert a job was pushed to a given queue...
Queue::assertPushedOn('queue-name', ShipOrder::class);
// Assert a job was pushed
Queue::assertPushed(ShipOrder::class);
// Assert a job was pushed exactly once...
Queue::assertPushedOnce(ShipOrder::class);
// Assert a job was pushed twice...
Queue::assertPushedTimes(ShipOrder::class, 2);
// Assert a job was not pushed...
Queue::assertNotPushed(AnotherJob::class);
// Assert that a closure was pushed to the queue...
Queue::assertClosurePushed();
// Assert that a closure was not pushed...
Queue::assertClosureNotPushed();
// Assert the total number of jobs that were pushed...
Queue::assertCount(3);
});PHPUnit
<?php
namespace Tests\Feature;
use App\Jobs\AnotherJob;
use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_orders_can_be_shipped(): void
{
Queue::fake();
// Perform order shipping...
// Assert that no jobs were pushed...
Queue::assertNothingPushed();
// Assert a job was pushed to a given queue...
Queue::assertPushedOn('queue-name', ShipOrder::class);
// Assert a job was pushed
Queue::assertPushed(ShipOrder::class);
// Assert a job was pushed exactly once...
Queue::assertPushedOnce(ShipOrder::class);
// Assert a job was pushed twice...
Queue::assertPushedTimes(ShipOrder::class, 2);
// Assert a job was not pushed...
Queue::assertNotPushed(AnotherJob::class);
// Assert that a closure was pushed to the queue...
Queue::assertClosurePushed();
// Assert that a closure was not pushed...
Queue::assertClosureNotPushed();
// Assert the total number of jobs that were pushed...
Queue::assertCount(3);
}
}assertPushed, assertNotPushed, assertClosurePushed, assertClosureNotPushed에 클로저를 전달하여 조건에 부합하는 Job의 전달 여부를 검증할 수 있습니다. 조건을 만족하는 Job이 하나 이상 전달되었다면 전달되었음을 확인하는 검증은 성공합니다:
use Illuminate\Queue\CallQueuedClosure;
Queue::assertPushed(function (ShipOrder $job) use ($order) {
return $job->order->id === $order->id;
});
Queue::assertClosurePushed(function (CallQueuedClosure $job) {
return $job->name === 'validate-order';
});예상 속성 배열을 assertPushed나 assertNotPushed의 두 번째 인수 또는 assertPushedOn의 세 번째 인수로 전달할 수도 있습니다:
Queue::assertPushed(ShipOrder::class, ['order' => $order]);
Queue::assertPushedOn('shipping', ShipOrder::class, ['order' => $order]);지정한 속성이 모두 일치해야 합니다. 일반 값은 엄격한 동등 비교, Eloquent 모델은 is를 사용합니다. Bus의 assertDispatched, assertNotDispatched, assertDispatchedSync, assertDispatchedAfterResponse도 두 번째 인수로 예상 속성 배열을 받습니다.
일부 Job만 가짜 처리
다른 Job은 정상 실행하면서 특정 Job만 가짜 처리하려면 fake에 클래스 이름을 전달합니다:
Pest
test('orders can be shipped', function () {
Queue::fake([
ShipOrder::class,
]);
// Perform order shipping...
// Assert a job was pushed twice...
Queue::assertPushedTimes(ShipOrder::class, 2);
});PHPUnit
public function test_orders_can_be_shipped(): void
{
Queue::fake([
ShipOrder::class,
]);
// Perform order shipping...
// Assert a job was pushed twice...
Queue::assertPushedTimes(ShipOrder::class, 2);
}except로 지정한 Job을 제외한 모두를 가짜 처리할 수도 있습니다:
Queue::fake()->except([
ShipOrder::class,
]);Job 체인 테스트
체인 테스트에는 Bus 파사드의 가짜 처리 기능을 사용합니다. assertChained로 Job 체인이 전달되었는지 검증합니다. 첫 인수는 체인 Job 배열입니다:
use App\Jobs\RecordShipment;
use App\Jobs\ShipOrder;
use App\Jobs\UpdateInventory;
use Illuminate\Support\Facades\Bus;
Bus::fake();
// ...
Bus::assertChained([
ShipOrder::class,
RecordShipment::class,
UpdateInventory::class
]);클래스 이름 배열뿐 아니라 실제 인스턴스 배열도 전달할 수 있습니다. 이 경우 Laravel은 전달된 체인의 Job과 클래스 및 속성 값이 같은지 확인합니다:
Bus::assertChained([
new ShipOrder,
new RecordShipment,
new UpdateInventory,
]);assertDispatchedWithoutChain은 체인 없이 전달된 Job을 검증합니다:
Bus::assertDispatchedWithoutChain(ShipOrder::class);체인 변경 테스트
Job이 체인 앞뒤에 항목을 추가한다면 assertHasChain으로 남은 체인이 예상과 같은지 확인합니다:
$job = new ProcessPodcast;
$job->handle();
$job->assertHasChain([
new TranscribePodcast,
new OptimizePodcast,
new ReleasePodcast,
]);assertDoesntHaveChain으로 남은 체인이 비었는지 확인할 수 있습니다:
$job->assertDoesntHaveChain();체인 안의 배치 테스트
체인에 배치가 포함되면 체인 검증에 Bus::chainedBatch를 넣어 배치가 예상과 같은지 확인합니다:
use App\Jobs\ShipOrder;
use App\Jobs\UpdateInventory;
use Illuminate\Bus\PendingBatch;
use Illuminate\Support\Facades\Bus;
Bus::assertChained([
new ShipOrder,
Bus::chainedBatch(function (PendingBatch $batch) {
return $batch->jobs->count() === 3;
}),
new UpdateInventory,
]);Job 배치 테스트
Bus의 assertBatched는 Job 배치 전달을 검증합니다. 클로저는 Illuminate\Bus\PendingBatch를 받아 배치의 Job을 확인할 수 있습니다:
use Illuminate\Bus\PendingBatch;
use Illuminate\Support\Facades\Bus;
Bus::fake();
// ...
Bus::assertBatched(function (PendingBatch $batch) {
return $batch->name == 'Import CSV' &&
$batch->jobs->count() === 10;
});hasJobs로 예상 Job이 포함되었는지 확인합니다. 인스턴스, 클래스 이름, 클로저의 배열을 받습니다:
Bus::assertBatched(function (PendingBatch $batch) {
return $batch->hasJobs([
new ProcessCsvRow(row: 1),
new ProcessCsvRow(row: 2),
new ProcessCsvRow(row: 3),
]);
});클로저는 Job 인스턴스를 받으며 타입 힌트로 예상 Job 유형을 추론합니다:
Bus::assertBatched(function (PendingBatch $batch) {
return $batch->hasJobs([
fn (ProcessCsvRow $job) => $job->row === 1,
fn (ProcessCsvRow $job) => $job->row === 2,
fn (ProcessCsvRow $job) => $job->row === 3,
]);
});assertBatchCount로 전달된 배치 수를 검증합니다:
Bus::assertBatchCount(3);assertNothingBatched로 배치가 전달되지 않았는지 검증합니다:
Bus::assertNothingBatched();Job과 배치 상호작용 테스트
개별 Job이 배치를 취소하는지 등 배치와의 상호작용도 테스트할 수 있습니다. withFakeBatch로 가짜 배치를 지정합니다. Job과 가짜 배치가 들어 있는 튜플을 반환합니다:
[$job, $batch] = (new ShipOrder)->withFakeBatch();
$job->handle();
$this->assertTrue($batch->cancelled());
$this->assertEmpty($batch->added);Job과 큐 상호작용 테스트
Job이 큐로 자신을 반환하거나 자신을 삭제하는지 테스트하려면 인스턴스를 생성하고 withFakeQueueInteractions를 호출합니다.
상호작용을 가짜 처리한 뒤 handle을 호출하면 여러 검증 메서드로 동작을 확인할 수 있습니다:
use App\Exceptions\CorruptedAudioException;
use App\Jobs\ProcessPodcast;
$job = (new ProcessPodcast)->withFakeQueueInteractions();
$job->handle();
$job->assertReleased(delay: 30);
$job->assertDeleted();
$job->assertNotDeleted();
$job->assertFailed();
$job->assertFailedWith(CorruptedAudioException::class);
$job->assertNotFailed();Job 이벤트
Queue 파사드의 before와 after로 처리 전후 콜백을 지정합니다. 추가 로그나 대시보드 통계에 유용하며 보통 서비스 프로바이더의 boot에서 등록합니다. 기본 AppServiceProvider를 사용하는 예제입니다:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// ...
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Queue::before(function (JobProcessing $event) {
// $event->connectionName
// $event->job
// $event->job->payload()
});
Queue::after(function (JobProcessed $event) {
// $event->connectionName
// $event->job
// $event->job->payload()
});
}
}Queue 파사드의 looping으로 워커가 큐에서 Job을 가져오려 하기 전에 실행할 콜백을 지정할 수 있습니다. 예를 들어 이전에 실패한 Job이 열어 둔 트랜잭션을 롤백하는 클로저를 등록할 수 있습니다:
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
Queue::looping(function () {
while (DB::transactionLevel() > 0) {
DB::rollBack();
}
});워커가 큐에서 Job을 가져오지 못하면 Laravel은 Illuminate\Queue\Events\WorkerIdle도 전달합니다:
use Illuminate\Queue\Events\WorkerIdle;
use Illuminate\Support\Facades\Event;
Event::listen(function (WorkerIdle $event) {
// $event->connectionName
// $event->queue
// $event->workerOptions
});