본문 바로가기

큐

업데이트됨

번역일: 2026년 11월 6일

이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.

원문 수정
2026년 11월 3일
번역 갱신
2026년 11월 6일

큐

소개

웹 애플리케이션을 만들다 보면 업로드된 CSV 파일을 파싱해 저장하는 작업처럼, 일반적인 웹 요청 안에서 처리하기에는 시간이 너무 오래 걸리는 작업이 생기기 마련입니다. Laravel에서는 이런 작업을 백그라운드에서 처리하는 큐 Job을 손쉽게 만들 수 있습니다. 시간이 많이 드는 작업을 큐로 옮기면 애플리케이션은 웹 요청에 훨씬 빠르게 응답할 수 있고, 사용자에게 더 나은 경험을 제공할 수 있습니다.

Laravel 큐는 Amazon SQS, Redis, 관계형 데이터베이스 등 다양한 큐 백엔드를 하나의 통일된 큐 API로 다룰 수 있게 해 줍니다.

Laravel의 큐 설정은 애플리케이션의 config/queue.php 설정 파일에 들어 있습니다. 이 파일에는 프레임워크에 포함된 각 큐 드라이버(데이터베이스, Amazon SQS, Redis, Beanstalkd)의 커넥션 설정이 있으며, 개발이나 테스트 중에 Job을 즉시 실행하는 동기(synchronous) 드라이버도 포함되어 있습니다. 큐에 넣은 Job을 그냥 버리는 null 큐 드라이버도 제공됩니다.

NOTE

Laravel Horizon은 Redis 기반 큐를 위한 대시보드이자 설정 시스템입니다. 자세한 내용은 Horizon 문서를 확인해 보세요.

커넥션과 큐의 차이

Laravel 큐를 사용하기 전에, "커넥션(connection)"과 "큐(queue)"의 차이를 먼저 이해해 두는 것이 중요합니다. config/queue.php 설정 파일에는 connections 배열이 있는데, 이 옵션은 Amazon SQS, Beanstalk, Redis 같은 백엔드 큐 서비스로의 연결을 정의합니다. 그런데 하나의 큐 커넥션 안에는 여러 개의 "큐"가 있을 수 있으며, 큐는 Job이 쌓이는 서로 다른 더미(stack 또는 pile)라고 생각하면 됩니다.

queue 설정 파일의 각 커넥션 설정 예시에는 queue 속성이 들어 있다는 점에 주목하세요. 이 값은 해당 커넥션으로 Job을 보낼 때 기본으로 사용되는 큐입니다. 즉, Job을 디스패치하면서 어느 큐로 보낼지 명시하지 않으면, 커넥션 설정의 queue 속성에 정의된 큐에 Job이 쌓입니다.

use App\Jobs\ProcessPodcast; // 이 Job은 기본 커넥션의 기본 큐로 전송됩니다... ProcessPodcast::dispatch(); // 이 Job은 기본 커넥션의 "emails" 큐로 전송됩니다... ProcessPodcast::dispatch()->onQueue('emails');

어떤 애플리케이션은 여러 큐에 Job을 나눠 넣을 필요 없이 하나의 단순한 큐만 사용해도 충분할 수 있습니다. 하지만 Job 처리의 우선순위를 두거나 처리 대상을 구분하려는 애플리케이션에서는 여러 큐를 쓰는 방식이 특히 유용합니다. Laravel 큐 워커는 어떤 큐를 어떤 우선순위로 처리할지 지정할 수 있기 때문입니다. 예를 들어 high 큐에 Job을 넣었다면, 해당 큐를 더 높은 우선순위로 처리하는 워커를 다음과 같이 실행할 수 있습니다.

php artisan queue:work --queue=high,default

드라이버별 참고 사항과 사전 준비

데이터베이스

database 큐 드라이버를 사용하려면 Job을 저장할 데이터베이스 테이블이 필요합니다. 보통은 Laravel 기본 데이터베이스 마이그레이션인 0001_01_01_000002_create_jobs_table.php에 이 테이블이 포함되어 있습니다. 만약 애플리케이션에 해당 마이그레이션이 없다면 make:queue-table Artisan 명령어로 생성할 수 있습니다.

php artisan make:queue-tablephp artisan migrate

Redis

redis 큐 드라이버를 사용하려면 config/database.php 설정 파일에 Redis 데이터베이스 커넥션을 설정해야 합니다.

여러 애플리케이션 인스턴스가 하나의 Redis 서버를 공유한다면, 각 인스턴스가 서로 다른 키 프리픽스나 Redis 데이터베이스를 사용하도록 해야 합니다. 그렇지 않으면 한 인스턴스의 큐 워커가 다른 인스턴스에서 디스패치한 Job을 처리해 버릴 수 있습니다.

WARNING

redis 큐 드라이버는 Redis의 serializer 및 compression 옵션을 지원하지 않습니다.

Redis 클러스터

Redis 큐 커넥션이 Redis 클러스터를 사용한다면, 큐 이름에 키 해시 태그가 반드시 포함되어야 합니다. 이는 특정 큐에 속한 모든 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을 확인하려고 Redis 데이터베이스를 계속 폴링하는 것보다 더 효율적일 수 있습니다. 예를 들어 값을 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에는 그 위치를 가리키는 포인터만 보내도록 Laravel을 설정할 수 있습니다. 이 기능을 활성화하려면 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이면 크기와 상관없이 모든 SQS 페이로드가 캐시 스토어에 저장됩니다. 큐에 쌓인 Job은 처리될 때 캐시 스토어에서 페이로드를 가져와야 하므로, 워커가 Job을 처리하기 전까지 페이로드를 유지할 수 있는 스토어를 선택해야 합니다. 기본적으로 저장된 페이로드는 해당 Job이 성공적으로 처리되어 SQS에서 삭제된 뒤에 함께 삭제됩니다.

flush_on_clear 옵션이 true이면, queue:clear 명령어로 SQS 큐를 비울 때 설정된 오버플로 캐시 스토어도 함께 플러시됩니다. 캐시 스토어를 플러시하면 해당 스토어의 모든 항목이 사라질 수 있으므로, 이 옵션을 사용할 때는 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 디렉터리에 저장됩니다. app/Jobs 디렉터리가 없다면 make:job Artisan 명령어를 실행할 때 자동으로 생성됩니다.

php artisan make:job ProcessPodcast

생성된 클래스는 Illuminate\Contracts\Queue\ShouldQueue 인터페이스를 구현합니다. 이는 해당 Job을 큐에 넣어 비동기로 실행해야 한다는 것을 Laravel에 알려 줍니다.

NOTE

Job 스텁은 스텁 퍼블리싱을 통해 커스터마이징할 수 있습니다.

클래스 구조

Job 클래스는 매우 단순하며, 보통 큐가 Job을 처리할 때 호출되는 handle 메서드 하나만 가지고 있습니다. 예제 Job 클래스를 살펴보며 시작해 보겠습니다. 이 예제에서는 팟캐스트 퍼블리싱 서비스를 운영하고 있고, 업로드된 팟캐스트 파일을 공개하기 전에 처리해야 한다고 가정합니다.

<?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 { // 업로드된 팟캐스트를 처리합니다... } }

이 예제에서 Eloquent 모델을 큐 Job의 생성자에 직접 전달할 수 있었다는 점에 주목하세요. Job이 사용하는 Queueable 트레이트 덕분에, Eloquent 모델과 이미 로드된 연관 관계는 Job이 처리될 때 자연스럽게 직렬화되고 역직렬화됩니다.

큐 Job이 생성자에서 Eloquent 모델을 받으면, 큐에는 모델의 식별자만 직렬화되어 들어갑니다. Job이 실제로 처리될 때 큐 시스템은 데이터베이스에서 전체 모델 인스턴스와 로드된 연관 관계를 자동으로 다시 조회합니다. 이런 모델 직렬화 방식 덕분에 큐 드라이버로 보내는 Job 페이로드를 훨씬 작게 유지할 수 있습니다.

`handle` 메서드 의존성 주입

handle 메서드는 큐가 Job을 처리할 때 호출됩니다. Job의 handle 메서드에는 의존성을 타입 힌트로 지정할 수 있으며, Laravel 서비스 컨테이너가 이 의존성을 자동으로 주입해 줍니다.

컨테이너가 handle 메서드에 의존성을 주입하는 방식을 완전히 직접 제어하고 싶다면 컨테이너의 bindMethod 메서드를 사용하면 됩니다. 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 함수를 거쳐야 합니다. 그렇지 않으면 Job을 큐에 넣을 때 JSON으로 올바르게 직렬화되지 않을 수 있습니다.

큐에 들어가는 연관 관계

Job이 큐에 들어갈 때는 로드된 Eloquent 모델의 연관 관계도 모두 직렬화되기 때문에, 직렬화된 Job 문자열이 상당히 커질 때가 있습니다. 또한 Job이 역직렬화되면서 모델의 연관 관계를 데이터베이스에서 다시 조회할 때는 연관 관계 전체가 조회됩니다. Job을 큐에 넣는 과정에서 모델이 직렬화되기 전에 연관 관계에 적용해 두었던 제약 조건은 역직렬화 시점에 적용되지 않습니다. 따라서 연관 관계의 일부만 다루고 싶다면, 큐 Job 안에서 해당 연관 관계에 제약 조건을 다시 지정해야 합니다.

또는 연관 관계가 직렬화되지 않도록, 프로퍼티 값을 설정할 때 모델의 withoutRelations 메서드를 호출할 수도 있습니다. 이 메서드는 로드된 연관 관계가 없는 모델 인스턴스를 반환합니다.

/** * Create a new job instance. */ public function __construct( Podcast $podcast, ) { $this->podcast = $podcast->withoutRelations(); }

특정 연관 관계만 제거하고 나머지는 그대로 유지하고 싶다면 withoutRelation 메서드를 사용할 수 있습니다.

$this->podcast = $podcast->withoutRelation('comments');

PHP 생성자 프로퍼티 승격(constructor property promotion)을 사용하면서 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, ) {} }

Job이 단일 모델이 아니라 Eloquent 모델의 컬렉션이나 배열을 받는 경우, 그 컬렉션 안의 모델들은 Job이 역직렬화되어 실행될 때 연관 관계가 복원되지 않습니다. 많은 수의 모델을 다루는 Job에서 리소스가 과도하게 사용되는 것을 막기 위한 동작입니다.

고유 Job

WARNING

고유 Job을 사용하려면 락을 지원하는 캐시 드라이버가 필요합니다. 현재 memcached, redis, dynamodb, database, file, array 캐시 드라이버가 원자적 락(atomic lock)을 지원합니다.

WARNING

고유 Job 제약은 배치 안에 포함된 Job에는 적용되지 않습니다.

특정 Job의 인스턴스가 어느 시점에든 큐에 하나만 존재하도록 보장하고 싶을 때가 있습니다. Job 클래스에 ShouldBeUnique 인터페이스를 구현하면 됩니다. 이 인터페이스는 클래스에 추가로 정의해야 하는 메서드가 없습니다.

<?php use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldBeUnique; class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique { // ... }

위 예제에서 UpdateSearchIndex Job은 고유합니다. 따라서 같은 Job의 다른 인스턴스가 이미 큐에 있고 아직 처리가 끝나지 않았다면, 이 Job은 디스패치되지 않습니다.

경우에 따라서는 Job을 고유하게 만드는 특정 "키"를 지정하거나, 일정 시간이 지나면 더 이상 고유하게 유지되지 않도록 타임아웃을 지정하고 싶을 수 있습니다. 이럴 때는 UniqueFor 어트리뷰트를 사용하고 Job 클래스에 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; } }

