큐
번역일: 2026년 7월 2일
큐
- 소개
- Job 생성
- Job 미들웨어
- Job 디스패치
- Job 배치
- 클로저 큐잉
- 큐 워커 실행
- Supervisor 설정
- 실패한 Job 처리
- 큐에서 Job 삭제
- 큐 모니터링
- 테스트
- Job 이벤트
소개
웹 애플리케이션을 개발하다 보면, 요청 처리 중에 실행하기엔 시간이 너무 오래 걸리는 작업이 생기기 마련입니다. 예를 들어 대용량 엑셀 파일 파싱, 외부 API 호출, 또는 대량 이메일 발송 같은 작업이 그렇습니다. Laravel은 이런 작업을 백그라운드에서 처리할 수 있도록 큐 시스템을 제공합니다. 작업을 큐에 넣어두면 사용자는 응답을 즉시 받고, 시간이 걸리는 처리는 워커가 비동기로 수행합니다.
Laravel의 큐 API는 Amazon SQS, Redis, 관계형 데이터베이스 등 다양한 백엔드를 동일한 인터페이스로 사용할 수 있도록 통합되어 있습니다.
큐 관련 설정 파일은 config/queue.php에 위치합니다. 각 큐 드라이버에 대한 커넥션 설정이 이 파일에 정의되어 있으며, database, beanstalkd, sqs, redis, sync 등 다양한 드라이버 예시가 포함되어 있습니다. 또한 실패한 Job을 추적하는 null 드라이버도 제공됩니다.
NOTE
Laravel은 이제 Redis 기반 큐를 위한 아름다운 대시보드와 설정 시스템인 Horizon을 제공합니다. 자세한 내용은 Horizon 공식 문서를 참고하세요.
커넥션 vs. 큐
Laravel 큐를 시작하기 전에, "커넥션(connection)"과 "큐(queue)"의 차이를 이해하는 것이 중요합니다.
- 커넥션: 큐 백엔드 서비스에 대한 연결 설정입니다.
config/queue.php의connections배열에 정의하며, Amazon SQS, Redis, 데이터베이스 등이 해당됩니다. - 큐: 커넥션 안에서 Job을 분류하는 채널입니다. 하나의 커넥션에 여러 큐를 운영할 수 있습니다.
config/queue.php의 각 커넥션 설정에는 queue 항목이 있습니다. 이것이 해당 커넥션으로 디스패치된 Job이 기본으로 들어가는 큐입니다. 즉, 큐를 명시적으로 지정하지 않으면 커넥션 설정의 queue 값으로 지정된 큐에 Job이 들어갑니다.
// 이 Job은 기본 큐에 들어갑니다.
Job::dispatch();
// 이 Job은 "emails" 큐에 들어갑니다.
Job::dispatch()->onQueue('emails');일부 애플리케이션은 여러 큐에 Job을 나눌 필요가 없고 단일 큐만으로 충분합니다. 그러나 Job을 여러 큐에 분산시키면 우선순위별로 처리하거나 처리량을 세분화할 수 있어 유용합니다. Laravel의 큐 워커는 어떤 큐를 우선적으로 처리할지 지정할 수 있습니다.
드라이버 참고사항 및 전제조건
Database
database 큐 드라이버를 사용하려면 Job을 저장할 데이터베이스 테이블이 필요합니다. 일반적으로 Laravel 기본 마이그레이션에 이미 포함되어 있지만, 만약 jobs 테이블 마이그레이션이 없다면 make:queue-table Artisan 명령어로 생성할 수 있습니다.
php artisan make:queue-tablephp artisan migrateRedis
redis 큐 드라이버를 사용하려면 config/database.php에 Redis 데이터베이스 커넥션을 설정해야 합니다.
WARNING
serializer와 compression Redis 옵션은 redis 큐 드라이버에서 지원되지 않습니다.
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,
],블로킹(Blocking)
Redis 큐를 사용할 때 block_for 옵션을 사용하면 워커 루프를 반복하고 Redis 데이터베이스를 다시 폴링하기 전에 Job이 사용 가능해질 때까지 드라이버가 대기할 시간을 초 단위로 지정할 수 있습니다.
큐 부하에 따라 이 값을 조정하는 것이 0으로 설정하여 새 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 같은 시그널이 처리되지 않습니다.
기타 드라이버 전제조건
아래 큐 드라이버에는 각각 해당 패키지가 필요합니다. Composer를 통해 설치할 수 있습니다.
- Amazon SQS:
aws/aws-sdk-php ~3.0 - Beanstalkd:
pda/pheanstalk ~5.0 - Redis:
predis/predis ~2.0또는 phpredis PHP 확장
큐
소개
웹 애플리케이션을 개발하다 보면 업로드된 CSV 파일을 파싱하고 저장하는 것처럼 일반적인 웹 요청 안에서 처리하기엔 너무 오래 걸리는 작업을 만나게 됩니다. Laravel은 이런 작업을 백그라운드에서 처리할 수 있도록 큐(queue) 기반의 Job을 손쉽게 만들 수 있는 방법을 제공합니다. 시간이 오래 걸리는 작업을 큐로 분리하면, 애플리케이션이 웹 요청에 훨씬 빠르게 응답할 수 있어 사용자 경험이 크게 향상됩니다.
Laravel 큐는 Amazon SQS, Redis, 관계형 데이터베이스 등 다양한 큐 백엔드에 대해 통일된 API를 제공합니다.
큐 관련 설정은 config/queue.php 파일에 있습니다. 이 파일에는 프레임워크에서 기본으로 지원하는 각 큐 드라이버(database, Amazon SQS, Redis, Beanstalkd)의 커넥션 설정이 포함되어 있습니다. 또한 Job을 즉시 동기적으로 실행하는 sync 드라이버(개발·테스트용)와, Job을 아예 버리는 null 드라이버도 제공합니다.
NOTE
Redis 기반 큐를 위한 아름다운 대시보드이자 설정 시스템인 Laravel Horizon도 있습니다. 자세한 내용은 Horizon 문서를 참고하세요.
커넥션 vs. 큐
Laravel 큐를 본격적으로 사용하기 전에 "커넥션(connection)"과 "큐(queue)"의 차이를 이해하는 것이 중요합니다.
config/queue.php에는 connections 배열이 있습니다. 이 배열은 Amazon SQS, Beanstalkd, Redis 같은 큐 백엔드 서비스에 대한 연결 정보를 정의합니다. 그리고 하나의 커넥션 안에는 여러 개의 큐가 존재할 수 있습니다. 큐는 Job들이 쌓이는 각각의 스택이라고 생각하면 됩니다.
각 커넥션 설정에는 queue 속성이 있습니다. 이것은 해당 커넥션으로 Job을 보낼 때 기본으로 사용되는 큐입니다. 즉, 큐를 명시하지 않고 Job을 디스패치하면 커넥션 설정의 queue 값에 지정된 큐로 전달됩니다.
use App\Jobs\ProcessPodcast;
// 기본 커넥션의 기본 큐로 전달됩니다.
ProcessPodcast::dispatch();
// 기본 커넥션의 "emails" 큐로 전달됩니다.
ProcessPodcast::dispatch()->onQueue('emails');일부 애플리케이션은 하나의 큐만 사용해도 충분하지만, 여러 큐를 활용하면 Job의 우선순위를 지정하거나 종류별로 분류하여 처리할 수 있습니다. Laravel 큐 워커는 처리할 큐와 그 우선순위를 직접 지정할 수 있기 때문입니다. 예를 들어, high 큐에 쌓인 Job을 default 큐보다 먼저 처리하려면 다음처럼 워커를 실행합니다.
php artisan queue:work --queue=high,default드라이버 참고 사항 및 사전 요구 사항
Database
database 큐 드라이버를 사용하려면 Job을 저장할 데이터베이스 테이블이 필요합니다. 일반적으로 Laravel 기본 마이그레이션 파일인 0001_01_01_000002_create_jobs_table.php가 이 테이블을 생성해 줍니다. 만약 해당 마이그레이션이 프로젝트에 없다면, make:queue-table Artisan 명령어로 직접 생성할 수 있습니다.
php artisan make:queue-tablephp artisan migrateRedis
redis 큐 드라이버를 사용하려면 config/database.php에 Redis 데이터베이스 커넥션이 설정되어 있어야 합니다.
WARNING
Redis의 serializer 및 compression 옵션은 redis 큐 드라이버에서 지원되지 않습니다.
Redis 클러스터
Redis 큐 커넥션이 Redis 클러스터를 사용하는 경우, 큐 이름에 반드시 키 해시 태그(key hash tag)를 포함해야 합니다. 이는 특정 큐에 속하는 모든 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,
],블로킹(Blocking)
Redis 큐를 사용할 때 block_for 설정 옵션으로, 드라이버가 새 Job을 기다리며 블로킹할 최대 시간을 지정할 수 있습니다. 이 시간이 지나면 워커 루프를 다시 순회하며 Redis를 재조회합니다.
큐 부하에 맞게 이 값을 조정하면 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 같은 시그널도 처리되지 않으니 주의하세요.
기타 드라이버 사전 요구 사항
아래 큐 드라이버를 사용하려면 Composer를 통해 해당 패키지를 설치해야 합니다.
- Amazon SQS:
aws/aws-sdk-php ~3.0 - Beanstalkd:
pda/pheanstalk ~5.0 - Redis:
predis/predis ~2.0또는 phpredis PHP 확장 - MongoDB:
mongodb/laravel-mongodb
Job 생성하기
Job 클래스 생성
큐에서 처리할 Job 클래스는 기본적으로 app/Jobs 디렉터리에 저장됩니다. 해당 디렉터리가 없더라도 make:job Artisan 명령을 실행하면 자동으로 생성됩니다.
php artisan make:job ProcessPodcast생성된 클래스는 Illuminate\Contracts\Queue\ShouldQueue 인터페이스를 구현합니다. 이 인터페이스가 있으면 Laravel이 해당 Job을 큐에 넣어 비동기로 실행해야 한다는 것을 인식합니다.
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;
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* Job을 실행합니다.
*/
public function handle(AudioProcessor $processor): void
{
// 업로드된 팟캐스트 처리...
}
}예시에서 주목할 점은, Eloquent 모델을 Job의 생성자에 직접 전달할 수 있다는 것입니다. Queueable 트레이트 덕분에 Eloquent 모델과 로드된 관계(Relation)는 Job이 직렬화·역직렬화될 때 올바르게 처리됩니다.
큐에 저장될 때는 모델의 식별자(ID)만 직렬화되고, 실제로 Job이 실행될 때 큐 시스템이 데이터베이스에서 전체 모델과 관계를 다시 조회합니다. 이 방식 덕분에 큐 드라이버로 전송되는 Job 페이로드 크기를 크게 줄일 수 있습니다.
`handle` 메서드 의존성 주입
handle 메서드는 큐 워커가 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 관계(Relation)
Job이 큐에 등록될 때 로드된 Eloquent 관계까지 함께 직렬화되므로, 직렬화된 문자열이 매우 커질 수 있습니다. 또한 Job이 역직렬화될 때 관계 데이터를 데이터베이스에서 다시 조회하는데, 이때 큐 등록 전에 적용했던 관계 제약 조건(constraints)은 복원되지 않습니다. 따라서 특정 관계의 일부 데이터만 다루고 싶다면, Job 내부에서 다시 조건을 지정해야 합니다.
관계 직렬화를 아예 막으려면 모델에서 withoutRelations 메서드를 호출하세요. 이 메서드는 로드된 관계가 제거된 모델 인스턴스를 반환합니다.
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct(
Podcast $podcast,
) {
$this->podcast = $podcast->withoutRelations();
}PHP 생성자 프로퍼티 승격(constructor property promotion)을 사용하고 있다면, WithoutRelations 어트리뷰트를 활용할 수 있습니다.
use Illuminate\Queue\Attributes\WithoutRelations;
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct(
#[WithoutRelations]
public Podcast $podcast,
) {}클래스 전체에 WithoutRelations 어트리뷰트를 적용하면, 해당 Job의 모든 모델 프로퍼티에서 관계를 직렬화하지 않도록 한 번에 설정할 수 있습니다.
<?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;
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct(
public Podcast $podcast,
public DistributionPlatform $platform,
) {}
}단일 모델이 아닌 Eloquent 모델의 컬렉션이나 배열을 Job에 전달하는 경우, Job이 역직렬화·실행될 때 컬렉션 내 모델들의 관계는 복원되지 않습니다. 대량의 모델을 다루는 Job에서 과도한 리소스 소비를 방지하기 위한 의도적인 동작입니다.
유니크 Job
WARNING
유니크 Job은 원자적 잠금(atomic lock)을 지원하는 캐시 드라이버가 필요합니다. 현재 memcached, redis, dynamodb, database, file, array 캐시 드라이버가 원자적 잠금을 지원합니다.
WARNING
유니크 Job 제약은 배치(batch) 내의 Job에는 적용되지 않습니다.
특정 Job이 큐에 항상 하나의 인스턴스만 존재하도록 보장하고 싶을 때가 있습니다. ShouldBeUnique 인터페이스를 구현하면 됩니다. 추가로 정의해야 할 메서드는 없습니다.
<?php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
// ...
}위 예시에서 UpdateSearchIndex Job은 유니크하게 처리됩니다. 즉, 동일한 Job이 이미 큐에 있고 아직 처리가 완료되지 않았다면, 새로운 디스패치는 무시됩니다.
유니크 키를 직접 지정하거나 유니크 상태를 유지할 제한 시간을 설정하려면, uniqueId와 uniqueFor 프로퍼티 또는 메서드를 정의하면 됩니다.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
/**
* 상품 인스턴스.
*
* @var \App\Models\Product
*/
public $product;
/**
* 유니크 잠금이 해제되기까지의 시간(초).
*
* @var int
*/
public $uniqueFor = 3600;
/**
* Job의 유니크 ID를 반환합니다.
*/
public function uniqueId(): string
{
return $this->product->id;
}
}위 예시에서 UpdateSearchIndex Job은 상품 ID를 기준으로 유니크합니다. 동일한 상품 ID로 새 Job을 디스패치해도, 기존 Job이 완료될 때까지 무시됩니다. 또한 기존 Job이 1시간 이내에 처리되지 않으면 유니크 잠금이 해제되어 동일한 유니크 키로 다시 디스패치할 수 있습니다.
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 키로 잠금(lock)을 시도합니다. 이미 잠금이 걸려 있으면 Job은 디스패치되지 않습니다. 잠금은 Job 처리가 완료되거나 모든 재시도가 실패했을 때 해제됩니다. 기본적으로 기본 캐시 드라이버를 사용하여 잠금을 획득하지만, uniqueVia 메서드를 정의하면 다른 캐시 드라이버를 지정할 수 있습니다.
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
// ...
/**
* 유니크 Job 잠금에 사용할 캐시 드라이버를 반환합니다.
*/
public function uniqueVia(): Repository
{
return Cache::driver('redis');
}
}NOTE
Job의 동시 처리 수만 제한하고 싶다면, 유니크 Job 대신 WithoutOverlapping 미들웨어를 사용하세요.
암호화된 Job
Laravel의 암호화 기능을 활용하면 Job 데이터의 기밀성과 무결성을 보장할 수 있습니다. ShouldBeEncrypted 인터페이스를 Job 클래스에 추가하기만 하면, Laravel이 Job을 큐에 등록하기 전에 자동으로 암호화합니다.
<?php
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
{
// ...
}큐
Job 미들웨어
Job 미들웨어를 사용하면 큐 Job 실행 전후에 공통 로직을 감싸서 처리할 수 있습니다. 덕분에 각 Job 클래스에 중복 코드를 반복해서 작성할 필요가 없어집니다.
예를 들어, 아래 handle 메서드는 Laravel의 Redis 속도 제한(rate limiting) 기능을 직접 사용해 5초에 한 번만 Job을 처리하도록 구현한 코드입니다:
use Illuminate\Support\Facades\Redis;
/**
* Job 실행
*/
public function handle(): void
{
Redis::throttle('key')->block(0)->allow(1)->every(5)->then(function () {
info('락 획득 완료...');
// Job 처리...
}, function () {
// 락 획득 실패...
return $this->release(5);
});
}이 코드는 정상적으로 동작하지만, handle 메서드 안에 Redis 속도 제한 로직이 뒤섞여 가독성이 떨어집니다. 게다가 동일한 제한이 필요한 다른 Job이 생길 때마다 같은 코드를 반복해야 합니다.
이런 경우 속도 제한 로직만 담당하는 Job 미들웨어를 별도로 만들면 훨씬 깔끔해집니다:
<?php
namespace App\Jobs\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class RateLimited
{
/**
* 큐 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 인스턴스와 다음 처리를 계속하기 위한 콜백($next)을 인자로 받습니다.
make:job-middleware Artisan 명령어로 Job 미들웨어 클래스를 생성할 수 있습니다. 생성한 미들웨어는 Job 클래스의 middleware 메서드에서 반환하면 됩니다. 단, make:job 명령어로 생성한 Job에는 middleware 메서드가 기본 포함되지 않으므로 직접 추가해야 합니다:
use App\Jobs\Middleware\RateLimited;
/**
* Job에 적용할 미들웨어 반환
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited];
}NOTE
Job 미들웨어는 큐에 등록된 이벤트 리스너, 메일러블, 알림에도 동일하게 적용할 수 있습니다.
속도 제한 (Rate Limiting)
앞서 속도 제한 미들웨어를 직접 작성하는 방법을 살펴봤지만, Laravel에는 이미 속도 제한 미들웨어가 내장되어 있습니다. 라우트 속도 제한과 마찬가지로, Job 속도 제한도 RateLimiter 파사드의 for 메서드로 정의합니다.
예를 들어, 일반 사용자에게는 시간당 1회 데이터 백업을 허용하고 프리미엄 회원에게는 제한 없이 허용하고 싶다면, AppServiceProvider의 boot 메서드에서 다음과 같이 설정할 수 있습니다:
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
/**
* 애플리케이션 서비스 부트스트랩
*/
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 메서드에는 임의의 값을 전달할 수 있으며, 보통 사용자별로 제한을 분리할 때 사용자 ID를 넘깁니다:
return Limit::perMinute(50)->by($job->user->id);속도 제한을 정의했다면, Illuminate\Queue\Middleware\RateLimited 미들웨어를 Job에 연결합니다. 제한을 초과한 Job은 자동으로 적절한 지연 시간과 함께 큐로 되돌아갑니다:
use Illuminate\Queue\Middleware\RateLimited;
/**
* Job에 적용할 미들웨어 반환
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited('backups')];
}NOTE
속도 제한으로 인해 Job이 큐로 되돌아가더라도 총 시도 횟수(attempts)는 계속 증가합니다. 따라서 Job 클래스의 tries 및 maxExceptions 값을 적절히 조정하거나, retryUntil 메서드를 사용해 재시도 가능 기간을 지정하세요.
releaseAfter 메서드를 사용하면 되돌아간 Job이 다시 시도되기까지 기다려야 하는 초 단위 시간을 직접 지정할 수 있습니다:
/**
* Job에 적용할 미들웨어 반환
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new RateLimited('backups'))->releaseAfter(60)];
}속도 제한에 걸렸을 때 Job을 재시도하지 않고 그냥 버리고 싶다면 dontRelease 메서드를 사용하세요:
/**
* Job에 적용할 미들웨어 반환
*
* @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이 있을 때, 같은 사용자 ID에 대한 Job이 동시에 실행되지 않도록 하려면 다음과 같이 작성합니다:
use Illuminate\Queue\Middleware\WithoutOverlapping;
/**
* Job에 적용할 미들웨어 반환
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new WithoutOverlapping($this->user->id)];
}중복으로 인해 큐로 되돌아간 Job도 시도 횟수가 증가합니다. tries와 maxExceptions 속성을 상황에 맞게 조정하세요. 예를 들어 tries를 기본값인 1로 두면, 중복된 Job은 한 번 시도 후 재시도 없이 종료됩니다.
releaseAfter 메서드로 되돌아간 Job이 다시 시도되기까지 기다리는 시간을 지정할 수 있습니다:
/**
* Job에 적용할 미들웨어 반환
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->releaseAfter(60)];
}중복된 Job을 큐로 되돌리지 않고 즉시 삭제하려면 dontRelease 메서드를 사용하세요:
/**
* Job에 적용할 미들웨어 반환
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->dontRelease()];
}WithoutOverlapping 미들웨어는 Laravel의 원자적 락(atomic lock)을 기반으로 동작합니다. 그런데 Job이 예상치 못하게 실패하거나 타임아웃이 발생하면 락이 해제되지 않을 수 있습니다. 이런 상황을 대비해 expireAfter 메서드로 락의 만료 시간을 명시적으로 지정할 수 있습니다. 아래 예시는 Job 처리 시작 후 3분(180초)이 지나면 락을 자동으로 해제합니다:
/**
* Job에 적용할 미들웨어 반환
*
* @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 메서드를 사용하세요:
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)
Illuminate\Queue\Middleware\ThrottlesExceptions 미들웨어를 사용하면 예외가 일정 횟수 이상 발생했을 때 Job 실행을 지정된 시간 동안 지연시킬 수 있습니다. 외부 API처럼 불안정한 서드파티 서비스와 통신하는 Job에 특히 유용합니다.
예를 들어, 서드파티 API에 연결하는 Job이 예외를 던지기 시작한다고 가정해 봅시다. ThrottlesExceptions 미들웨어는 보통 시간 기반 재시도와 함께 사용합니다:
use DateTime;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Job에 적용할 미들웨어 반환
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new ThrottlesExceptions(10, 5 * 60)];
}
/**
* Job 타임아웃 시각 결정
*/
public function retryUntil(): DateTime
{
return now()->plus(minutes: 30);
}첫 번째 인자는 스로틀이 걸리기 전까지 허용되는 예외 발생 횟수이고, 두 번째 인자는 스로틀 후 다시 시도하기까지 기다리는 초 단위 시간입니다. 위 예시에서는 예외가 10번 연속으로 발생하면 5분 대기 후 재시도하며, 전체 재시도 가능 시간은 30분입니다.
예외가 발생했지만 임계치에 아직 도달하지 않은 경우, Job은 기본적으로 즉시 재시도됩니다. backoff 메서드를 사용하면 임계치 미달 시에도 몇 분간 지연 후 재시도하도록 설정할 수 있습니다:
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Job에 적용할 미들웨어 반환
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 5 * 60))->backoff(5)];
}이 미들웨어는 내부적으로 Laravel 캐시 시스템을 사용하며, Job 클래스 이름을 캐시 키로 활용합니다. 여러 Job이 같은 서드파티 서비스를 공유하고 단일 스로틀 버킷을 함께 쓰고 싶다면, by 메서드로 키를 직접 지정하세요:
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Job에 적용할 미들웨어 반환
*
* @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;
/**
* Job에 적용할 미들웨어 반환
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 10 * 60))->when(
fn (Throwable $throwable) => $throwable instanceof HttpClientException
)];
}when 메서드는 조건에 맞지 않는 예외를 큐로 되돌리거나 예외를 그대로 던지는 반면, deleteWhen 메서드는 특정 예외가 발생했을 때 Job을 아예 삭제합니다:
use App\Exceptions\CustomerDeletedException;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Job에 적용할 미들웨어 반환
*
* @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;
/**
* Job에 적용할 미들웨어 반환
*
* @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 건너뛰기
Skip 미들웨어를 사용하면 Job 클래스의 로직을 수정하지 않고도 특정 조건에서 Job을 건너뛰거나 삭제할 수 있습니다. Skip::when은 조건이 true일 때, Skip::unless는 조건이 false일 때 Job을 삭제합니다:
use Illuminate\Queue\Middleware\Skip;
/**
* Job에 적용할 미들웨어 반환
*/
public function middleware(): array
{
return [
Skip::when($condition),
];
}더 복잡한 조건이 필요하다면 when 및 unless 메서드에 클로저를 전달하세요:
use Illuminate\Queue\Middleware\Skip;
/**
* Job에 적용할 미들웨어 반환
*/
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
{
/**
* 새 팟캐스트를 저장합니다.
*/
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
{
/**
* 새 팟캐스트를 저장합니다.
*/
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분입니다. 이보다 긴 지연은 SQS에서 지원되지 않습니다.
동기 디스패치
Job을 큐에 넣지 않고 현재 프로세스에서 즉시 실행하려면 dispatchSync 메서드를 사용하세요.
<?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
{
/**
* 새 팟캐스트를 저장합니다.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// 팟캐스트 생성...
ProcessPodcast::dispatchSync($podcast);
return redirect('/podcasts');
}
}지연 동기 디스패치 (Deferred)
deferred 커넥션을 사용하면 HTTP 응답을 사용자에게 먼저 보낸 뒤, 현재 프로세스 안에서 Job을 처리합니다. 큐 워커 없이도 사용자 응답 속도에 영향을 주지 않고 작업을 처리할 수 있습니다.
RecordDelivery::dispatch($order)->onConnection('deferred');deferred 커넥션은 페일오버 큐의 기본 후보이기도 합니다.
background 커넥션도 응답 전송 후에 Job을 처리하지만, 별도로 생성된 PHP 프로세스에서 실행됩니다. 덕분에 PHP-FPM / 애플리케이션 워커가 다음 HTTP 요청을 바로 받을 수 있습니다.
RecordDelivery::dispatch($order)->onConnection('background');Job과 데이터베이스 트랜잭션
데이터베이스 트랜잭션 안에서 Job을 디스패치하는 것 자체는 문제가 없습니다. 다만, 트랜잭션이 아직 커밋되기 전에 워커가 Job을 먼저 처리하기 시작하면, 트랜잭션 안에서 변경한 데이터가 DB에 반영되지 않은 상태일 수 있습니다. 이 경우 트랜잭션 중 생성하거나 수정한 레코드가 Job 실행 시점에 존재하지 않을 수도 있습니다.
이 문제를 해결하는 가장 간단한 방법은 큐 커넥션 설정에서 after_commit 옵션을 true로 설정하는 것입니다.
'redis' => [
'driver' => 'redis',
// ...
'after_commit' => true,
],after_commit이 true이면, 열려 있는 트랜잭션이 모두 커밋된 후에야 Job이 실제로 큐에 등록됩니다. 트랜잭션이 없는 상황이라면 즉시 디스패치됩니다. 트랜잭션 도중 예외가 발생해 롤백되면, 그 트랜잭션 안에서 디스패치된 Job은 폐기됩니다.
NOTE
after_commit을 true로 설정하면 큐에 등록된 이벤트 리스너, Mailable, 알림, 브로드캐스트 이벤트도 모두 트랜잭션 커밋 이후에 디스패치됩니다.
개별 Job에 커밋 시점 지정
after_commit 옵션을 전역으로 설정하지 않더라도, 특정 Job에 한해 커밋 이후 디스패치되도록 afterCommit을 체이닝할 수 있습니다.
use App\Jobs\ProcessPodcast;
ProcessPodcast::dispatch($podcast)->afterCommit();반대로 after_commit이 true로 설정된 상태에서도 특정 Job을 즉시 디스패치하려면 beforeCommit을 사용하세요.
ProcessPodcast::dispatch($podcast)->beforeCommit();Job 체이닝
Job 체이닝을 사용하면 첫 번째 Job이 성공적으로 완료된 후 순서대로 실행될 Job 목록을 지정할 수 있습니다. 체인 중 하나라도 실패하면 이후 Job들은 실행되지 않습니다. Bus 파사드의 chain 메서드를 활용하세요.
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이 실패했을 때뿐입니다.
체인의 커넥션과 큐 지정
체인 전체에 적용할 커넥션과 큐 이름은 onConnection과 onQueue로 지정합니다. 개별 Job에 명시적으로 다른 값이 설정되어 있지 않으면 이 설정이 적용됩니다.
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->onConnection('redis')->onQueue('podcasts')->dispatch();실행 중 체인에 Job 추가
이미 실행 중인 체인에서 Job을 앞이나 뒤에 동적으로 추가하고 싶을 때는 prependToChain과 appendToChain을 사용하세요.
/**
* Job을 실행합니다.
*/
public function handle(): void
{
// ...
// 현재 Job 바로 다음에 실행되도록 체인 앞에 추가
$this->prependToChain(new TranscribePodcast);
// 체인의 맨 끝에 추가
$this->appendToChain(new TranscribePodcast);
}체인 실패 처리
체인 내 Job이 실패했을 때 실행할 콜백을 catch 메서드로 등록할 수 있습니다. 콜백은 실패 원인이 된 Throwable 인스턴스를 받습니다.
use Illuminate\Support\Facades\Bus;
use Throwable;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->catch(function (Throwable $e) {
// 체인 내 Job이 실패했습니다...
})->dispatch();WARNING
체인 콜백은 직렬화(serialize)되어 나중에 큐 워커에 의해 실행되므로, 콜백 안에서 $this를 사용하면 안 됩니다.
큐와 커넥션 커스터마이징
특정 큐로 디스패치
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
{
/**
* 새 팟캐스트를 저장합니다.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// 팟캐스트 생성...
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;
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct()
{
$this->onQueue('processing');
}
}WARNING
생성자에서 onQueue로 큐를 지정하는 방식은 Job 클래스에서만 동작합니다. 큐 이벤트 리스너에서는 viaQueue 메서드나 $queue 프로퍼티를 사용하세요.
특정 커넥션으로 디스패치
애플리케이션에서 여러 큐 커넥션을 사용한다면 onConnection 메서드로 커넥션을 지정할 수 있습니다.
<?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
{
/**
* 새 팟캐스트를 저장합니다.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// 팟캐스트 생성...
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;
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct()
{
$this->onConnection('sqs');
}
}최대 시도 횟수 / 타임아웃 설정
최대 시도 횟수
시도 횟수(attempt)는 Laravel 큐 시스템의 핵심 개념으로, 여러 고급 기능의 동작 방식과 직결됩니다. 기본값을 변경하기 전에 정확히 어떻게 동작하는지 이해하는 것이 중요합니다.
Job이 디스패치되면 큐에 등록되고, 워커가 이를 꺼내 실행을 시도합니다. 이것이 하나의 "시도"입니다. 단, handle 메서드가 실행되지 않아도 시도 횟수가 소비되는 경우가 있습니다.
- Job 실행 중 처리되지 않은 예외가 발생한 경우
$this->release()로 Job을 큐에 다시 반환한 경우WithoutOverlapping이나RateLimited같은 미들웨어가 락 획득에 실패해 Job을 반환한 경우- Job이 타임아웃된 경우
handle메서드가 예외 없이 정상 완료된 경우
Job을 무한정 재시도하는 것은 바람직하지 않으므로, Laravel은 다양한 방법으로 시도 횟수나 시간을 제한할 수 있습니다.
NOTE
기본적으로 Laravel은 Job을 한 번만 시도합니다. WithoutOverlapping, RateLimited 같은 미들웨어를 사용하거나 release()를 직접 호출하는 Job이라면, tries 값을 늘려야 합니다.
queue:work 명령의 --tries 옵션으로 워커 전체의 최대 시도 횟수를 지정할 수 있습니다. Job 클래스에 별도 설정이 없으면 이 값이 적용됩니다.
php artisan queue:work --tries=3최대 시도 횟수를 초과한 Job은 "실패한 Job"으로 처리됩니다. 자세한 내용은 실패한 Job 처리 섹션을 참고하세요. --tries=0을 지정하면 Job이 무한 재시도됩니다.
Job 클래스에 직접 $tries 프로퍼티를 정의하면 커맨드라인의 --tries 값보다 우선 적용됩니다.
<?php
namespace App\Jobs;
class ProcessPodcast implements ShouldQueue
{
/**
* Job 최대 시도 횟수.
*
* @var int
*/
public $tries = 5;
}동적으로 시도 횟수를 결정해야 한다면 tries 메서드를 정의하세요.
/**
* Job의 최대 시도 횟수를 반환합니다.
*/
public function tries(): int
{
return 5;
}시간 기반 시도 제한
횟수 대신 특정 시점까지만 재시도되도록 하려면 retryUntil 메서드를 정의하세요. 이 메서드는 DateTime 인스턴스를 반환해야 합니다.
use DateTime;
/**
* Job이 더 이상 시도되지 않아야 할 시점을 반환합니다.
*/
public function retryUntil(): DateTime
{
return now()->plus(minutes: 10);
}retryUntil과 tries가 모두 정의된 경우 retryUntil이 우선 적용됩니다.
최대 예외 횟수
재시도 횟수는 많이 허용하되, 처리되지 않은 예외가 일정 횟수 이상 발생하면 Job을 실패 처리하고 싶을 때 $maxExceptions 프로퍼티를 사용합니다. release()로 직접 반환한 경우는 예외 횟수에 포함되지 않습니다.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Redis;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Job 최대 시도 횟수.
*
* @var int
*/
public $tries = 25;
/**
* 허용할 최대 미처리 예외 횟수.
*
* @var int
*/
public $maxExceptions = 3;
/**
* Job을 실행합니다.
*/
public function handle(): void
{
Redis::throttle('key')->allow(10)->every(60)->then(function () {
// 락 획득 성공, 팟캐스트 처리...
}, function () {
// 락 획득 실패...
return $this->release(10);
});
}
}위 예시에서 Redis 락을 획득하지 못하면 10초 후 재시도하며, 최대 25번까지 시도합니다. 단, 처리되지 않은 예외가 3번 발생하면 즉시 실패 처리됩니다.
타임아웃
실행 시간이 예측 가능한 Job이라면 타임아웃을 설정하는 것이 좋습니다. 기본 타임아웃은 60초이며, 이 시간을 초과하면 워커는 에러와 함께 종료됩니다. 워커는 프로세스 매니저에 의해 자동으로 재시작됩니다.
queue:work 명령의 --timeout 옵션으로 타임아웃을 지정할 수 있습니다.
php artisan queue:work --timeout=30타임아웃으로 인해 최대 시도 횟수를 초과하면 Job은 실패 처리됩니다.
Job 클래스에 $timeout 프로퍼티를 정의하면 커맨드라인 값보다 우선 적용됩니다.
<?php
namespace App\Jobs;
class ProcessPodcast implements ShouldQueue
{
/**
* Job이 타임아웃되기 전까지 실행될 수 있는 최대 초.
*
* @var int
*/
public $timeout = 120;
}소켓이나 외부 HTTP 요청처럼 I/O가 블로킹되는 경우에는 PHP 타임아웃 설정이 적용되지 않을 수 있습니다. 이런 경우에는 라이브러리 자체의 타임아웃 옵션도 반드시 설정하세요. 예를 들어 Guzzle을 사용한다면 커넥션 타임아웃과 요청 타임아웃을 함께 지정해야 합니다.
WARNING
Job 타임아웃 기능을 사용하려면 PHP의 PCNTL 확장이 설치되어 있어야 합니다. 또한 timeout 값은 반드시 retry after 값보다 작아야 합니다. 그렇지 않으면 Job이 실제로 완료되거나 타임아웃되기 전에 다시 시도될 수 있습니다.
타임아웃 시 실패 처리
기본적으로 타임아웃이 발생하면 시도 횟수 1회가 소비되고 Job이 큐로 반환됩니다(남은 시도 횟수가 있는 경우). 타임아웃 즉시 Job을 실패 처리하려면 $failOnTimeout 프로퍼티를 true로 설정하세요.
/**
* 타임아웃 시 Job을 실패 처리할지 여부.
*
* @var bool
*/
public $failOnTimeout = true;NOTE
$failOnTimeout이 true이면 타임아웃 발생 시 tries 값과 관계없이 재시도되지 않고 바로 실패 처리됩니다.
SQS FIFO 및 공정 큐
Laravel은 Amazon SQS FIFO(First-In-First-Out) 큐를 지원합니다. FIFO 큐를 사용하면 Job이 전송된 순서대로 처리되며, 메시지 중복 제거(deduplication)를 통해 정확히 한 번(exactly-once) 처리를 보장합니다.
FIFO 큐에서는 메시지 그룹 ID로 병렬 처리 범위를 결정합니다. 같은 그룹 ID를 가진 Job은 순차 처리되고, 다른 그룹 ID를 가진 Job은 병렬로 처리될 수 있습니다.
onGroup 메서드로 메시지 그룹 ID를 지정할 수 있습니다.
ProcessOrder::dispatch($order)
->onGroup("customer-{$order->customer_id}");중복 제거 ID를 직접 지정하려면 Job 클래스에 deduplicationId 메서드를 구현하세요.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessSubscriptionRenewal implements ShouldQueue
{
use Queueable;
// ...
/**
* Job의 중복 제거 ID를 반환합니다.
*/
public function deduplicationId(): string
{
return "renewal-{$this->subscription->id}";
}
}FIFO 큐의 리스너, Mailable, 알림
FIFO 큐를 사용하는 경우, 이벤트 리스너·Mailable·알림에도 메시지 그룹을 지정해야 합니다. 그렇지 않으면 해당 객체들을 일반(non-FIFO) 큐로 디스패치해야 합니다.
큐 이벤트 리스너의 경우 리스너에 messageGroup 메서드를 정의하세요. 필요에 따라 deduplicationId 메서드도 함께 정의할 수 있습니다.
<?php
namespace App\Listeners;
class SendShipmentNotification
{
// ...
/**
* Job의 메시지 그룹을 반환합니다.
*/
public function messageGroup(): string
{
return 'shipments';
}
/**
* Job의 중복 제거 ID를 반환합니다.
*/
public function deduplicationId(): string
{
return "shipment-notification-{$this->shipment->id}";
}
}FIFO 큐를 사용하는 Mailable을 보낼 때는 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 큐를 사용하는 알림을 보낼 때도 동일하게 적용합니다.
use App\Notifications\InvoicePaid;
$invoicePaid = (new InvoicePaid($invoice))
->onGroup('invoices')
->withDeduplicator(fn () => 'invoices-'.$invoice->id);
$user->notify($invoicePaid);큐 페일오버
failover 큐 드라이버는 Job을 등록할 때 기본 커넥션이 실패하면 자동으로 다음 커넥션으로 전환하는 기능을 제공합니다. 운영 환경에서 큐의 고가용성을 확보하는 데 유용합니다.
페일오버 큐 커넥션은 failover 드라이버와 함께 시도할 커넥션 목록을 배열로 지정합니다. Laravel은 기본적으로 config/queue.php에 예시 설정을 포함하고 있습니다.
'failover' => [
'driver' => 'failover',
'connections' => [
'redis',
'database',
'sync',
],
],설정 후 .env 파일에서 기본 큐 커넥션을 failover로 지정합니다.
QUEUE_CONNECTION=failover그런 다음 페일오버 목록에 포함된 각 커넥션마다 워커를 하나 이상 실행하세요.
php artisan queue:work redisphp artisan queue:work databaseNOTE
sync, background, deferred 드라이버는 현재 PHP 프로세스 안에서 Job을 처리하므로 별도의 워커가 필요 없습니다.
큐 커넥션이 실패해 페일오버가 활성화되면 Illuminate\Queue\Events\QueueFailedOver 이벤트가 발생합니다. 이 이벤트를 리스닝하면 커넥션 실패를 로깅하거나 알림을 보낼 수 있습니다.
NOTE
Laravel Horizon을 사용하는 경우, Horizon은 Redis 큐만 관리합니다. 페일오버 목록에 database가 포함되어 있다면 php artisan queue:work database 프로세스를 Horizon과 별도로 실행해야 합니다.
에러 처리
Job 처리 중 예외가 발생하면 Job은 자동으로 큐로 반환되어 재시도됩니다. 최대 시도 횟수에 도달할 때까지 이 과정이 반복됩니다. 최대 시도 횟수는 queue:work 명령의 --tries 옵션이나 Job 클래스의 $tries 프로퍼티로 설정합니다. 큐 워커 실행에 대한 자세한 내용은 아래 섹션을 참고하세요.
수동으로 Job 반환
특정 상황에서 Job을 큐로 직접 되돌려 나중에 다시 시도하게 하려면 release 메서드를 호출하세요.
/**
* Job을 실행합니다.
*/
public function handle(): void
{
// ...
$this->release();
}기본적으로 release는 Job을 즉시 처리 가능 상태로 큐에 반환합니다. 일정 시간이 지난 후에 처리되도록 하려면 초 단위 정수 또는 날짜 인스턴스를 인자로 전달하세요.
$this->release(10);
$this->release(now()->plus(seconds: 10));수동으로 Job 실패 처리
경우에 따라 Job을 직접 실패 처리해야 할 때는 fail 메서드를 호출하세요.
/**
* Job을 실행합니다.
*/
public function handle(): void
{
// ...
$this->fail();
}잡은 예외를 fail 메서드에 전달하거나, 간단히 에러 메시지 문자열을 전달할 수도 있습니다.
$this->fail($exception);
$this->fail('처리 중 문제가 발생했습니다.');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\Middleware\FailOnException;
use Illuminate\Support\Facades\Http;
class SyncChatHistory implements ShouldQueue
{
use Queueable;
public $tries = 3;
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct(
public User $user,
) {}
/**
* Job을 실행합니다.
*/
public function handle(): void
{
$this->user->authorize('sync-chat-history');
$response = Http::throw()->get(
"https://chat.laravel.test/?user={$this->user->uuid}"
);
// ...
}
/**
* Job이 통과할 미들웨어를 반환합니다.
*/
public function middleware(): array
{
return [
new FailOnException([AuthorizationException::class])
];
}
}Job 배치 처리
Laravel의 Job 배치(Batch) 기능을 사용하면 여러 Job을 병렬로 실행하고, 전체 배치가 완료된 후 특정 작업을 수행할 수 있습니다.
시작하기 전에, 배치의 메타 정보(완료율 등)를 저장할 데이터베이스 테이블을 생성해야 합니다. make:queue-batches-table Artisan 명령으로 마이그레이션 파일을 생성하세요:
php artisan make:queue-batches-tablephp artisan migrate배치 가능한 Job 정의하기
배치로 실행할 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;
/**
* Job 실행
*/
public function handle(): void
{
if ($this->batch()->cancelled()) {
// 배치가 취소된 경우 처리 중단
return;
}
// CSV 파일의 일부 데이터를 가져오는 처리...
}
}배치 디스패치하기
배치를 실행하려면 Bus 파사드의 batch 메서드를 사용합니다. 배치는 완료 콜백과 함께 사용할 때 가장 유용합니다. then, catch, finally 메서드로 각 단계별 콜백을 정의할 수 있으며, 각 콜백은 Illuminate\Bus\Batch 인스턴스를 인자로 받습니다.
여러 큐 워커가 실행 중이라면 배치 내 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를 확인할 수 있으며, 이 ID를 이용해 배치 상태를 조회할 수 있습니다.
WARNING
배치 콜백은 직렬화되어 나중에 큐에서 실행되므로, 콜백 내부에서 $this 변수를 사용하면 안 됩니다. 또한 배치 Job은 데이터베이스 트랜잭션 내에서 실행되므로, 암묵적 커밋(implicit commit)을 유발하는 데이터베이스 구문은 Job 내부에서 실행하지 않아야 합니다.
배치 이름 지정
Laravel Horizon이나 Laravel Telescope 같은 도구는 배치에 이름이 있을 때 더 유용한 디버그 정보를 제공합니다. 배치에 이름을 지정하려면 name 메서드를 사용하세요:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료되었을 때...
})->name('CSV 가져오기')->dispatch();배치 커넥션 및 큐 지정
배치 Job에 사용할 커넥션과 큐를 지정하려면 onConnection과 onQueue 메서드를 사용하세요. 배치 내 모든 Job은 동일한 커넥션과 큐에서 실행되어야 합니다:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료되었을 때...
})->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) {
// 모든 Job이 성공적으로 완료되었을 때...
})->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을 웹 요청 중에 한꺼번에 디스패치하기 어려울 때 유용합니다. 초기에는 "로더(loader)" Job 몇 개만 배치에 추가하고, 각 로더 Job이 실행되면서 실제 Job들을 배치에 채워 넣는 방식으로 활용할 수 있습니다:
$batch = Bus::batch([
new LoadImportBatch,
new LoadImportBatch,
new LoadImportBatch,
])->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료되었을 때...
})->name('연락처 가져오기')->dispatch();LoadImportBatch Job 내부에서는 batch() 메서드로 현재 배치에 접근하여 add 메서드로 새 Job을 추가합니다:
use App\Jobs\ImportContacts;
use Illuminate\Support\Collection;
/**
* 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 인스턴스는 배치 상태를 확인하고 조작할 수 있는 다양한 속성과 메서드를 제공합니다:
// 배치의 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으로 응답할 수 있습니다. 프론트엔드에서 진행 상황을 실시간으로 표시할 때 유용합니다.
배치 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 메서드를 호출하세요:
/**
* 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;
/**
* Job이 통과할 미들웨어 반환
*/
public function middleware(): array
{
return [new SkipIfBatchCancelled];
}배치 실패 처리
배치 내 Job이 실패하면 catch 콜백이 호출됩니다. 이 콜백은 배치 내에서 처음 실패한 Job에 대해서만 한 번 호출된다는 점에 유의하세요.
실패 허용하기
기본적으로 배치 내 Job이 하나라도 실패하면 Laravel은 해당 배치를 "취소됨" 상태로 표시합니다. 이 동작을 비활성화하여 일부 Job이 실패하더라도 배치가 계속 진행되도록 하려면 allowFailures 메서드를 사용하세요:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료되었을 때...
})->allowFailures()->dispatch();개별 Job 실패 시 실행할 클로저를 allowFailures에 전달할 수도 있습니다:
$batch = Bus::batch([
// ...
])->allowFailures(function (Batch $batch, $exception) {
// 개별 Job 실패를 처리하는 로직...
})->dispatch();실패한 배치 Job 재시도하기
실패한 배치 Job을 일괄 재시도하려면 queue:retry-batch Artisan 명령을 사용하세요. 재시도할 배치의 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 실패 후 재시도 없이 완료되지 않은 배치 레코드도 누적될 수 있습니다. 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에 배치 정보 저장하기
Laravel은 관계형 데이터베이스 대신 DynamoDB에 배치 메타 정보를 저장하는 것도 지원합니다. 단, 배치 레코드를 저장할 DynamoDB 테이블은 직접 생성해야 합니다.
테이블 이름은 보통 job_batches를 사용하지만, queue 설정 파일의 queue.batching.table 값을 기준으로 결정됩니다.
DynamoDB 배치 테이블 설정
job_batches 테이블에는 다음 키가 필요합니다:
- 파티션 키(Partition Key):
application(문자열 타입) —app설정 파일의name값이 저장됩니다. 애플리케이션 이름을 키의 일부로 사용하므로 하나의 DynamoDB 테이블을 여러 Laravel 애플리케이션이 공유할 수 있습니다. - 정렬 키(Sort Key):
id(문자열 타입)
자동 배치 정리를 활용하려면 테이블에 ttl 속성도 정의하세요.
DynamoDB 설정
먼저 AWS SDK를 설치합니다:
composer require aws/aws-sdk-php그런 다음 queue.batching.driver 설정값을 dynamodb로 변경하고, batching 배열에 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에 배치 정보를 저장하는 경우, 관계형 데이터베이스용 queue:prune-batches 명령은 동작하지 않습니다. 대신 DynamoDB의 네이티브 TTL 기능을 활용하여 오래된 레코드를 자동으로 삭제할 수 있습니다.
DynamoDB 테이블에 ttl 속성을 정의했다면, Laravel이 배치 레코드의 만료를 관리하도록 설정할 수 있습니다. queue.batching.ttl_attribute는 TTL 값을 저장할 속성 이름을, queue.batching.ttl은 레코드가 마지막으로 업데이트된 시점으로부터 몇 초 후에 삭제될지를 지정합니다:
'batching' => [
'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' => '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 콜백은 직렬화(serialize)된 후 나중에 Laravel 큐에 의해 실행되므로, catch 콜백 내부에서는 $this 변수를 사용하지 않아야 합니다.
큐
큐 워커 실행하기
`queue:work` 명령어
Laravel은 큐 워커를 시작하고 큐에 추가된 새로운 Job을 처리하는 Artisan 명령어를 제공합니다. queue:work 명령어로 워커를 실행할 수 있습니다. 한 번 시작된 워커는 수동으로 중지하거나 터미널을 닫기 전까지 계속 실행됩니다.
php artisan queue:workNOTE
queue:work 프로세스를 백그라운드에서 지속적으로 실행하려면 Supervisor와 같은 프로세스 관리자를 사용하여 워커가 중단되지 않도록 해야 합니다.
처리된 Job ID, 연결 이름, 큐 이름을 출력에 포함하려면 -v 플래그를 추가하세요.
php artisan queue:work -v큐 워커는 장기 실행 프로세스입니다. 부팅된 애플리케이션 상태를 메모리에 유지하기 때문에, 워커가 시작된 이후에 코드를 변경해도 해당 변경 사항이 반영되지 않습니다. 따라서 배포 과정에서 반드시 큐 워커를 재시작해야 합니다. 또한 애플리케이션에서 생성하거나 수정한 정적 상태(static state)는 Job 사이에 자동으로 초기화되지 않으므로 주의하세요.
대안으로 queue:listen 명령어를 사용할 수도 있습니다. 이 명령어는 코드가 변경되거나 애플리케이션 상태를 초기화해야 할 때 워커를 수동으로 재시작하지 않아도 됩니다. 하지만 queue:work에 비해 성능이 현저히 낮으므로 프로덕션 환경에서는 일반적으로 권장하지 않습니다.
php artisan queue:listen여러 큐 워커 실행하기
하나의 큐에 여러 워커를 할당해 Job을 동시에 처리하려면 queue:work 프로세스를 여러 개 실행하면 됩니다. 로컬에서는 터미널 탭을 여러 개 열어 실행할 수 있고, 프로덕션 환경에서는 프로세스 관리자의 설정을 활용하면 됩니다. Supervisor를 사용하는 경우 numprocs 설정값으로 워커 수를 지정할 수 있습니다.
연결 및 큐 지정하기
워커가 사용할 큐 연결을 직접 지정할 수 있습니다. 전달하는 연결 이름은 config/queue.php 파일에 정의된 연결 중 하나여야 합니다.
php artisan queue:work redis기본적으로 queue:work 명령어는 해당 연결의 기본 큐에 있는 Job만 처리합니다. 특정 큐만 처리하도록 더 세밀하게 지정할 수도 있습니다. 예를 들어 이메일 관련 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 컨테이너 환경에서 큐가 비워지면 컨테이너도 함께 종료하고 싶을 때 유용합니다.
php artisan queue:work --stop-when-empty일정 시간 동안만 Job 처리하기
--max-time 옵션을 사용하면 지정한 시간(초) 동안 Job을 처리한 후 워커가 종료됩니다. Supervisor와 함께 사용하면 일정 시간마다 워커를 자동으로 재시작하여 메모리를 정리할 수 있습니다.
<h1 id="worker-restart-and-pause-signals">1시간 동안 Job을 처리한 후 종료</h1>
php artisan queue:work --max-time=3600워커 슬립(Sleep) 시간 설정
큐에 Job이 있으면 워커는 쉬지 않고 계속 처리합니다. 큐가 비어 있을 때 워커가 몇 초 동안 "슬립" 상태로 대기할지는 --sleep 옵션으로 지정합니다. 슬립 중에는 새로운 Job을 처리하지 않습니다.
php artisan queue:work --sleep=3점검 모드와 큐
애플리케이션이 점검 모드(maintenance mode)일 때는 큐에 등록된 Job이 처리되지 않습니다. 점검 모드가 해제되면 Job 처리가 정상적으로 재개됩니다.
점검 모드 중에도 강제로 Job을 처리하려면 --force 옵션을 사용하세요.
php artisan queue:work --force리소스 관리 주의사항
데몬 방식의 큐 워커는 Job을 처리하기 전에 프레임워크를 재부팅하지 않습니다. 따라서 각 Job이 완료된 후에는 무거운 리소스를 반드시 해제해야 합니다. 예를 들어 GD 라이브러리로 이미지를 처리한다면 작업 완료 후 imagedestroy를 호출해 메모리를 해제해야 합니다.
큐 우선순위
때로는 여러 큐의 처리 순서를 제어해야 할 때가 있습니다. 예를 들어 config/queue.php 파일에서 redis 연결의 기본 큐를 low로 설정했더라도, 특정 Job은 high 우선순위 큐에 디스패치하고 싶을 수 있습니다.
dispatch((new Job)->onQueue('high'));low 큐의 Job보다 high 큐의 Job을 먼저 처리하도록 하려면, 큐 이름을 쉼표로 구분해 work 명령어에 전달하세요.
php artisan queue:work --queue=high,low큐 워커와 배포
큐 워커는 장기 실행 프로세스이므로, 재시작하기 전까지는 코드 변경 사항을 인식하지 못합니다. 따라서 배포 시 가장 간단한 방법은 queue:restart 명령어로 워커를 재시작하는 것입니다.
php artisan queue:restart이 명령어는 모든 큐 워커에게 현재 처리 중인 Job을 완료한 후 종료하도록 지시합니다. 이미 처리 중인 Job이 유실되지 않으므로 안전합니다. 워커가 종료된 후 자동으로 재시작되려면 Supervisor와 같은 프로세스 관리자가 반드시 실행 중이어야 합니다.
NOTE
재시작 신호는 캐시를 통해 전달됩니다. 이 기능을 사용하기 전에 캐시 드라이버가 올바르게 설정되어 있는지 확인하세요.
Job 만료 및 타임아웃
Job 만료 (`retry_after`)
config/queue.php 파일의 각 큐 연결에는 retry_after 옵션이 있습니다. 이 옵션은 처리 중인 Job이 지정된 시간(초) 내에 완료되지 않으면 큐로 다시 돌려보내도록 설정합니다. 예를 들어 retry_after가 90이면, Job이 90초 내에 완료·삭제되지 않을 경우 큐로 반환됩니다. 일반적으로 이 값은 Job이 정상적으로 완료되는 데 걸리는 최대 시간으로 설정하는 것이 좋습니다.
WARNING
Amazon SQS는 retry_after 옵션을 지원하지 않습니다. SQS는 AWS 콘솔에서 관리되는 기본 가시성 타임아웃(Default Visibility Timeout)을 기준으로 Job을 재시도합니다.
워커 타임아웃 (`--timeout`)
queue:work 명령어는 --timeout 옵션을 지원합니다. 기본값은 60초입니다. 지정된 시간 이상 Job이 처리되고 있다면 해당 워커는 오류와 함께 종료됩니다. 이후 Supervisor가 워커를 자동으로 재시작합니다.
php artisan queue:work --timeout=60retry_after와 --timeout은 서로 다른 설정이지만, 함께 동작하여 Job이 유실되지 않고 단 한 번만 성공적으로 처리되도록 보장합니다.
WARNING
--timeout 값은 반드시 retry_after 값보다 몇 초 이상 짧게 설정해야 합니다. 이렇게 해야 무한 대기 상태에 빠진 Job을 처리하는 워커가 해당 Job이 큐로 반환되기 전에 종료될 수 있습니다. --timeout이 retry_after보다 길면 동일한 Job이 두 번 처리될 수 있습니다.
큐 워커 일시 중지 및 재개
워커를 완전히 중지하지 않고 새로운 Job 처리만 일시적으로 멈춰야 할 때가 있습니다. 시스템 점검 중 Job 처리를 잠시 중단하고 싶은 경우가 대표적입니다. Laravel은 이를 위해 queue:pause와 queue:continue 명령어를 제공합니다.
특정 큐를 일시 중지하려면 큐 연결 이름과 큐 이름을 함께 전달합니다.
php artisan queue:pause database:default위 예시에서 database는 큐 연결 이름이고 default는 큐 이름입니다. 큐가 일시 중지되면 해당 큐의 워커는 현재 처리 중인 Job은 완료하지만, 큐가 재개될 때까지 새로운 Job은 가져오지 않습니다.
일시 중지된 큐의 Job 처리를 재개하려면 queue:continue 명령어를 사용하세요.
php artisan queue:continue database:default재개 후 워커는 즉시 해당 큐에서 새로운 Job을 처리하기 시작합니다. 큐를 일시 중지해도 워커 프로세스 자체는 종료되지 않으며, 지정된 큐에서 새로운 Job을 가져오는 동작만 멈춘다는 점을 기억하세요.
워커 재시작·일시 중지 신호 폴링
기본적으로 큐 워커는 매 Job 반복(iteration)마다 캐시 드라이버에 재시작·일시 중지 신호가 있는지 확인합니다. 이 폴링은 queue:restart와 queue:pause 명령어에 응답하기 위해 필요하지만, 소량의 성능 오버헤드를 유발합니다.
이 인터럽트 기능이 필요 없고 성능을 최적화하고 싶다면 AppServiceProvider의 boot 메서드에서 Queue 파사드의 withoutInterruptionPolling 메서드를 호출해 폴링을 전역으로 비활성화할 수 있습니다.
use Illuminate\Support\Facades\Queue;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Queue::withoutInterruptionPolling();
}재시작과 일시 중지 폴링을 개별적으로 비활성화하려면 Illuminate\Queue\Worker 클래스의 정적 프로퍼티를 설정하세요.
use Illuminate\Queue\Worker;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Worker::$restartable = false;
Worker::$pausable = false;
}WARNING
인터럽트 폴링을 비활성화하면 비활성화된 기능에 해당하는 queue:restart 또는 queue:pause 명령어에 워커가 응답하지 않습니다.
Supervisor 설정
프로덕션 환경에서는 queue:work 프로세스가 항상 실행 중인 상태를 유지해야 합니다. 그런데 워커 타임아웃 초과, queue:restart 명령 실행 등 다양한 이유로 queue:work 프로세스가 종료될 수 있습니다.
이런 상황에 대비해, 프로세스가 종료되는 것을 감지하고 자동으로 재시작해주는 프로세스 모니터가 필요합니다. 프로세스 모니터를 사용하면 queue:work 프로세스를 몇 개나 동시에 실행할지도 지정할 수 있습니다. Linux 환경에서 가장 널리 사용되는 프로세스 모니터인 Supervisor의 설정 방법을 아래에서 설명합니다.
Supervisor 설치
Supervisor는 Linux 운영체제용 프로세스 모니터로, queue:work 프로세스가 비정상 종료되면 자동으로 재시작해줍니다. Ubuntu에서는 다음 명령으로 설치할 수 있습니다.
sudo apt-get install supervisorNOTE
Supervisor를 직접 설정하고 관리하는 것이 번거롭게 느껴진다면, Laravel Cloud를 고려해보세요. Laravel Cloud는 큐 워커 실행을 위한 완전 관리형 플랫폼을 제공합니다.
Supervisor 설정
Supervisor 설정 파일은 일반적으로 /etc/supervisor/conf.d 디렉터리에 저장됩니다. 이 디렉터리 안에 원하는 만큼 설정 파일을 만들어 각 프로세스의 모니터링 방식을 지정할 수 있습니다. 예를 들어, 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 처리가 완료되기 전에 프로세스를 강제 종료할 수 있습니다.
Supervisor 시작
설정 파일을 작성했다면, 다음 명령으로 Supervisor 설정을 갱신하고 프로세스를 시작합니다.
sudo supervisorctl rereadsudo supervisorctl updatesudo supervisorctl start "laravel-worker:*"Supervisor에 대한 자세한 내용은 Supervisor 공식 문서를 참고하세요.
실패한 Job 처리
큐에 등록된 Job이 실패하는 경우가 생길 수 있습니다. 이는 드문 일이 아닙니다. Laravel은 Job의 최대 시도 횟수를 지정하는 편리한 방법을 제공합니다. 비동기 Job이 최대 시도 횟수를 초과하면 failed_jobs 데이터베이스 테이블에 해당 Job 정보가 기록됩니다. 단, 동기적으로 디스패치된 Job이 실패하는 경우에는 이 테이블에 저장되지 않으며, 발생한 예외는 즉시 애플리케이션에서 처리됩니다.
새로 생성된 Laravel 애플리케이션에는 보통 failed_jobs 테이블을 위한 마이그레이션이 이미 포함되어 있습니다. 만약 해당 마이그레이션이 없다면, make:queue-failed-table 명령어로 생성할 수 있습니다.
php artisan make:queue-failed-tablephp artisan migrate큐 워커를 실행할 때 queue:work 명령어의 --tries 옵션으로 Job의 최대 시도 횟수를 지정할 수 있습니다. --tries 값을 지정하지 않으면 Job 클래스의 $tries 프로퍼티에 정의된 횟수만큼, 또는 그것도 없으면 단 한 번만 시도합니다.
php artisan queue:work redis --tries=3--backoff 옵션을 사용하면 예외가 발생한 Job을 재시도하기 전에 몇 초를 기다릴지 설정할 수 있습니다. 기본적으로는 대기 없이 즉시 큐에 다시 반환됩니다.
php artisan queue:work redis --tries=3 --backoff=3Job별로 재시도 대기 시간을 다르게 설정하고 싶다면, Job 클래스에 backoff 프로퍼티를 정의하면 됩니다.
/**
* 재시도 전 대기 시간(초)
*
* @var int
*/
public $backoff = 3;더 복잡한 로직이 필요한 경우, backoff 메서드를 정의할 수도 있습니다.
/**
* 재시도 전 대기 시간을 계산합니다.
*/
public function backoff(): int
{
return 3;
}backoff 메서드에서 배열을 반환하면 지수 백오프(exponential backoff) 를 간단하게 구현할 수 있습니다. 아래 예시에서 첫 번째 재시도는 1초, 두 번째는 5초, 세 번째는 10초 후에 실행되며, 이후 시도가 더 남아 있다면 마지막 값인 10초가 계속 적용됩니다.
/**
* 재시도 전 대기 시간을 계산합니다.
*
* @return array<int, int>
*/
public function backoff(): array
{
return [1, 5, 10];
}실패 후 정리 작업
Job이 실패했을 때 사용자에게 알림을 보내거나, 부분적으로 처리된 작업을 되돌리는 등의 후처리가 필요할 수 있습니다. 이를 위해 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;
/**
* Job 인스턴스를 생성합니다.
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* Job을 실행합니다.
*/
public function handle(AudioProcessor $processor): void
{
// 업로드된 팟캐스트 처리...
}
/**
* Job 실패를 처리합니다.
*/
public function failed(?Throwable $exception): void
{
// 사용자에게 실패 알림 발송 등...
}
}WARNING
failed 메서드가 호출될 때는 Job의 새 인스턴스가 생성됩니다. 따라서 handle 메서드 실행 중에 클래스 프로퍼티에 적용된 변경 사항은 failed 메서드에서는 유지되지 않습니다.
실패한 Job이 반드시 처리되지 않은 예외를 던졌다는 의미는 아닙니다. 허용된 모든 시도 횟수를 소진한 경우에도 실패로 처리됩니다. 시도 횟수가 소진되는 경우는 다음과 같습니다.
- Job 실행이 타임아웃으로 종료된 경우
- Job 실행 중 처리되지 않은 예외가 발생한 경우
- Job이 수동으로 또는 미들웨어에 의해 큐로 반환된 경우
마지막 시도 중 예외가 발생하여 실패하면, 해당 예외가 failed 메서드로 전달됩니다. 최대 허용 시도 횟수를 초과하여 실패한 경우에는 $exception이 Illuminate\Queue\MaxAttemptsExceededException 인스턴스가 됩니다. 설정된 타임아웃을 초과하여 실패한 경우에는 Illuminate\Queue\TimeoutExceededException 인스턴스가 전달됩니다.
실패한 Job 재시도
failed_jobs 테이블에 기록된 실패한 Job 목록은 queue:failed Artisan 명령어로 확인할 수 있습니다.
php artisan queue:failed이 명령어는 Job ID, 연결(connection), 큐 이름, 실패 시각 등의 정보를 출력합니다. 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 전체를 재시도하려면 --queue 옵션을 사용합니다.
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 명령어로 실패한 Job을 삭제해야 합니다.
failed_jobs 테이블의 모든 레코드를 삭제하려면 queue:flush 명령어를 실행합니다.
php artisan queue:flushqueue:flush는 기록된 날짜와 관계없이 모든 실패한 Job 레코드를 삭제합니다. 특정 기간보다 오래된 레코드만 삭제하려면 --hours 옵션을 사용하세요.
php artisan queue:flush --hours=48모델이 없을 때 Job 무시하기
Eloquent 모델을 Job에 주입하면, 큐에 등록될 때 모델이 자동으로 직렬화(serialize)되고, Job이 처리될 때 데이터베이스에서 다시 조회됩니다. 그런데 워커가 Job을 처리하기 전에 해당 모델이 삭제된 경우, ModelNotFoundException이 발생하여 Job이 실패할 수 있습니다.
이런 상황을 간편하게 처리하려면 Job 클래스의 deleteWhenMissingModels 프로퍼티를 true로 설정하세요. 이 값이 true이면 Laravel은 예외를 발생시키지 않고 해당 Job을 조용히 폐기합니다.
/**
* 관련 모델이 존재하지 않으면 Job을 삭제합니다.
*
* @var bool
*/
public $deleteWhenMissingModels = true;실패한 Job 레코드 정리(Pruning)
queue:prune-failed Artisan 명령어를 실행하면 failed_jobs 테이블의 오래된 레코드를 정리할 수 있습니다.
php artisan queue:prune-failed기본적으로 24시간이 지난 레코드가 삭제됩니다. --hours 옵션을 사용하면 최근 N시간 이내의 레코드는 유지하고 그보다 오래된 레코드만 삭제합니다. 아래 예시는 48시간 이전에 기록된 레코드를 모두 삭제합니다.
php artisan queue:prune-failed --hours=48DynamoDB에 실패한 Job 저장하기
Laravel은 관계형 데이터베이스 테이블 대신 DynamoDB에 실패한 Job 레코드를 저장하는 것도 지원합니다. 단, DynamoDB 테이블은 직접 생성해야 합니다. 테이블 이름은 보통 failed_jobs로 지정하지만, 애플리케이션의 queue 설정 파일에서 queue.failed.table 값으로 정의된 이름을 따르는 것이 좋습니다.
DynamoDB 테이블에는 application이라는 문자열 기본 파티션 키(primary partition key)와 uuid라는 문자열 기본 정렬 키(primary sort key)가 필요합니다. application 값에는 app 설정 파일의 name 값으로 정의된 애플리케이션 이름이 사용됩니다. 애플리케이션 이름이 키의 일부이므로, 하나의 DynamoDB 테이블에 여러 Laravel 애플리케이션의 실패한 Job을 함께 저장할 수 있습니다.
먼저 AWS SDK를 설치합니다.
composer require aws/aws-sdk-php그런 다음 queue.failed.driver 설정값을 dynamodb로 변경하고, 인증에 필요한 key, secret, region을 설정합니다. 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로 지정하면 Laravel이 실패한 Job을 저장하지 않고 즉시 폐기합니다. 보통 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
{
/**
* 애플리케이션 서비스를 등록합니다.
*/
public function register(): void
{
// ...
}
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Queue::failing(function (JobFailed $event) {
// $event->connectionName
// $event->job
// $event->exception
});
}
}큐에서 Job 삭제하기
NOTE
Horizon을 사용하는 경우, queue:clear 명령 대신 horizon:clear 명령을 사용해 큐의 Job을 삭제해야 합니다.
기본 커넥션의 기본 큐에 대기 중인 모든 Job을 삭제하려면 queue:clear Artisan 명령을 실행합니다:
php artisan queue:clear특정 커넥션과 큐를 지정해 삭제하려면 connection 인수와 --queue 옵션을 함께 사용합니다:
php artisan queue:clear redis --queue=emailsWARNING
큐 Job 삭제 기능은 SQS, Redis, database 큐 드라이버에서만 지원됩니다. 또한 SQS는 메시지 삭제 처리에 최대 60초가 소요될 수 있으므로, 큐를 비운 직후 60초 이내에 SQS 큐로 전송된 Job도 함께 삭제될 수 있습니다.
큐 모니터링
큐에 Job이 갑자기 대량으로 유입되면 처리 지연이 발생하고 전체 대기 시간이 크게 늘어날 수 있습니다. Laravel은 큐의 Job 수가 지정한 임계값을 초과할 때 알림을 보내는 기능을 제공합니다.
시작하려면 queue:monitor 명령을 1분마다 실행되도록 스케줄에 등록하세요. 명령 인수로 모니터링할 큐 이름과 최대 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;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Event::listen(function (QueueBusy $event) {
Notification::route('mail', 'dev@example.com')
->notify(new QueueHasLongWaitTime(
$event->connection,
$event->queue,
$event->size
));
});
}NOTE
QueueBusy 이벤트에는 connection(연결 이름), queue(큐 이름), size(현재 Job 수) 속성이 포함되어 있습니다. 알림 클래스에서 이 정보를 활용하면 어느 큐에서 문제가 발생했는지 팀원이 즉시 파악할 수 있습니다.
테스트
Job을 디스패치하는 코드를 테스트할 때, 실제로 Job이 실행되지 않도록 Laravel에 지시하고 싶을 때가 있습니다. Job 코드 자체는 별도로 직접 테스트할 수 있기 때문입니다. 물론 Job 자체를 테스트하려면 Job 인스턴스를 직접 생성하고 테스트 내에서 handle 메서드를 호출하면 됩니다.
Queue 파사드의 fake 메서드를 사용하면 큐에 실제로 Job이 추가되지 않도록 막을 수 있습니다. fake 메서드를 호출한 뒤에는 애플리케이션이 큐에 Job을 추가하려 했는지 여러 assertion 메서드로 검증할 수 있습니다.
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::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::assertPushedTimes(ShipOrder::class, 2);
// 특정 Job이 추가되지 않았는지 확인...
Queue::assertNotPushed(AnotherJob::class);
// 클로저가 큐에 추가됐는지 확인...
Queue::assertClosurePushed();
// 클로저가 큐에 추가되지 않았는지 확인...
Queue::assertClosureNotPushed();
// 추가된 Job의 총 개수 확인...
Queue::assertCount(3);
}
}assertPushed, assertNotPushed, assertClosurePushed, assertClosureNotPushed 메서드에 클로저를 전달하면, 조건을 직접 정의하여 검증할 수 있습니다. 클로저가 true를 반환하는 Job이 하나 이상 존재하면 assertion이 성공합니다.
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';
});일부 Job만 Fake 처리하기
특정 Job만 Fake 처리하고 나머지 Job은 실제로 실행되도록 하려면, fake 메서드에 Fake 처리할 Job 클래스 이름을 배열로 전달하면 됩니다.
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);
}반대로 특정 Job을 제외한 나머지를 모두 Fake 처리하려면 except 메서드를 사용하면 됩니다.
Queue::fake()->except([
ShipOrder::class,
]);Job 체인 테스트
Job 체인을 테스트하려면 Bus 파사드의 Fake 기능을 활용해야 합니다. 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
]);위 예시처럼 클래스 이름 배열을 전달할 수도 있고, 실제 Job 인스턴스 배열을 전달할 수도 있습니다. 인스턴스를 전달하는 경우 Laravel은 클래스 타입뿐만 아니라 프로퍼티 값까지 일치하는지 확인합니다.
Bus::assertChained([
new ShipOrder,
new RecordShipment,
new UpdateInventory,
]);체인 없이 단독으로 디스패치된 Job을 검증하려면 assertDispatchedWithoutChain 메서드를 사용하세요.
Bus::assertDispatchedWithoutChain(ShipOrder::class);체인 수정 테스트
체인 내의 Job이 실행 중에 체인 앞이나 뒤에 Job을 추가하는 경우, Job 인스턴스의 assertHasChain 메서드로 남은 체인이 예상과 일치하는지 검증할 수 있습니다.
$job = new ProcessPodcast;
$job->handle();
$job->assertHasChain([
new TranscribePodcast,
new OptimizePodcast,
new ReleasePodcast,
]);남은 체인이 비어 있는지 확인하려면 assertDoesntHaveChain 메서드를 사용하세요.
$job->assertDoesntHaveChain();체인 내 배치 테스트
Job 체인에 배치가 포함된 경우, 체인 assertion 내에 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 메서드로 Fake 배치를 Job에 할당합니다. withFakeBatch는 Job 인스턴스와 Fake 배치를 담은 튜플을 반환합니다.
[$job, $batch] = (new ShipOrder)->withFakeBatch();
$job->handle();
$this->assertTrue($batch->cancelled());
$this->assertEmpty($batch->added);Job과 큐 간의 상호작용 테스트
때로는 큐에 등록된 Job이 스스로를 다시 큐에 반환하거나 삭제하는 동작을 테스트해야 할 수 있습니다. Job 인스턴스를 생성하고 withFakeQueueInteractions 메서드를 호출하면 이러한 큐 상호작용을 Fake 처리할 수 있습니다.
Fake 처리 후 handle 메서드를 호출하면, 다양한 assertion 메서드로 Job의 큐 상호작용 결과를 검증할 수 있습니다.
use App\Exceptions\CorruptedAudioException;
use App\Jobs\ProcessPodcast;
$job = (new ProcessPodcast)->withFakeQueueInteractions();
$job->handle();
$job->assertReleased(delay: 30); // 30초 후 재시도로 반환됐는지 확인
$job->assertDeleted(); // Job이 삭제됐는지 확인
$job->assertNotDeleted(); // Job이 삭제되지 않았는지 확인
$job->assertFailed(); // Job이 실패했는지 확인
$job->assertFailedWith(CorruptedAudioException::class); // 특정 예외로 실패했는지 확인
$job->assertNotFailed(); // Job이 실패하지 않았는지 확인큐
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
{
/**
* 애플리케이션 서비스를 등록합니다.
*/
public function register(): void
{
// ...
}
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Queue::before(function (JobProcessing $event) {
// $event->connectionName — 큐 연결 이름
// $event->job — Job 인스턴스
// $event->job->payload() — Job 페이로드
});
Queue::after(function (JobProcessed $event) {
// $event->connectionName — 큐 연결 이름
// $event->job — Job 인스턴스
// $event->job->payload() — Job 페이로드
});
}
}Queue 파사드의 looping 메서드를 사용하면, 워커가 큐에서 Job을 가져오기 직전마다 실행할 콜백을 등록할 수 있습니다. 예를 들어, 이전에 실패한 Job이 열어둔 채 남겨진 데이터베이스 트랜잭션을 롤백하는 용도로 활용할 수 있습니다.
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
Queue::looping(function () {
while (DB::transactionLevel() > 0) {
DB::rollBack();
}
});NOTE
looping 콜백은 Job을 처리할 때마다가 아니라, 워커 루프가 반복될 때마다 실행됩니다. 따라서 미처 닫히지 않은 트랜잭션을 정리하는 것처럼 워커 상태를 초기화하는 작업에 적합합니다.