위 예제에서 UpdateSearchIndex Job은 상품 ID를 기준으로 고유합니다. 따라서 같은 상품 ID로 새로 디스패치되는 Job은 기존 Job의 처리가 끝날 때까지 무시됩니다. 또한 기존 Job이 1시간 안에 처리되지 않으면 고유 락이 해제되어, 같은 고유 키를 가진 다른 Job을 큐에 디스패치할 수 있습니다.

WARNING

애플리케이션이 여러 웹 서버나 컨테이너에서 Job을 디스패치한다면, Laravel이 Job의 고유 여부를 정확하게 판단할 수 있도록 모든 서버가 같은 중앙 캐시 서버와 통신하게 해야 합니다.

처리가 시작될 때까지만 Job을 고유하게 유지하기

기본적으로 고유 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 키로 락을 얻으려고 시도합니다. 이미 락이 잡혀 있으면 Job은 디스패치되지 않습니다. 이 락은 Job의 처리가 완료되거나 모든 재시도가 실패하면 해제됩니다. 기본적으로 Laravel은 기본 캐시 드라이버를 사용해 이 락을 얻습니다. 락을 얻을 때 다른 드라이버를 사용하고 싶다면, 사용할 캐시 드라이버를 반환하는 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

같은 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; } }

위 예제에서 같은 상품에 대해 30초 이내에 UpdateSearchIndex를 반복해서 디스패치하면 Job이 디바운스되어, 가장 마지막 디스패치만 실행됩니다.

자주 다시 디스패치되는 Job이 최대 얼마까지 미뤄질 수 있는지 상한을 두고 싶다면, DebounceFor 어트리뷰트에 maxWait 인자를 지정할 수 있습니다.

#[DebounceFor(30, maxWait: 120)] class UpdateSearchIndex implements ShouldQueue { use Queueable; // ... }

Job에 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과 고유 Job은 함께 사용할 수 없습니다. DebounceFor 어트리뷰트를 사용하는 Job은 ShouldBeUnique를 구현하지 않아야 합니다.

WARNING

애플리케이션이 여러 웹 서버나 컨테이너에서 디바운스 Job을 디스패치한다면, 모든 서버가 같은 중앙 캐시 서버와 통신하도록 해야 합니다.

암호화된 Job

Laravel은 암호화를 통해 Job 데이터의 기밀성과 무결성을 보장할 수 있습니다. 시작하려면 Job 클래스에 ShouldBeEncrypted 인터페이스를 추가하기만 하면 됩니다. 이 인터페이스가 클래스에 추가되면 Laravel은 Job을 큐에 넣기 전에 자동으로 암호화합니다.

<?php use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted { // ... }

Job 미들웨어

Job 미들웨어를 사용하면 큐 Job의 실행 과정을 사용자 정의 로직으로 감쌀 수 있어, Job 자체의 반복적인 보일러플레이트 코드를 줄일 수 있습니다. 예를 들어 Laravel의 Redis 요청 제한 기능을 활용해 5초에 Job 하나만 처리되도록 하는 다음 handle 메서드를 살펴보겠습니다.

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...'); // Job을 처리합니다... }, function () { // 락을 얻지 못했습니다... return $this->release(5); }); }

이 코드는 올바르게 동작하지만, handle 메서드에 Redis 요청 제한 로직이 뒤섞여 있어 구현이 지저분해집니다. 게다가 요청을 제한하고 싶은 다른 Job마다 이 로직을 똑같이 복사해야 합니다. handle 메서드 안에서 요청을 제한하는 대신, 요청 제한을 처리하는 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) { // 락을 얻었습니다... $next($job); }, function () use ($job) { // 락을 얻지 못했습니다... $job->release(5); }); } }

보시다시피 라우트 미들웨어와 마찬가지로, Job 미들웨어는 처리 중인 Job과 Job 처리를 이어 가기 위해 호출해야 하는 콜백을 전달받습니다.

make:job-middleware Artisan 명령어로 새 Job 미들웨어 클래스를 생성할 수 있습니다. Job 미들웨어를 만든 뒤에는 Job의 middleware 메서드에서 반환하는 방식으로 Job에 연결합니다. make:job Artisan 명령어로 생성한 Job에는 이 메서드가 없으므로, 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]; }

NOTE

Job 미들웨어는 큐 처리되는 이벤트 리스너, 메일러블, 알림에도 지정할 수 있습니다.

요청 제한(Rate Limiting)

방금 요청 제한 Job 미들웨어를 직접 작성하는 방법을 살펴봤지만, Laravel에는 Job의 요청 제한에 활용할 수 있는 요청 제한 미들웨어가 이미 포함되어 있습니다. 라우트 요청 제한자와 마찬가지로, Job 요청 제한자도 RateLimiter 파사드의 for 메서드로 정의합니다.

예를 들어 일반 사용자는 데이터 백업을 한 시간에 한 번만 하도록 하되, 프리미엄 고객에게는 그런 제한을 두지 않으려 할 수 있습니다. 이를 위해 AppServiceProvider의 boot 메서드에서 RateLimiter를 정의합니다.

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이 요청 제한을 초과할 때마다 이 미들웨어는 제한 시간에 맞는 적절한 지연을 두고 Job을 큐로 다시 돌려보냅니다(release).

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을 큐로 다시 돌려보내더라도 Job의 총 attempts(시도 횟수)는 계속 증가합니다. 따라서 Job 클래스의 Tries와 MaxExceptions 어트리뷰트를 그에 맞게 조정하는 것이 좋습니다. 또는 retryUntil 메서드를 사용해 더 이상 Job을 시도하지 않을 시점을 정의할 수도 있습니다.

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를 사용한다면 Illuminate\Queue\Middleware\RateLimitedWithRedis 미들웨어를 사용할 수 있습니다. 이 미들웨어는 Redis에 맞게 세밀하게 튜닝되어 있어 기본 요청 제한 미들웨어보다 효율적입니다.

use Illuminate\Queue\Middleware\RateLimitedWithRedis; public function middleware(): array { return [new RateLimitedWithRedis('backups')]; }

connection 메서드로 미들웨어가 사용할 Redis 커넥션을 지정할 수 있습니다.

return [(new RateLimitedWithRedis('backups'))->connection('limiter')];

Job 중복 실행 방지

Laravel에는 임의의 키를 기준으로 Job의 중복 실행을 막아 주는 Illuminate\Queue\Middleware\WithoutOverlapping 미들웨어가 포함되어 있습니다. 큐 Job이 한 번에 하나의 Job만 수정해야 하는 리소스를 수정할 때 유용합니다.

예를 들어 사용자의 신용 점수를 갱신하는 큐 Job이 있고, 같은 사용자 ID에 대한 신용 점수 갱신 Job이 겹쳐 실행되지 않도록 하고 싶다고 해 보겠습니다. 이럴 때는 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을 큐로 다시 돌려보내더라도 Job의 총 시도 횟수는 계속 증가합니다. 따라서 Job 클래스의 Tries와 MaxExceptions 어트리뷰트를 그에 맞게 조정하는 것이 좋습니다. 예를 들어 Tries를 기본값인 1로 두면, 중복 실행된 Job은 나중에 다시 시도되지 않습니다.

같은 종류의 중복 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의 원자적 락(atomic lock) 기능을 기반으로 동작합니다. 때로는 Job이 예기치 않게 실패하거나 타임아웃되어 락이 해제되지 않을 수 있습니다. 따라서 expireAfter 메서드로 락 만료 시간을 명시적으로 정의할 수 있습니다. 예를 들어 아래 예제는 Job 처리가 시작된 지 3분이 지나면 WithoutOverlapping 락을 해제하도록 Laravel에 지시합니다.

/** * 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 미들웨어는 같은 클래스의 Job끼리만 중복 실행을 막습니다. 따라서 서로 다른 두 Job 클래스가 같은 락 키를 사용하더라도 중복 실행이 방지되지는 않습니다. 하지만 shared 메서드를 사용하면 Laravel이 그 키를 Job 클래스 간에 공통으로 적용하도록 지시할 수 있습니다.

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(), ]; } }

예외 발생 횟수 제한(Throttling Exceptions)

Laravel에는 예외 발생을 제한(throttle)할 수 있는 Illuminate\Queue\Middleware\ThrottlesExceptions 미들웨어가 포함되어 있습니다. Job이 지정한 횟수만큼 예외를 던지면, 지정한 시간 간격이 지날 때까지 그 Job을 실행하려는 이후의 모든 시도가 지연됩니다. 이 미들웨어는 불안정한 서드파티 서비스와 연동하는 Job에서 특히 유용합니다.

예를 들어 서드파티 API와 연동하는 큐 Job에서 해당 API가 예외를 던지기 시작했다고 가정해 보겠습니다. 예외 발생을 제한하려면 Job의 middleware 메서드에서 ThrottlesExceptions 미들웨어를 반환하면 됩니다. 일반적으로 이 미들웨어는 시간 기반 시도를 구현한 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이 제한되기 전까지 던질 수 있는 예외 횟수이고, 두 번째 생성자 인자는 제한된 Job을 다시 시도하기까지 경과해야 하는 시간(초)입니다. 위 코드 예제에서는 Job이 연속으로 10번 예외를 던지면 5분을 기다린 뒤 Job을 다시 시도하며, 이는 30분의 시간 제한 안에서만 이루어집니다.

Job이 예외를 던졌지만 아직 예외 임계값에 도달하지 않았다면, Job은 보통 즉시 재시도됩니다. 하지만 미들웨어를 Job에 연결할 때 backoff 메서드를 호출하면 이런 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, 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의 클래스 이름이 캐시 "키"로 사용됩니다. 미들웨어를 Job에 연결할 때 by 메서드를 호출하면 이 키를 재정의할 수 있습니다. 같은 서드파티 서비스와 연동하는 Job이 여러 개 있을 때, 이 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')]; }

기본적으로 이 미들웨어는 모든 예외를 제한합니다. 미들웨어를 Job에 연결할 때 when 메서드를 호출하면 이 동작을 바꿀 수 있습니다. 이 경우 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)]; }

제한된 예외를 애플리케이션의 예외 핸들러에 보고하고 싶다면, 미들웨어를 Job에 연결할 때 report 메서드를 호출하면 됩니다. 선택적으로 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를 사용한다면 Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis 미들웨어를 사용할 수 있습니다. 이 미들웨어는 Redis에 맞게 세밀하게 튜닝되어 있어 기본 예외 제한 미들웨어보다 효율적입니다.

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 다시 풀어주기(Releasing)

Release 미들웨어를 사용하면 Job을 실행하지 않고 큐로 다시 돌려보낼 수 있습니다. Release::when 메서드는 주어진 조건이 true로 평가되면 Job을 되돌리고, Release::unless 메서드는 조건이 false로 평가되면 Job을 되돌립니다.

use Illuminate\Queue\Middleware\Release; /** * Get the middleware the job should pass through. */ public function middleware(): array { return [ Release::when($condition, releaseAfter: 60), ]; }

Job을 큐로 다시 돌려보내더라도 Job의 총 시도 횟수는 계속 증가합니다. 따라서 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의 로직을 수정하지 않고도 Job을 건너뛰거나(삭제) 하도록 지정할 수 있습니다. Skip::when 메서드는 주어진 조건이 true로 평가되면 Job을 삭제하고, Skip::unless 메서드는 조건이 false로 평가되면 Job을 삭제합니다.

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 메서드로 디스패치할 수 있습니다. 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'); } }

Job을 조건부로 디스패치하고 싶다면 dispatchIf와 dispatchUnless 메서드를 사용할 수 있습니다.

ProcessPodcast::dispatchIf($accountActive, $podcast); ProcessPodcast::dispatchUnless($accountSuspended, $podcast);

새로 만든 Laravel 애플리케이션에서는 database 커넥션이 기본 큐로 정의되어 있습니다. 애플리케이션의 .env 파일에서 QUEUE_CONNECTION 환경 변수를 변경하면 다른 기본 큐 커넥션을 지정할 수 있습니다.

지연 디스패치

Job을 큐 워커가 즉시 처리할 수 없도록 지정하고 싶다면 Job을 디스패치할 때 delay 메서드를 사용하면 됩니다. 예를 들어 Job을 디스패치한 지 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에 기본 지연 시간이 설정되어 있을 수 있습니다. 이 지연을 건너뛰고 Job을 즉시 처리하도록 디스패치해야 한다면 withoutDelay 메서드를 사용하세요.

ProcessPodcast::dispatch($podcast)->withoutDelay();

WARNING

Amazon SQS 큐 서비스의 최대 지연 시간은 15분입니다.

동기 디스패치

Job을 즉시(동기적으로) 디스패치하고 싶다면 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(/* ... */); // 팟캐스트를 생성합니다... ProcessPodcast::dispatchSync($podcast); return redirect('/podcasts'); } }

지연 실행(Deferred) 디스패치

지연된 동기 디스패치(deferred synchronous dispatching)를 사용하면, Job을 현재 프로세스 안에서 처리하되 HTTP 응답이 사용자에게 전송된 뒤에 처리하도록 디스패치할 수 있습니다. 덕분에 사용자의 애플리케이션 사용 경험을 느리게 만들지 않으면서 "큐에 넣은" Job을 동기적으로 처리할 수 있습니다. 동기 Job의 실행을 미루려면 deferred 커넥션으로 Job을 디스패치하세요.

RecordDelivery::dispatch($order)->onConnection('deferred');

deferred 커넥션은 기본 페일오버 큐 역할도 합니다.

마찬가지로 background 커넥션도 HTTP 응답이 사용자에게 전송된 뒤에 Job을 처리합니다. 다만 Job이 별도로 생성된 PHP 프로세스에서 처리되므로, PHP-FPM / 애플리케이션 워커는 다른 HTTP 요청을 처리할 수 있는 상태로 남게 됩니다.

RecordDelivery::dispatch($order)->onConnection('background');

대량 디스패치

서로 독립적인 Job을 한꺼번에 많이 디스패치해야 하고 배치 추적이나 콜백이 필요 없다면 Bus 파사드의 bulk 메서드를 사용할 수 있습니다. Laravel은 Job을 설정된 큐 커넥션과 큐 이름별로 묶은 뒤, 각 그룹을 해당 큐에 한꺼번에 푸시합니다.

use App\Jobs\ProcessUser; use Illuminate\Support\Facades\Bus; Bus::bulk( $users->map(fn ($user) => new ProcessUser($user)) );

디스패치 전 Job 준비하기

Job이 큐에 푸시되기 전에 자신의 상태를 준비하거나 점검해야 한다면, Job에 Illuminate\Contracts\Queue\PreparesForDispatch 인터페이스를 구현할 수 있습니다. Laravel은 Job을 디스패치하기 전에 Job의 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이 실제로 성공적으로 실행될 수 있는지 특별히 신경 써야 합니다. 트랜잭션 안에서 Job을 디스패치하면, 부모 트랜잭션이 커밋되기 전에 워커가 Job을 처리할 가능성이 있습니다. 이런 일이 생기면 데이터베이스 트랜잭션 중에 모델이나 데이터베이스 레코드에 수행한 변경 사항이 아직 데이터베이스에 반영되지 않았을 수 있습니다. 또한 트랜잭션 안에서 생성한 모델이나 데이터베이스 레코드가 데이터베이스에 아직 존재하지 않을 수도 있습니다.

다행히 Laravel은 이 문제를 우회하는 몇 가지 방법을 제공합니다. 먼저 큐 커넥션의 설정 배열에서 after_commit 커넥션 옵션을 설정할 수 있습니다.

'redis' => [ 'driver' => 'redis', // ... 'after_commit' => true, ],

after_commit 옵션이 true이면 데이터베이스 트랜잭션 안에서 Job을 디스패치해도 되며, Laravel은 열려 있는 부모 데이터베이스 트랜잭션이 커밋될 때까지 기다렸다가 Job을 실제로 디스패치합니다. 물론 현재 열려 있는 데이터베이스 트랜잭션이 없다면 Job은 즉시 디스패치됩니다.

트랜잭션 도중 발생한 예외로 트랜잭션이 롤백되면, 그 트랜잭션 안에서 디스패치된 Job은 폐기됩니다.

NOTE

after_commit 설정 옵션을 true로 지정하면, 큐 처리되는 이벤트 리스너, 메일러블, 알림, 브로드캐스트 이벤트도 열려 있는 모든 데이터베이스 트랜잭션이 커밋된 뒤에 디스패치됩니다.

커밋 후 디스패치 동작을 개별로 지정하기

큐 커넥션 설정의 after_commit 옵션을 true로 지정하지 않았더라도, 특정 Job만 열려 있는 모든 데이터베이스 트랜잭션이 커밋된 뒤에 디스패치되도록 지정할 수 있습니다. 디스패치 작업에 afterCommit 메서드를 체이닝하면 됩니다.

use App\Jobs\ProcessPodcast; ProcessPodcast::dispatch($podcast)->afterCommit();

반대로 after_commit 설정 옵션이 true로 지정되어 있더라도, 특정 Job은 열려 있는 데이터베이스 트랜잭션의 커밋을 기다리지 않고 즉시 디스패치되도록 지정할 수 있습니다.

ProcessPodcast::dispatch($podcast)->beforeCommit();

Job 체이닝

Job 체이닝을 사용하면 주 Job이 성공적으로 실행된 뒤에 순서대로 실행할 큐 Job 목록을 지정할 수 있습니다. 순서 중 한 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의 처리는 막을 수 없습니다. 체인은 체인 안의 Job이 실패했을 때만 실행을 멈춥니다.

체인의 커넥션과 큐

체인으로 연결된 Job에 사용할 커넥션과 큐를 지정하고 싶다면 onConnection과 onQueue 메서드를 사용할 수 있습니다. 이 메서드들은 큐 Job에 다른 커넥션 / 큐가 명시적으로 지정되어 있지 않은 한, 사용할 큐 커넥션과 큐 이름을 지정합니다.

Bus::chain([ new ProcessPodcast, new OptimizePodcast, new ReleasePodcast, ])->onConnection('redis')->onQueue('podcasts')->dispatch();

체인에 Job 추가하기

가끔은 체인 안의 다른 Job 내부에서 기존 Job 체인의 앞이나 뒤에 Job을 추가해야 할 수 있습니다. 이럴 때는 prependToChain과 appendToChain 메서드를 사용합니다.

/** * Execute the job. */ public function handle(): void { // ... // 현재 체인의 맨 앞에 추가합니다. 현재 Job 직후에 바로 실행됩니다... $this->prependToChain(new TranscribePodcast); // 현재 체인의 맨 뒤에 추가합니다. 체인의 마지막에 실행됩니다... $this->appendToChain(new TranscribePodcast); }

체인 실패 처리

Job을 체이닝할 때 catch 메서드를 사용하면, 체인 안의 Job이 실패했을 때 호출할 클로저를 지정할 수 있습니다. 전달된 콜백은 Job 실패의 원인이 된 Throwable 인스턴스를 받습니다.

use Illuminate\Support\Facades\Bus; use Throwable; Bus::chain([ new ProcessPodcast, new OptimizePodcast, new ReleasePodcast, ])->catch(function (Throwable $e) { // 체인 안의 Job 중 하나가 실패했습니다... })->dispatch();

WARNING

체인 콜백은 직렬화되어 Laravel 큐가 나중에 실행하므로, 체인 콜백 안에서는 $this 변수를 사용하지 않아야 합니다.

큐와 커넥션 지정하기

특정 큐로 디스패치하기

Job을 서로 다른 큐에 푸시하면 큐 Job을 "분류"할 수 있고, 각 큐에 몇 개의 워커를 할당할지로 우선순위를 둘 수도 있습니다. 이는 큐 설정 파일에 정의된 서로 다른 큐 "커넥션"으로 Job을 푸시하는 것이 아니라, 하나의 커넥션 안에 있는 특정 큐로만 푸시한다는 점에 유의하세요. 큐를 지정하려면 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(/* ... */); // 팟캐스트를 생성합니다... ProcessPodcast::dispatch($podcast)->onQueue('processing'); return redirect('/podcasts'); } }

또는 Job의 생성자 안에서 onQueue 메서드를 호출해 Job의 큐를 지정할 수도 있습니다.

<?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(/* ... */); // 팟캐스트를 생성합니다... ProcessPodcast::dispatch($podcast)->onConnection('sqs'); return redirect('/podcasts'); } }

onConnection과 onQueue 메서드를 함께 체이닝해 Job의 커넥션과 큐를 모두 지정할 수 있습니다.

ProcessPodcast::dispatch($podcast) ->onConnection('sqs') ->onQueue('processing');

또는 Job의 생성자 안에서 onConnection 메서드를 호출해 Job의 커넥션을 지정할 수도 있습니다.

<?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이 항상 특정 큐를 사용하도록 보장하고 싶을 때 유용합니다.

특정 Job 클래스뿐 아니라 인터페이스, 트레이트, 부모 클래스를 route 메서드에 전달할 수도 있습니다. 이렇게 하면 해당 인터페이스를 구현하거나, 트레이트를 사용하거나, 부모 클래스를 상속하는 모든 Job이 설정된 커넥션과 큐를 자동으로 사용합니다.

보통 route 메서드는 서비스 프로바이더의 boot 메서드에서 호출합니다.

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'); }

큐 없이 커넥션만 지정하면 Job은 기본 큐로 전송됩니다.

Queue::route(ProcessPodcast::class, connection: 'redis');

route 메서드에 배열을 전달해 여러 Job 클래스를 한꺼번에 라우팅할 수도 있습니다.

Queue::route([ ProcessPodcast::class => ['redis', 'podcasts'], // 커넥션과 큐 ProcessVideo::class => 'videos', // 큐만 지정 (기본 커넥션 사용) ]);

NOTE

큐 라우팅은 Job 단위로 해당 Job에서 여전히 재정의할 수 있습니다.

forward 메서드를 사용하면 한 큐의 Job을 다른 큐 및 / 또는 커넥션으로 전달(forward)할 수 있습니다. 개별 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에 명시적으로 설정된 커넥션은 전달(forward)된 커넥션보다 우선합니다.

최대 시도 횟수 / 타임아웃 값 지정

최대 시도 횟수

Job 시도(attempt)는 Laravel 큐 시스템의 핵심 개념이며 여러 고급 기능의 기반이 됩니다. 처음에는 헷갈릴 수 있지만, 기본 설정을 변경하기 전에 동작 방식을 이해해 두는 것이 중요합니다.

Job이 디스패치되면 큐에 푸시됩니다. 그러면 워커가 Job을 가져가 실행을 시도하는데, 이것이 하나의 Job 시도입니다.

하지만 시도가 반드시 Job의 handle 메서드가 실행되었다는 뜻은 아닙니다. 시도는 다음과 같은 여러 경우에도 "소모"될 수 있습니다.

  • Job이 실행 중 처리되지 않은 예외를 만났을 때
  • $this->release()로 Job을 직접 큐에 되돌렸을 때
  • WithoutOverlapping이나 RateLimited 같은 미들웨어가 락을 얻지 못해 Job을 되돌렸을 때
  • Job이 타임아웃되었을 때
  • Job의 handle 메서드가 예외 없이 실행을 마쳤을 때

Job을 무한정 계속 시도하는 것은 대개 원하지 않을 것입니다. 그래서 Laravel은 Job을 몇 번, 또는 얼마 동안 시도할 수 있는지 지정하는 여러 방법을 제공합니다.

NOTE

기본적으로 Laravel은 Job을 한 번만 시도합니다. Job이 WithoutOverlapping이나 RateLimited 같은 미들웨어를 사용하거나 Job을 직접 되돌린다면, 대개 tries 옵션으로 허용 시도 횟수를 늘려야 합니다.

Job을 최대 몇 번까지 시도할 수 있는지 지정하는 한 가지 방법은 Artisan 명령줄의 --tries 스위치입니다. 이 값은 처리 중인 Job이 자체적으로 시도 가능 횟수를 지정하지 않는 한, 워커가 처리하는 모든 Job에 적용됩니다.

php artisan queue:work --tries=3

Job이 최대 시도 횟수를 초과하면 "실패한" Job으로 간주됩니다. 실패한 Job 처리에 관한 자세한 내용은 실패한 Job 문서를 참고하세요. queue:work 명령어에 --tries=0을 지정하면 Job은 무한정 재시도됩니다.

Job 클래스에서 Tries 어트리뷰트로 최대 시도 횟수를 직접 정의하면 더 세밀하게 제어할 수 있습니다. Job에 최대 시도 횟수가 지정되어 있으면 명령줄에서 지정한 --tries 값보다 우선합니다.

<?php namespace App\Jobs; use Illuminate\Queue\Attributes\Tries; #[Tries(5)] class ProcessPodcast implements ShouldQueue { // ... }

특정 Job의 최대 시도 횟수를 동적으로 제어해야 한다면 Job에 tries 메서드를 정의할 수 있습니다.

/** * Determine number of times the job may be attempted. */ public function tries(): int { return 5; }

시간 기반 시도

Job이 실패하기까지 몇 번 시도할 수 있는지 정의하는 대신, 더 이상 Job을 시도하지 않을 시각을 정의할 수도 있습니다. 이렇게 하면 지정한 기간 안에서는 Job을 몇 번이든 시도할 수 있습니다. Job을 더 이상 시도하지 않을 시각을 정의하려면 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가 모두 정의되어 있으면 Laravel은 retryUntil 메서드를 우선합니다.

NOTE

큐 처리되는 이벤트 리스너와 큐 처리되는 알림에도 Tries 어트리뷰트나 retryUntil 메서드를 정의할 수 있습니다.

최대 예외 횟수

Job을 여러 번 시도할 수 있게 하되, 재시도의 원인이 처리되지 않은 예외(release 메서드로 직접 되돌린 경우가 아니라)인 횟수가 지정한 만큼 쌓이면 실패하도록 하고 싶을 때가 있습니다. 이럴 때는 Job 클래스에 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 () { // 락을 얻었습니다. 팟캐스트를 처리합니다... }, function () { // 락을 얻지 못했습니다... return $this->release(10); }); } }

이 예제에서 애플리케이션이 Redis 락을 얻지 못하면 Job은 10초 동안 되돌려지며, 최대 25번까지 계속 재시도됩니다. 하지만 Job에서 처리되지 않은 예외가 세 번 발생하면 Job은 실패합니다.

기본적으로 워커 프로세스가 비정상 종료되거나(예: 메모리 부족) 강제로 종료되어 끝난 시도는 Job의 최대 예외 횟수에 포함되지 않습니다. 이런 시도도 예외로 집계하고 싶다면 Job 클래스에 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 { // ... }

이 어트리뷰트가 있으면 워커는 Job을 처리하는 동안 애플리케이션 캐시에 마커를 저장합니다. Job이 다음에 시도될 때 이 마커가 아직 남아 있으면, 이전 시도는 예외로 집계됩니다.

예외로 재시도 중단하기

때로는 예외가 발생했을 때 큐 Job을 다른 시도를 위해 되돌리지 않고 즉시 실패시켜야 할 수 있습니다. 애플리케이션의 bootstrap/app.php 파일에서 dontRetry 예외 메서드를 사용해 Job 재시도를 중단할 예외 타입을 설정할 수 있습니다.

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이 대략 얼마나 걸릴지 미리 알고 있는 경우가 많습니다. 이 때문에 Laravel은 "타임아웃" 값을 지정할 수 있게 해 줍니다. 기본 타임아웃 값은 60초입니다. Job이 타임아웃 값으로 지정한 초 수보다 오래 처리되면, 해당 Job을 처리하던 워커는 오류와 함께 종료됩니다. 보통은 서버에 구성한 프로세스 매니저가 워커를 자동으로 다시 시작합니다.

Artisan 명령줄의 --timeout 스위치로 Job이 실행될 수 있는 최대 시간(초)을 지정할 수 있습니다.

php artisan queue:work --timeout=30

Job이 계속 타임아웃되어 최대 시도 횟수를 초과하면 실패한 Job으로 표시됩니다.

Job 클래스의 Timeout 어트리뷰트로 Job이 실행될 수 있는 최대 시간(초)을 정의할 수도 있습니다. Job에 타임아웃이 지정되어 있으면 명령줄에서 지정한 타임아웃보다 우선합니다.

<?php namespace App\Jobs; use Illuminate\Queue\Attributes\Timeout; #[Timeout(120)] class ProcessPodcast implements ShouldQueue { // ... }

소켓이나 외부로 나가는 HTTP 연결처럼 IO를 블로킹하는 프로세스는 지정한 타임아웃을 따르지 않을 수 있습니다. 따라서 이런 기능을 사용할 때는 해당 기능의 API로도 항상 타임아웃을 지정해야 합니다. 예를 들어 Guzzle을 사용한다면 연결 타임아웃과 요청 타임아웃 값을 항상 지정하세요.

WARNING

Job 타임아웃을 지정하려면 PCNTL PHP 확장이 설치되어 있어야 합니다. 또한 Job의 "타임아웃" 값은 항상 "retry after" 값보다 작아야 합니다. 그렇지 않으면 Job이 실제로 실행을 마치거나 타임아웃되기 전에 다시 시도될 수 있습니다. queue:work 명령어를 --once 옵션과 함께 호출하면 --timeout 옵션은 효과가 없습니다.

타임아웃 시 실패 처리

Job이 타임아웃되었을 때 실패로 표시하고 싶다면 Job 클래스에 FailOnTimeout 어트리뷰트를 사용할 수 있습니다.

<?php namespace App\Jobs; use Illuminate\Queue\Attributes\FailOnTimeout; #[FailOnTimeout] class ProcessPodcast implements ShouldQueue { // ... }

NOTE

기본적으로 Job이 타임아웃되면 시도 횟수가 하나 소모되고, (재시도가 허용된다면) Job은 큐로 되돌려집니다. 하지만 타임아웃 시 실패하도록 Job을 설정하면, tries 값과 관계없이 Job은 재시도되지 않습니다.

SQS FIFO 큐와 공정(Fair) 큐

Laravel은 Amazon SQS FIFO(First-In-First-Out) 큐와 공정(fair) 큐를 지원합니다. FIFO 큐를 사용하면 Job을 보낸 순서 그대로 처리할 수 있으며, 메시지 중복 제거를 통해 정확히 한 번(exactly-once) 처리가 보장됩니다.

FIFO 큐에서는 어떤 Job을 병렬로 처리할 수 있는지 결정하기 위해 메시지 그룹 ID가 필요합니다. 같은 그룹 ID를 가진 Job은 순차적으로 처리되고, 서로 다른 그룹 ID를 가진 메시지는 동시에 처리될 수 있습니다.

Laravel은 Job을 디스패치할 때 메시지 그룹 ID를 지정할 수 있는 플루언트한 onGroup 메서드를 제공합니다.

ProcessOrder::dispatch($order) ->onGroup("customer-{$order->customer_id}");

메시지 그룹을 지정하지 않고 SQS FIFO 큐로 Job을 디스패치하면, Laravel은 큐 이름을 메시지 그룹 ID로 사용합니다.

SQS FIFO 큐는 정확히 한 번 처리를 보장하기 위한 메시지 중복 제거를 지원합니다. Job 클래스에 deduplicationId 메서드를 구현하면 사용자 정의 중복 제거 ID를 지정할 수 있습니다.

<?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}"; } }

공정(Fair) 큐

SQS 표준 큐를 사용한다면 메시지 그룹을 지정하는 것만으로 공정 큐잉(fair queueing)이 활성화됩니다. 즉, 그룹을 할당하면 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 메서드를 정의하세요. 선택적으로 deduplicator 메서드도 정의할 수 있는데, 이 메서드는 이벤트를 받아 중복 제거 ID를 생성하는 클로저를 반환해야 합니다.

<?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 설정의 기본(primary) 큐 커넥션이 어떤 이유로든 실패하면, Laravel은 목록에서 다음으로 설정된 커넥션에 Job을 푸시하도록 자동으로 시도합니다. 큐의 신뢰성이 중요한 프로덕션 환경에서 고가용성을 확보하는 데 특히 유용합니다.

페일오버 큐 커넥션을 설정하려면 failover 드라이버를 지정하고, 순서대로 시도할 커넥션 이름의 배열을 제공하세요. 기본적으로 Laravel은 애플리케이션의 config/queue.php 설정 파일에 페일오버 설정 예시를 포함하고 있습니다.

'failover' => [ 'driver' => 'failover', 'connections' => [ 'redis', 'database', 'sync', ], ],

failover 드라이버를 사용하는 커넥션을 설정했다면, 페일오버 기능을 사용하기 위해 애플리케이션의 .env 파일에서 이 페일오버 커넥션을 기본 큐 커넥션으로 지정해야 합니다.

QUEUE_CONNECTION=failover

그런 다음 페일오버 커넥션 목록에 있는 각 커넥션마다 워커를 하나 이상 시작하세요.

php artisan queue:work redisphp artisan queue:work database

NOTE

sync, background, deferred 큐 드라이버를 사용하는 커넥션은 현재 PHP 프로세스 안에서 Job을 처리하므로 워커를 실행할 필요가 없습니다.

큐 커넥션 작업이 실패해 페일오버가 활성화되면, Laravel은 Illuminate\Queue\Events\QueueFailedOver 이벤트를 디스패치하므로 큐 커넥션 실패를 보고하거나 로그로 남길 수 있습니다.

NOTE

Laravel Horizon을 사용한다면 Horizon은 Redis 큐만 관리한다는 점을 기억하세요. 페일오버 목록에 database가 포함되어 있다면, Horizon과 함께 일반 php artisan queue:work database 프로세스를 실행해야 합니다.

오류 처리

Job을 처리하는 도중 예외가 발생하면, Job은 다시 시도될 수 있도록 자동으로 큐에 되돌려집니다. Job은 애플리케이션이 허용하는 최대 시도 횟수만큼 시도될 때까지 계속 되돌려집니다. 최대 시도 횟수는 queue:work Artisan 명령어에 사용하는 --tries 스위치로 정의합니다. 또는 Job 클래스 자체에 최대 시도 횟수를 정의할 수도 있습니다. 큐 워커 실행에 관한 자세한 내용은 아래에서 확인할 수 있습니다.

Job을 수동으로 되돌리기

나중에 다시 시도할 수 있도록 Job을 직접 큐에 되돌리고 싶을 때가 있습니다. release 메서드를 호출하면 됩니다.

/** * Execute the job. */ public function handle(): void { // ... $this->release(); }

기본적으로 release 메서드는 Job을 즉시 처리할 수 있도록 큐에 되돌립니다. 하지만 release 메서드에 정수나 날짜 인스턴스를 전달하면, 지정한 시간(초)이 지나기 전까지는 Job을 처리할 수 없게 하도록 큐에 지시할 수 있습니다.

$this->release(10); $this->release(now()->plus(seconds: 10));

Job을 수동으로 실패 처리하기

가끔은 Job을 직접 "실패"로 표시해야 할 수 있습니다. 이럴 때는 fail 메서드를 호출하세요.

/** * Execute the job. */ public function handle(): void { // ... $this->fail(); }

붙잡은 예외 때문에 Job을 실패로 표시하고 싶다면 fail 메서드에 그 예외를 전달할 수 있습니다. 편의를 위해 문자열 오류 메시지를 전달하면 자동으로 예외로 변환됩니다.

$this->fail($exception); $this->fail('Something went wrong.');

NOTE

실패한 Job에 대한 자세한 내용은 Job 실패 처리 문서를 확인하세요.

특정 예외에서 Job 실패시키기

FailOnException Job 미들웨어를 사용하면 특정 예외가 발생했을 때 재시도를 즉시 끊을 수 있습니다. 외부 API 오류 같은 일시적인 예외에서는 재시도하되, 사용자의 권한이 회수된 경우처럼 지속되는 예외에서는 Job을 영구적으로 실패시킬 수 있습니다.

<?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 배치

Laravel의 Job 배치 기능을 사용하면 여러 Job을 병렬로 손쉽게 실행하고, 배치에 속한 Job의 실행이 모두 끝났을 때 특정 작업을 수행할 수 있습니다.

시작하기 전에, Job 배치의 완료율 같은 메타 정보를 담을 테이블을 만드는 데이터베이스 마이그레이션을 생성해야 합니다. 이 마이그레이션은 make:queue-batches-table Artisan 명령어로 생성할 수 있습니다.

php artisan make:queue-batches-tablephp artisan migrate

배치 가능한 Job 정의

배치 가능한 Job을 정의하려면 평소처럼 큐 가능한 Job을 생성하되, Job 클래스에 Illuminate\Bus\Batchable 트레이트를 추가해야 합니다. 이 트레이트는 Job이 실행 중인 현재 배치를 가져올 수 있는 batch 메서드를 제공합니다.

<?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()) { // 배치가 취소되었는지 확인합니다... return; } // CSV 파일의 일부를 가져옵니다... } }

배치 디스패치

Job 배치를 디스패치하려면 Bus 파사드의 batch 메서드를 사용합니다. 물론 배치는 완료 콜백과 함께 사용할 때 가장 유용합니다. then, catch, finally 메서드로 배치의 완료 콜백을 정의할 수 있으며, 이 콜백들은 호출될 때 각각 Illuminate\Bus\Batch 인스턴스를 전달받습니다.

여러 큐 워커를 실행하면 배치 안의 Job은 병렬로 처리됩니다. 따라서 Job이 완료되는 순서는 배치에 추가한 순서와 다를 수 있습니다. 일련의 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) { // 배치가 생성되었지만 아직 Job이 추가되지 않았습니다... })->progress(function (Batch $batch) { // Job 하나가 성공적으로 완료되었습니다... })->then(function (Batch $batch) { // 모든 Job이 성공적으로 완료되었습니다... })->catch(function (Batch $batch, Throwable $e) { // 배치 Job 실패가 감지되었습니다... })->finally(function (Batch $batch) { // 배치 실행이 끝났습니다... })->dispatch(); return $batch->id;

$batch->id 프로퍼티로 접근할 수 있는 배치의 ID는, 배치가 디스패치된 뒤 Laravel 커맨드 버스를 조회해 배치 정보를 얻는 데 사용할 수 있습니다.

WARNING

배치 콜백은 직렬화되어 Laravel 큐가 나중에 실행하므로, 콜백 안에서는 $this 변수를 사용하지 않아야 합니다. 또한 배치 Job은 데이터베이스 트랜잭션으로 감싸져 실행되므로, 암묵적 커밋(implicit commit)을 일으키는 데이터베이스 구문은 Job 안에서 실행하지 않아야 합니다.

배치에 이름 붙이기

Laravel Horizon이나 Laravel Telescope 같은 일부 도구는 배치에 이름이 있으면 배치에 대해 더 보기 좋은 디버그 정보를 제공할 수 있습니다. 배치에 임의의 이름을 지정하려면 배치를 정의할 때 name 메서드를 호출하세요.

$batch = Bus::batch([ // ... ])->then(function (Batch $batch) { // 모든 Job이 성공적으로 완료되었습니다... })->name('Import CSV')->dispatch();

배치의 커넥션과 큐

배치 Job에 사용할 커넥션과 큐를 지정하고 싶다면 onConnection과 onQueue 메서드를 사용할 수 있습니다. 배치에 속한 모든 Job은 같은 커넥션과 큐 안에서 실행되어야 합니다.

$batch = Bus::batch([ // ... ])->then(function (Batch $batch) { // 모든 Job이 성공적으로 완료되었습니다... })->onConnection('redis')->onQueue('imports')->dispatch();

체인과 배치

배열 안에 체인 Job을 넣으면 배치 안에 체인 Job 묶음을 정의할 수 있습니다. 예를 들어 두 개의 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) { // 모든 Job이 성공적으로 완료되었습니다... })->dispatch();

반대로 체인 안에 배치를 정의하면 체인 안에서 Job 배치를 실행할 수도 있습니다. 예를 들어 먼저 여러 팟캐스트를 공개하는 Job 배치를 실행한 다음, 공개 알림을 보내는 Job 배치를 실행할 수 있습니다.

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을 배치로 묶어야 하는데, 웹 요청 중에 디스패치하기에는 시간이 너무 오래 걸릴 때 유용합니다. 이럴 때는 처음에 배치를 채워 줄 "로더(loader)" Job으로 구성된 초기 배치를 디스패치하고, 이 Job들이 더 많은 Job으로 배치를 채우게 할 수 있습니다.

$batch = Bus::batch([ new LoadImportBatch, new LoadImportBatch, new LoadImportBatch, ])->then(function (Batch $batch) { // 모든 Job이 성공적으로 완료되었습니다... })->name('Import Contacts')->dispatch();

이 예제에서는 LoadImportBatch Job을 사용해 배치에 추가 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 인스턴스에는 특정 Job 배치와 상호작용하고 상태를 조회할 수 있는 다양한 프로퍼티와 메서드가 있습니다.

// 배치의 UUID... $batch->id; // 배치의 이름 (있는 경우)... $batch->name; // 배치에 할당된 Job의 수... $batch->totalJobs; // 큐에서 아직 처리되지 않은 Job의 수... $batch->pendingJobs; // 실패한 Job의 수... $batch->failedJobs; // 지금까지 처리된 Job의 수... $batch->processedJobs(); // 배치의 완료율 (0-100)... $batch->progress(); // 배치 실행이 끝났는지 여부... $batch->finished(); // 배치 실행 취소... $batch->cancel(); // 배치가 취소되었는지 여부... $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은 보통 실행을 계속하기 전에 해당 배치가 취소되었는지 확인해야 합니다. 하지만 편의를 위해 Job에 SkipIfBatchCancelled 미들웨어를 지정할 수도 있습니다. 이름에서 알 수 있듯이, 이 미들웨어는 해당 배치가 취소된 경우 Job을 처리하지 않도록 Laravel에 지시합니다.

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은 해당 배치를 자동으로 "취소됨"으로 표시합니다. 원한다면 이 동작을 비활성화해, Job이 실패해도 배치가 자동으로 취소됨으로 표시되지 않게 할 수 있습니다. 배치를 디스패치할 때 allowFailures 메서드를 호출하면 됩니다.

$batch = Bus::batch([ // ... ])->then(function (Batch $batch) { // 모든 Job이 성공적으로 완료되었습니다... })->allowFailures()->dispatch();

allowFailures 메서드에 클로저를 전달할 수도 있으며, 이 클로저는 Job이 실패할 때마다 실행됩니다.

$batch = Bus::batch([ // ... ])->allowFailures(function (Batch $batch, $exception) { // 개별 Job 실패를 처리합니다... })->dispatch();

실패한 배치 Job 재시도하기

편의를 위해 Laravel은 특정 배치의 실패한 Job을 모두 쉽게 재시도할 수 있는 queue:retry-batch Artisan 명령어를 제공합니다. 이 명령어는 실패한 Job을 재시도할 배치의 UUID를 인자로 받습니다.

php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5

배치 정리(Pruning)

정리하지 않으면 job_batches 테이블에 레코드가 매우 빠르게 쌓일 수 있습니다. 이를 완화하려면 queue:prune-batches Artisan 명령어가 매일 실행되도록 스케줄에 등록해야 합니다.

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_batches 테이블에는 Job이 실패했는데 그 Job이 끝내 성공적으로 재시도되지 않은 배치처럼, 한 번도 성공적으로 완료되지 못한 배치의 레코드가 쌓일 수도 있습니다. unfinished 옵션을 사용하면 queue:prune-batches 명령어가 이런 미완료 배치 레코드도 정리하도록 지시할 수 있습니다.

use Illuminate\Support\Facades\Schedule; Schedule::command('queue:prune-batches --hours=48 --unfinished=72')->daily();

마찬가지로 job_batches 테이블에는 취소된 배치의 레코드도 쌓일 수 있습니다. cancelled 옵션을 사용하면 queue:prune-batches 명령어가 이런 취소된 배치 레코드도 정리하도록 지시할 수 있습니다.

use Illuminate\Support\Facades\Schedule; Schedule::command('queue:prune-batches --hours=48 --cancelled=72')->daily();

DynamoDB에 배치 저장하기

Laravel은 배치 메타 정보를 관계형 데이터베이스 대신 DynamoDB에 저장하는 기능도 지원합니다. 다만 모든 배치 레코드를 저장할 DynamoDB 테이블은 직접 만들어야 합니다.

보통 이 테이블의 이름은 job_batches로 짓지만, 애플리케이션의 queue 설정 파일에 있는 queue.batching.table 설정 값에 따라 테이블 이름을 정해야 합니다.

DynamoDB 배치 테이블 설정

job_batches 테이블에는 문자열 타입의 기본 파티션 키 application과 문자열 타입의 기본 정렬 키 id가 있어야 합니다. 키의 application 부분에는 애플리케이션의 app 설정 파일에 있는 name 설정 값으로 정의된 애플리케이션 이름이 들어갑니다. 애플리케이션 이름이 DynamoDB 테이블 키의 일부이므로, 같은 테이블에 여러 Laravel 애플리케이션의 Job 배치를 저장할 수 있습니다.

또한 자동 배치 정리 기능을 활용하려면 테이블에 ttl 속성을 정의할 수 있습니다.

DynamoDB 설정

다음으로 Laravel 애플리케이션이 Amazon DynamoDB와 통신할 수 있도록 AWS SDK를 설치합니다.

composer require aws/aws-sdk-php

그런 다음 queue.batching.driver 설정 옵션의 값을 dynamodb로 지정하세요. 또한 batching 설정 배열 안에 key, secret, region 설정 옵션을 정의해야 합니다. 이 옵션들은 AWS 인증에 사용됩니다. dynamodb 드라이버를 사용할 때는 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에 Job 배치 정보를 저장할 때는, 관계형 데이터베이스에 저장된 배치를 정리하는 일반적인 정리 명령어가 동작하지 않습니다. 대신 DynamoDB의 기본 TTL 기능을 활용해 오래된 배치의 레코드를 자동으로 삭제할 수 있습니다.

DynamoDB 테이블에 ttl 속성을 정의했다면, 배치 레코드를 어떻게 정리할지 Laravel에 알려 주는 설정 파라미터를 정의할 수 있습니다. queue.batching.ttl_attribute 설정 값은 TTL을 담은 속성의 이름을 정의하고, queue.batching.ttl 설정 값은 레코드가 마지막으로 갱신된 시점을 기준으로 몇 초가 지나면 배치 레코드를 DynamoDB 테이블에서 삭제할 수 있는지를 정의합니다.

'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일... ],

클로저 큐잉

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) { // 이 Job은 실패했습니다... });

WARNING

catch 콜백은 직렬화되어 Laravel 큐가 나중에 실행하므로, catch 콜백 안에서는 $this 변수를 사용하지 않아야 합니다.

큐 워커 실행

`queue:work` 명령어

Laravel에는 큐 워커를 시작하고 큐에 새 Job이 푸시되면 이를 처리하는 Artisan 명령어가 포함되어 있습니다. queue:work Artisan 명령어로 워커를 실행할 수 있습니다. queue:work 명령어가 시작되면 직접 중지하거나 터미널을 닫기 전까지 계속 실행된다는 점에 유의하세요.

php artisan queue:work

NOTE

queue:work 프로세스를 백그라운드에서 영구적으로 실행하려면 Supervisor 같은 프로세스 모니터를 사용해 큐 워커가 멈추지 않도록 해야 합니다.

처리된 Job의 ID, 커넥션 이름, 큐 이름을 명령어 출력에 포함하고 싶다면 queue:work 명령어를 호출할 때 -v 플래그를 추가하세요.

php artisan queue:work -v

큐 워커는 오래 실행되는 프로세스이며 부팅된 애플리케이션 상태를 메모리에 저장한다는 점을 기억하세요. 그 결과 워커가 시작된 이후에 코드베이스가 바뀌어도 이를 알아차리지 못합니다. 따라서 배포 과정에서 큐 워커를 재시작해야 합니다. 또한 애플리케이션이 생성하거나 수정한 정적(static) 상태는 Job 사이에 자동으로 초기화되지 않는다는 점도 기억하세요.

대안으로 queue:listen 명령어를 실행할 수도 있습니다. queue:listen 명령어를 사용하면 수정한 코드를 다시 불러오거나 애플리케이션 상태를 초기화하고 싶을 때 워커를 직접 재시작할 필요가 없습니다. 다만 이 명령어는 queue:work 명령어보다 효율이 크게 떨어집니다.

php artisan queue:listen

여러 큐 워커 실행하기

하나의 큐에 여러 워커를 할당해 Job을 동시에 처리하려면 queue:work 프로세스를 여러 개 시작하기만 하면 됩니다. 로컬에서는 터미널 탭을 여러 개 열어서 할 수 있고, 프로덕션에서는 프로세스 매니저의 설정으로 할 수 있습니다. Supervisor를 사용한다면 numprocs 설정 값을 사용할 수 있습니다.

커넥션과 큐 지정하기

워커가 사용할 큐 커넥션도 지정할 수 있습니다. work 명령어에 전달하는 커넥션 이름은 config/queue.php 설정 파일에 정의된 커넥션 중 하나여야 합니다.

php artisan queue:work redis

기본적으로 queue:work 명령어는 해당 커넥션의 기본 큐에 있는 Job만 처리합니다. 하지만 특정 커넥션의 특정 큐만 처리하도록 큐 워커를 더 세밀하게 구성할 수도 있습니다. 예를 들어 모든 이메일이 redis 큐 커넥션의 emails 큐에서 처리된다면, 다음 명령어로 그 큐만 처리하는 워커를 시작할 수 있습니다.

php artisan queue:work redis --queue=emails

지정한 개수만큼 Job 처리하기

--once 옵션을 사용하면 워커가 큐에서 Job 하나만 처리하도록 지시할 수 있습니다.

php artisan queue:work --once

--max-jobs 옵션을 사용하면 워커가 지정한 개수의 Job을 처리한 뒤 종료하도록 지시할 수 있습니다. 이 옵션은 Supervisor와 함께 쓰면 유용합니다. 워커가 지정한 개수의 Job을 처리한 뒤 자동으로 재시작되어, 쌓여 있던 메모리를 해제할 수 있기 때문입니다.

php artisan queue:work --max-jobs=1000

큐에 쌓인 모든 Job을 처리하고 종료하기

--stop-when-empty 옵션을 사용하면 워커가 모든 Job을 처리한 뒤 정상적으로 종료하도록 지시할 수 있습니다. 이 옵션은 Docker 컨테이너 안에서 Laravel 큐를 처리하면서 큐가 비면 컨테이너를 종료하고 싶을 때 유용합니다.

php artisan queue:work --stop-when-empty

지정한 시간(초) 동안 Job 처리하기

--max-time 옵션을 사용하면 워커가 지정한 시간(초) 동안 Job을 처리한 뒤 종료하도록 지시할 수 있습니다. 이 옵션도 Supervisor와 함께 쓰면 유용합니다. 워커가 일정 시간 동안 Job을 처리한 뒤 자동으로 재시작되어, 쌓여 있던 메모리를 해제할 수 있기 때문입니다.

# 한 시간 동안 Job을 처리한 뒤 종료합니다...php artisan queue:work --max-time=3600

워커 대기 시간

큐에 처리할 Job이 있으면 워커는 Job 사이에 지연 없이 계속 Job을 처리합니다. 하지만 sleep 옵션은 처리할 Job이 없을 때 워커가 몇 초 동안 "잠들지"를 결정합니다. 물론 잠들어 있는 동안에는 워커가 새 Job을 처리하지 않습니다.

php artisan queue:work --sleep=3

유지보수 모드와 큐

애플리케이션이 유지보수 모드인 동안에는 큐에 쌓인 Job이 처리되지 않습니다. 애플리케이션이 유지보수 모드에서 벗어나면 Job은 다시 정상적으로 처리됩니다.

유지보수 모드가 켜져 있어도 큐 워커가 Job을 처리하도록 강제하려면 --force 옵션을 사용하세요.

php artisan queue:work --force

리소스 관련 고려 사항

--memory 옵션은 워커가 종료되는 메모리 한도를 메가바이트 단위로 지정합니다. 기본 한도는 128MB입니다. PHP에 설정된 memory_limit의 백분율로 지정할 수도 있습니다.

php artisan queue:work --memory=60%

백분율 기반 한도를 사용하려면 PHP의 memory_limit이 유한한 값이어야 합니다. PHP의 메모리 제한이 무제한(-1)이라면 대신 메가바이트 단위로 한도를 지정하세요. Supervisor 같은 프로세스 모니터를 쓰면 워커가 종료된 뒤 자동으로 다시 시작할 수 있습니다.

데몬 큐 워커는 각 Job을 처리하기 전에 프레임워크를 "재부팅"하지 않습니다. 따라서 각 Job이 끝날 때마다 무거운 리소스를 해제해야 합니다. 예를 들어 GD 라이브러리로 이미지 처리를 한다면, 이미지 처리가 끝난 뒤 imagedestroy로 메모리를 해제해야 합니다.

큐 우선순위

큐가 처리되는 우선순위를 정하고 싶을 때가 있습니다. 예를 들어 config/queue.php 설정 파일에서 redis 커넥션의 기본 queue를 low로 지정할 수 있습니다. 하지만 가끔은 다음과 같이 high 우선순위 큐로 Job을 푸시하고 싶을 수도 있습니다.

dispatch((new Job)->onQueue('high'));

low 큐의 Job을 처리하기 전에 high 큐의 Job을 모두 먼저 처리하도록 보장하는 워커를 시작하려면, work 명령어에 쉼표로 구분한 큐 이름 목록을 전달하세요.

php artisan queue:work --queue=high,low

큐 워커와 배포

큐 워커는 오래 실행되는 프로세스이므로, 재시작하지 않으면 코드 변경을 알아차리지 못합니다. 따라서 큐 워커를 사용하는 애플리케이션을 배포하는 가장 간단한 방법은 배포 과정에서 워커를 재시작하는 것입니다. queue:restart 명령어로 모든 워커를 정상적으로(gracefully) 재시작할 수 있습니다.

php artisan queue:restart

이 명령어는 모든 큐 워커가 현재 처리 중인 Job을 마친 뒤 정상적으로 종료하도록 지시하므로, 기존 Job이 유실되지 않습니다. queue:restart 명령어를 실행하면 큐 워커가 종료되므로, 큐 워커를 자동으로 다시 시작해 줄 Supervisor 같은 프로세스 매니저를 실행해 두어야 합니다.

NOTE

큐는 재시작 신호를 저장하는 데 캐시를 사용하므로, 이 기능을 사용하기 전에 애플리케이션에 캐시 드라이버가 올바르게 구성되어 있는지 확인해야 합니다.

워커 시그널에 대응하기

큐 워커가 Job을 처리하는 도중 SIGQUIT, SIGTERM, SIGINT 같은 종료 시그널을 받으면, 워커는 현재 Job을 마친 뒤 종료합니다. 하지만 서버나 컨테이너 오케스트레이터가 프로세스를 강제로 중지하기 전에 Job이 이 시그널에 대응해야 할 수도 있습니다. 예를 들어 오래 실행되는 가져오기(import) Job은 새 레코드 가져오기를 멈추고 현재 진행 상황을 저장해야 할 수 있습니다.

Job 안에서 워커 시그널에 대응하려면 Illuminate\Contracts\Queue\Interruptible 인터페이스를 구현하고 Job에 interrupted 메서드를 정의하세요. 워커가 받은 시그널 번호가 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; } // 상품 행을 가져옵니다... } $this->import->saveProgress(); } /** * Handle a signal received by the queue worker. */ public function interrupted(int $signal): void { $this->shouldStop = true; } }

interrupted 메서드는 Job이 현재 실행 중일 때 워커가 프로세스 시그널을 받은 경우에만 호출됩니다. 이는 타임아웃이나 Job의 failed 메서드를 대체하는 기능이 아닙니다.

Job 만료와 타임아웃

Job 만료

config/queue.php 설정 파일에서 각 큐 커넥션은 retry_after 옵션을 정의합니다. 이 옵션은 처리 중인 Job을 다시 시도하기 전에 큐 커넥션이 몇 초를 기다려야 하는지 지정합니다. 예를 들어 retry_after 값이 90이면, Job이 되돌려지거나 삭제되지 않은 채 90초 동안 처리 중이면 큐로 다시 돌려보내집니다. 보통 retry_after 값은 Job이 처리를 마치는 데 합리적으로 걸릴 수 있는 최대 시간(초)으로 설정해야 합니다.

WARNING

retry_after 값을 가지지 않는 유일한 큐 커넥션은 Amazon SQS입니다. SQS는 AWS 콘솔에서 관리하는 기본 가시성 타임아웃(Default Visibility Timeout)에 따라 Job을 재시도합니다.

워커 타임아웃

queue:work Artisan 명령어는 --timeout 옵션을 제공합니다. 기본 --timeout 값은 60초입니다. Job이 타임아웃 값으로 지정한 초 수보다 오래 처리되면, 해당 Job을 처리하던 워커는 오류와 함께 종료됩니다. 보통은 서버에 구성한 프로세스 매니저가 워커를 자동으로 다시 시작합니다.

php artisan queue:work --timeout=60

retry_after 설정 옵션과 --timeout CLI 옵션은 서로 다르지만, 함께 동작하여 Job이 유실되지 않고 성공적으로 한 번만 처리되도록 보장합니다.

WARNING

--timeout 값은 항상 retry_after 설정 값보다 최소 몇 초 이상 짧아야 합니다. 그래야 멈춰 버린(frozen) Job을 처리하던 워커가 Job이 재시도되기 전에 항상 종료됩니다. --timeout 옵션이 retry_after 설정 값보다 길면 Job이 두 번 처리될 수 있습니다.

큐 워커 일시 정지와 재개

워커를 완전히 중지하지 않고 큐 워커가 새 Job을 처리하지 못하도록 일시적으로 막아야 할 때가 있습니다. 예를 들어 시스템 점검 중에는 Job 처리를 잠시 멈추고 싶을 수 있습니다. Laravel은 큐 워커를 일시 정지하고 재개하는 queue:pause와 queue:continue Artisan 명령어를 제공합니다.

특정 큐를 일시 정지하려면 큐 커넥션 이름과 큐 이름을 지정하세요.

php artisan queue:pause database:default

이 예제에서 database는 큐 커넥션 이름이고 default는 큐 이름입니다. 큐가 일시 정지되면 그 큐의 Job을 처리하던 워커는 현재 Job을 끝까지 처리하지만, 큐가 재개되기 전까지는 새 Job을 가져가지 않습니다.

모든 커넥션의 모든 큐에서 Job 처리를 일시 정지하려면 --all 옵션을 사용하세요.

php artisan queue:pause --all

일시 정지된 큐에서 Job 처리를 재개하려면 queue:continue 명령어를 사용합니다.

php artisan queue:continue database:default

모든 커넥션의 모든 큐에서 Job 처리를 재개하려면 queue:resume 명령어에 --all 옵션을 사용하세요.

php artisan queue:resume --all

큐를 재개하면 워커는 즉시 그 큐의 새 Job을 처리하기 시작합니다. 모든 큐를 재개해도 개별적으로 일시 정지한 큐는 재개되지 않습니다. 큐를 일시 정지해도 워커 프로세스 자체가 멈추는 것은 아니며, 지정한 큐에서 새 Job을 처리하지 못하게 막을 뿐이라는 점에 유의하세요.

워커 재시작 및 일시 정지 시그널

기본적으로 큐 워커는 Job을 반복 처리할 때마다 캐시 드라이버를 폴링해 재시작과 일시 정지 시그널을 확인합니다. 이 폴링은 queue:restart와 queue:pause 명령어에 대응하는 데 필수적이지만, 약간의 성능 오버헤드가 있습니다.

성능을 최적화해야 하고 이런 중단 기능이 필요 없다면, 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:work 프로세스는 워커 타임아웃 초과나 queue:restart 명령어 실행 등 다양한 이유로 멈출 수 있습니다.

이 때문에 queue:work 프로세스가 종료되는 것을 감지해 자동으로 다시 시작해 주는 프로세스 모니터를 구성해야 합니다. 또한 프로세스 모니터를 사용하면 동시에 실행할 queue:work 프로세스의 수도 지정할 수 있습니다. Supervisor는 Linux 환경에서 흔히 쓰이는 프로세스 모니터이며, 이어지는 문서에서 Supervisor 설정 방법을 설명합니다.

Supervisor 설치

Supervisor는 Linux 운영체제용 프로세스 모니터로, queue:work 프로세스가 실패하면 자동으로 다시 시작해 줍니다. Ubuntu에서 Supervisor를 설치하려면 다음 명령어를 사용하세요.

sudo apt-get install supervisor

NOTE

Supervisor를 직접 설정하고 관리하는 일이 부담스럽다면, Laravel 큐 워커를 실행하기 위한 완전 관리형 플랫폼을 제공하는 Laravel Cloud를 고려해 보세요.

Supervisor 설정하기

Supervisor 설정 파일은 보통 /etc/supervisor/conf.d 디렉터리에 저장합니다. 이 디렉터리 안에 프로세스를 어떻게 모니터링할지 Supervisor에 알려 주는 설정 파일을 원하는 만큼 만들 수 있습니다. 예를 들어 queue:work 프로세스를 시작하고 모니터링하는 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=3600

이 예제에서 numprocs 지시어는 Supervisor가 queue:work 프로세스 8개를 실행하고 모두 모니터링하며, 실패하면 자동으로 다시 시작하도록 지시합니다. 원하는 큐 커넥션과 워커 옵션에 맞게 설정의 command 지시어를 변경해야 합니다.

WARNING

stopwaitsecs 값은 가장 오래 실행되는 Job이 소요하는 시간(초)보다 커야 합니다. 그렇지 않으면 Supervisor가 Job의 처리가 끝나기 전에 Job을 강제 종료할 수 있습니다.

Supervisor 시작하기

설정 파일을 만들었다면 다음 명령어로 Supervisor 설정을 갱신하고 프로세스를 시작할 수 있습니다.

sudo supervisorctl rereadsudo supervisorctl updatesudo supervisorctl start "laravel-worker:*"

Supervisor에 대한 자세한 내용은 Supervisor 문서를 참고하세요.

실패한 Job 다루기

큐 Job이 실패할 때도 있습니다. 걱정하지 마세요. 일이 항상 계획대로 되는 것은 아닙니다! Laravel에는 Job을 시도할 최대 횟수를 지정하는 편리한 방법이 포함되어 있습니다. 비동기 Job이 이 시도 횟수를 초과하면 failed_jobs 데이터베이스 테이블에 기록됩니다. 실패한 동기 디스패치 Job은 이 테이블에 저장되지 않으며, 그 예외는 애플리케이션이 즉시 처리합니다.

failed_jobs 테이블을 생성하는 마이그레이션은 보통 새 Laravel 애플리케이션에 이미 포함되어 있습니다. 하지만 애플리케이션에 이 테이블의 마이그레이션이 없다면 make:queue-failed-table 명령어로 마이그레이션을 생성할 수 있습니다.

php artisan make:queue-failed-tablephp artisan migrate

큐 워커 프로세스를 실행할 때 queue:work 명령어의 --tries 스위치로 Job을 시도할 최대 횟수를 지정할 수 있습니다. --tries 옵션에 값을 지정하지 않으면 Job은 한 번만 시도되거나, Job 클래스의 Tries 어트리뷰트에 지정된 횟수만큼 시도됩니다.

php artisan queue:work redis --tries=3

--backoff 옵션을 사용하면 예외가 발생한 Job을 Laravel이 다시 시도하기 전에 몇 초를 기다릴지 지정할 수 있습니다. 기본적으로 Job은 다시 시도될 수 있도록 즉시 큐에 되돌려집니다.

php artisan queue:work redis --tries=3 --backoff=3

예외가 발생한 Job을 다시 시도하기 전에 기다릴 시간(초)을 Job 단위로 설정하고 싶다면 Job 클래스에 Backoff 어트리뷰트를 사용할 수 있습니다.

<?php namespace App\Jobs; use Illuminate\Queue\Attributes\Backoff; #[Backoff(3)] class ProcessPodcast implements ShouldQueue { // ... }

Job의 백오프 시간을 결정하는 데 더 복잡한 로직이 필요하다면 Job 클래스에 backoff 메서드를 정의할 수 있습니다.

/** * Calculate the number of seconds to wait before retrying the job. */ public function backoff(): int { return 3; }

백오프 값의 배열을 정의하면 "지수(exponential)" 백오프도 쉽게 설정할 수 있습니다. 다음 예제에서 재시도 지연 시간은 첫 번째 재시도에 1초, 두 번째 재시도에 5초, 세 번째 재시도에 10초이며, 남은 시도가 더 있다면 그 이후의 모든 재시도에도 10초입니다.

<?php namespace App\Jobs; use Illuminate\Queue\Attributes\Backoff; #[Backoff([1, 5, 10])] class ProcessPodcast implements ShouldQueue { // ... }

실패한 Job 이후 정리 작업

특정 Job이 실패하면 사용자에게 알림을 보내거나, Job이 일부만 완료한 작업을 되돌리고 싶을 수 있습니다. 이를 위해 Job 클래스에 failed 메서드를 정의할 수 있습니다. Job 실패의 원인이 된 Throwable 인스턴스가 failed 메서드에 전달됩니다.

<?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 { // 업로드된 팟캐스트를 처리합니다... } /** * Handle a job failure. */ public function failed(?Throwable $exception): void { // 사용자에게 실패 알림을 보내는 등의 작업을 수행합니다... } }

WARNING

failed 메서드를 호출하기 전에 Job의 새 인스턴스가 생성됩니다. 따라서 handle 메서드 안에서 이루어졌을 수 있는 클래스 프로퍼티 변경은 모두 사라집니다.

실패한 Job이 반드시 처리되지 않은 예외를 만난 Job인 것은 아닙니다. Job이 허용된 시도 횟수를 모두 소진했을 때도 실패한 것으로 간주될 수 있습니다. 이런 시도는 여러 방식으로 소모될 수 있습니다.

  • Job이 타임아웃되었을 때
  • Job이 실행 중 처리되지 않은 예외를 만났을 때
  • Job이 직접 또는 미들웨어에 의해 큐로 되돌려졌을 때

마지막 시도가 Job 실행 중 던져진 예외로 실패하면, 그 예외가 Job의 failed 메서드에 전달됩니다. 하지만 Job이 허용된 최대 시도 횟수에 도달해서 실패한 경우에는 $exception이 Illuminate\Queue\MaxAttemptsExceededException의 인스턴스가 됩니다. 마찬가지로 Job이 설정된 타임아웃을 초과해서 실패한 경우에는 $exception이 Illuminate\Queue\TimeoutExceededException의 인스턴스가 됩니다.

실패한 Job 재시도

failed_jobs 데이터베이스 테이블에 기록된 실패한 Job을 모두 보려면 queue:failed Artisan 명령어를 사용하세요.

php artisan queue:failed

queue:failed 명령어는 Job ID, 커넥션, 큐, 실패 시각 등 Job에 대한 정보를 나열합니다. Job ID로 실패한 Job을 재시도할 수 있습니다. 예를 들어 ID가 ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece인 실패한 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을 전부 재시도하려면 queue:retry 명령어를 실행하면서 ID로 all을 전달하세요.

php artisan queue:retry all

실패한 Job을 삭제하고 싶다면 queue:forget 명령어를 사용하세요.

php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24d

NOTE

Horizon을 사용할 때는 실패한 Job을 삭제하려면 queue:forget 명령어 대신 horizon:forget 명령어를 사용해야 합니다.

failed_jobs 테이블에서 실패한 Job을 모두 삭제하려면 queue:flush 명령어를 사용하세요.

php artisan queue:flush

queue:flush 명령어는 실패한 Job이 얼마나 오래되었는지와 관계없이 큐의 실패한 Job 레코드를 모두 제거합니다. --hours 옵션을 사용하면 지정한 시간 전 또는 그 이전에 실패한 Job만 삭제할 수 있습니다.

php artisan queue:flush --hours=48

존재하지 않는 모델 무시하기

Job에 Eloquent 모델을 주입하면, 모델은 큐에 들어가기 전에 자동으로 직렬화되고 Job이 처리될 때 데이터베이스에서 다시 조회됩니다. 하지만 워커가 Job을 처리하기를 기다리는 동안 모델이 삭제되었다면, Job이 ModelNotFoundException과 함께 실패할 수 있습니다.

편의를 위해 Job 클래스에 DeleteWhenMissingModels 어트리뷰트를 사용하면, 모델이 없는 Job을 자동으로 삭제하도록 선택할 수 있습니다. 이 어트리뷰트가 있으면 Laravel은 예외를 발생시키지 않고 조용히 Job을 폐기합니다.

<?php namespace App\Jobs; use Illuminate\Queue\Attributes\DeleteWhenMissingModels; #[DeleteWhenMissingModels] class ProcessPodcast implements ShouldQueue { // ... }

실패한 Job 정리(Pruning)

queue:prune-failed Artisan 명령어를 실행하면 애플리케이션의 failed_jobs 테이블에 있는 레코드를 정리할 수 있습니다.

php artisan queue:prune-failed

기본적으로 24시간이 넘은 실패한 Job 레코드는 모두 정리됩니다. 명령어에 --hours 옵션을 지정하면, 최근 N시간 이내에 기록된 실패한 Job 레코드만 남깁니다. 예를 들어 다음 명령어는 48시간보다 오래전에 기록된 실패한 Job 레코드를 모두 삭제합니다.

php artisan queue:prune-failed --hours=48

DynamoDB에 실패한 Job 저장하기

Laravel은 실패한 Job 레코드를 관계형 데이터베이스 테이블 대신 DynamoDB에 저장하는 기능도 지원합니다. 다만 모든 실패한 Job 레코드를 저장할 DynamoDB 테이블은 직접 만들어야 합니다. 보통 이 테이블의 이름은 failed_jobs로 짓지만, 애플리케이션의 queue 설정 파일에 있는 queue.failed.table 설정 값에 따라 테이블 이름을 정해야 합니다.

failed_jobs 테이블에는 문자열 타입의 기본 파티션 키 application과 문자열 타입의 기본 정렬 키 uuid가 있어야 합니다. 키의 application 부분에는 애플리케이션의 app 설정 파일에 있는 name 설정 값으로 정의된 애플리케이션 이름이 들어갑니다. 애플리케이션 이름이 DynamoDB 테이블 키의 일부이므로, 같은 테이블에 여러 Laravel 애플리케이션의 실패한 Job을 저장할 수 있습니다.

또한 Laravel 애플리케이션이 Amazon DynamoDB와 통신할 수 있도록 AWS SDK를 반드시 설치하세요.

composer require aws/aws-sdk-php

다음으로 queue.failed.driver 설정 옵션의 값을 dynamodb로 지정하세요. 또한 실패한 Job 설정 배열 안에 key, secret, region 설정 옵션을 정의해야 합니다. 이 옵션들은 AWS 인증에 사용됩니다. dynamodb 드라이버를 사용할 때는 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을 저장하지 않고 폐기하도록 Laravel에 지시할 수 있습니다. 보통 QUEUE_FAILED_DRIVER 환경 변수로 지정합니다.

QUEUE_FAILED_DRIVER=null

실패한 Job 이벤트

Job이 실패했을 때 호출될 이벤트 리스너를 등록하고 싶다면 Queue 파사드의 failing 메서드를 사용할 수 있습니다. 예를 들어 Laravel에 포함된 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을 사용할 때는 큐에서 Job을 비우려면 queue:clear 명령어 대신 horizon:clear 명령어를 사용해야 합니다.

기본 커넥션의 기본 큐에 있는 Job을 모두 삭제하고 싶다면 queue:clear Artisan 명령어를 사용하면 됩니다.

php artisan queue:clear

connection 인자와 queue 옵션을 지정하면 특정 커넥션과 큐의 Job을 삭제할 수도 있습니다.

php artisan queue:clear redis --queue=emails

WARNING

큐에서 Job을 비우는 기능은 SQS, Redis, 데이터베이스 큐 드라이버에서만 사용할 수 있습니다. 또한 SQS의 메시지 삭제 과정은 최대 60초가 걸리므로, 큐를 비운 뒤 최대 60초 이내에 SQS 큐로 전송된 Job도 함께 삭제될 수 있습니다.

큐 모니터링

큐에 Job이 갑자기 몰리면 큐가 과부하 상태가 되어 Job이 완료되기까지 오래 기다리게 될 수 있습니다. 원한다면 큐의 Job 수가 지정한 임계값을 넘을 때 Laravel이 알려 주도록 할 수 있습니다.

시작하려면 queue:monitor 명령어가 매분 실행되도록 스케줄에 등록해야 합니다. 이 명령어는 모니터링할 큐의 이름과 원하는 Job 수 임계값을 인자로 받습니다.

php artisan queue:monitor redis:default,redis:deployments --max=100

이 명령어를 스케줄에 등록하는 것만으로는 큐가 과부하 상태라는 알림이 발송되지 않습니다. 명령어가 Job 수가 임계값을 넘은 큐를 발견하면 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의 코드는 그것을 디스패치하는 코드와 별개로 직접 테스트할 수 있으므로 Laravel이 Job 자체를 실제로 실행하지 않도록 지시하고 싶을 수 있습니다. 물론 Job 자체를 테스트하려면 테스트에서 Job 인스턴스를 만들고 handle 메서드를 직접 호출하면 됩니다.

Queue 파사드의 fake 메서드를 사용하면 큐 Job이 실제로 큐에 푸시되지 않도록 막을 수 있습니다. Queue 파사드의 fake 메서드를 호출한 뒤에는, 애플리케이션이 Job을 큐에 푸시하려고 시도했는지 단언(assert)할 수 있습니다.

Pest

<?php use App\Jobs\AnotherJob; use App\Jobs\ShipOrder; use Illuminate\Support\Facades\Queue; test('orders can be shipped', function () { Queue::fake(); // 주문 배송을 수행합니다... // 푸시된 Job이 없는지 단언합니다... Queue::assertNothingPushed(); // 지정한 큐에 Job이 푸시되었는지 단언합니다... Queue::assertPushedOn('queue-name', ShipOrder::class); // Job이 푸시되었는지 단언합니다 Queue::assertPushed(ShipOrder::class); // Job이 정확히 한 번 푸시되었는지 단언합니다... Queue::assertPushedOnce(ShipOrder::class); // Job이 두 번 푸시되었는지 단언합니다... Queue::assertPushedTimes(ShipOrder::class, 2); // Job이 푸시되지 않았는지 단언합니다... Queue::assertNotPushed(AnotherJob::class); // 클로저가 큐에 푸시되었는지 단언합니다... Queue::assertClosurePushed(); // 클로저가 푸시되지 않았는지 단언합니다... Queue::assertClosureNotPushed(); // 푸시된 Job의 총 개수를 단언합니다... 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(); // 주문 배송을 수행합니다... // 푸시된 Job이 없는지 단언합니다... Queue::assertNothingPushed(); // 지정한 큐에 Job이 푸시되었는지 단언합니다... Queue::assertPushedOn('queue-name', ShipOrder::class); // Job이 푸시되었는지 단언합니다 Queue::assertPushed(ShipOrder::class); // Job이 정확히 한 번 푸시되었는지 단언합니다... Queue::assertPushedOnce(ShipOrder::class); // Job이 두 번 푸시되었는지 단언합니다... Queue::assertPushedTimes(ShipOrder::class, 2); // Job이 푸시되지 않았는지 단언합니다... Queue::assertNotPushed(AnotherJob::class); // 클로저가 큐에 푸시되었는지 단언합니다... Queue::assertClosurePushed(); // 클로저가 푸시되지 않았는지 단언합니다... Queue::assertClosureNotPushed(); // 푸시된 Job의 총 개수를 단언합니다... Queue::assertCount(3); } }

assertPushed, assertNotPushed, assertClosurePushed, assertClosureNotPushed 메서드에 클로저를 전달하면, 주어진 "진위 테스트(truth test)"를 통과하는 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]);

지정한 프로퍼티는 모두 일치해야 합니다. 값은 엄격한 동등 비교(strict equality)로 비교되며, Eloquent 모델은 is 메서드로 비교됩니다. Bus 파사드의 assertDispatched, assertNotDispatched, assertDispatchedSync, assertDispatchedAfterResponse 메서드도 두 번째 인자로 기대하는 프로퍼티 값의 배열을 받을 수 있습니다.

일부 Job만 페이크 처리하기

특정 Job만 페이크 처리하고 나머지 Job은 정상적으로 실행되도록 하고 싶다면, 페이크 처리할 Job의 클래스 이름을 fake 메서드에 전달하면 됩니다.

Pest

test('orders can be shipped', function () { Queue::fake([ ShipOrder::class, ]); // 주문 배송을 수행합니다... // Job이 두 번 푸시되었는지 단언합니다... Queue::assertPushedTimes(ShipOrder::class, 2); });

PHPUnit

public function test_orders_can_be_shipped(): void { Queue::fake([ ShipOrder::class, ]); // 주문 배송을 수행합니다... // Job이 두 번 푸시되었는지 단언합니다... Queue::assertPushedTimes(ShipOrder::class, 2); }

except 메서드를 사용하면 지정한 Job을 제외한 모든 Job을 페이크 처리할 수 있습니다.

Queue::fake()->except([ ShipOrder::class, ]);

Job 체인 테스트

Job 체인을 테스트하려면 Bus 파사드의 페이크 기능을 활용해야 합니다. Bus 파사드의 assertChained 메서드로 Job 체인이 디스패치되었는지 단언할 수 있습니다. assertChained 메서드는 첫 번째 인자로 체인에 연결된 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 ]);

위 예제에서 보듯이 체인에 연결된 Job의 배열은 Job의 클래스 이름 배열이 될 수 있습니다. 하지만 실제 Job 인스턴스의 배열을 전달할 수도 있습니다. 이 경우 Laravel은 Job 인스턴스가 애플리케이션이 디스패치한 체인 Job과 같은 클래스이고 같은 프로퍼티 값을 가지고 있는지 확인합니다.

Bus::assertChained([ new ShipOrder, new RecordShipment, new UpdateInventory, ]);

assertDispatchedWithoutChain 메서드를 사용하면 Job이 체인 없이 푸시되었는지 단언할 수 있습니다.

Bus::assertDispatchedWithoutChain(ShipOrder::class);

체인 변경 테스트

체인에 연결된 Job이 기존 체인의 앞이나 뒤에 Job을 추가한다면, Job의 assertHasChain 메서드로 Job이 기대하는 나머지 Job 체인을 가지고 있는지 단언할 수 있습니다.

$job = new ProcessPodcast; $job->handle(); $job->assertHasChain([ new TranscribePodcast, new OptimizePodcast, new ReleasePodcast, ]);

assertDoesntHaveChain 메서드를 사용하면 Job의 남은 체인이 비어 있는지 단언할 수 있습니다.

$job->assertDoesntHaveChain();

체인에 연결된 배치 테스트

Job 체인이 Job 배치를 포함한다면, 체인 단언 안에 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 배치가 디스패치되었는지 단언할 수 있습니다. assertBatched 메서드에 전달하는 클로저는 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을 포함하고 있는지 검증할 수 있습니다. 이 메서드는 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이 자신이 속한 배치와 상호작용하는 방식을 테스트해야 할 때도 있습니다. 예를 들어 Job이 배치의 이후 처리를 취소했는지 테스트해야 할 수 있습니다. 이를 위해 withFakeBatch 메서드로 Job에 페이크 배치를 할당해야 합니다. withFakeBatch 메서드는 Job 인스턴스와 페이크 배치를 담은 튜플을 반환합니다.

[$job, $batch] = (new ShipOrder)->withFakeBatch(); $job->handle(); $this->assertTrue($batch->cancelled()); $this->assertEmpty($batch->added);

Job과 큐의 상호작용 테스트

큐 Job이 스스로를 큐에 되돌리는지 테스트해야 할 때가 있습니다. 또는 Job이 스스로를 삭제했는지 테스트해야 할 수도 있습니다. Job을 인스턴스화한 뒤 withFakeQueueInteractions 메서드를 호출하면 이런 큐 상호작용을 테스트할 수 있습니다.

Job의 큐 상호작용을 페이크 처리한 뒤에는 Job의 handle 메서드를 호출할 수 있습니다. Job을 호출한 뒤에는 Job의 큐 상호작용을 검증하는 다양한 단언 메서드를 사용할 수 있습니다.

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 메서드를 사용하면 큐 Job이 처리되기 전이나 후에 실행할 콜백을 지정할 수 있습니다. 이 콜백은 추가 로깅을 하거나 대시보드용 통계를 올리기에 좋은 기회입니다. 보통 이 메서드들은 서비스 프로바이더의 boot 메서드에서 호출합니다. 예를 들어 Laravel에 포함된 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(); } });

또한 Laravel은 큐 워커가 큐에서 Job을 가져오지 못했을 때 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 });

이 문서는 Laravel 공식 문서(MIT)를 한국 개발자를 위해 번역·재구성한 것입니다.

번역일: 2026년 11월 6일