큐
번역일: 2026년 7월 2일
큐
- 소개
- Job 생성
- Job 미들웨어
- Job 디스패치
- Job 배치
- 클로저 큐잉
- 큐 워커 실행
- Supervisor 설정
- 실패한 Job 처리
- 큐에서 Job 삭제
- 큐 모니터링
- 테스트
- Job 이벤트
소개
웹 애플리케이션을 개발하다 보면, 업로드된 CSV 파일을 파싱하거나 이메일을 전송하는 것처럼 처리 시간이 길어서 HTTP 요청 중에 즉시 실행하기 부담스러운 작업들이 생깁니다. Laravel은 이런 작업들을 백그라운드에서 처리할 수 있도록 큐 시스템을 제공합니다. 작업을 큐에 넣어두면 사용자는 빠른 응답을 받고, 무거운 처리는 워커가 나중에 실행합니다.
Laravel의 큐 API는 Amazon SQS, Redis, 관계형 데이터베이스 등 다양한 백엔드를 동일한 인터페이스로 지원합니다. 큐 설정은 config/queue.php 파일에서 관리합니다.
NOTE
Laravel은 Redis 기반 큐를 위한 아름다운 대시보드인 Horizon도 제공합니다. 자세한 내용은 Horizon 문서를 참고하세요.
커넥션 vs. 큐
처음 Laravel 큐를 접할 때 헷갈리기 쉬운 개념이 **커넥션(connection)**과 **큐(queue)**의 차이입니다.
- 커넥션: 큐 백엔드 서비스와의 연결 정보입니다 (예: SQS, Redis, 데이터베이스).
- 큐: 커넥션 안에서 Job이 쌓이는 실제 대기열입니다. 하나의 커넥션에 여러 큐를 둘 수 있습니다.
config/queue.php의 각 커넥션 설정에는 queue 항목이 있는데, 이것이 해당 커넥션에 디스패치될 때 기본으로 사용할 큐 이름입니다. 즉, Job을 어느 큐로 보낼지 명시하지 않으면 이 기본 큐에 들어갑니다.
use App\Jobs\ProcessPodcast;
// 기본 커넥션의 기본 큐에 디스패치
ProcessPodcast::dispatch();
// 기본 커넥션의 "emails" 큐에 디스패치
ProcessPodcast::dispatch()->onQueue('emails');큐를 분리하면 Job의 우선순위를 유연하게 조정할 수 있고, 여러 워커를 큐별로 나누어 처리량을 높일 수 있습니다.
드라이버 참고사항 및 사전 요구사항
데이터베이스
database 큐 드라이버를 사용하려면 Job을 저장할 테이블이 필요합니다. 보통 Laravel 기본 마이그레이션에 포함되어 있지만, 없다면 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' => [
'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이 올 때까지 얼마나 기다릴지 지정할 수 있습니다.
새로운 Job을 확인하기 위해 Redis를 반복적으로 폴링하는 것보다, block_for를 적절히 설정하면 효율적입니다. 예를 들어 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또는phpredisPHP 확장
큐
목차
소개
웹 애플리케이션을 개발하다 보면, 업로드된 CSV 파일을 파싱하거나 저장하는 것처럼 일반적인 웹 요청 안에서 처리하기엔 너무 오래 걸리는 작업이 생기기 마련입니다. Laravel은 이런 작업을 백그라운드에서 처리할 수 있도록 큐 Job을 쉽게 만들 수 있는 기능을 제공합니다. 시간이 오래 걸리는 작업을 큐로 넘기면, 애플리케이션은 웹 요청에 훨씬 빠르게 응답할 수 있고 사용자 경험도 크게 향상됩니다.
Laravel 큐는 Amazon SQS, Redis, 또는 관계형 데이터베이스 등 다양한 큐 백엔드를 통합된 API로 사용할 수 있게 해줍니다.
큐 관련 설정은 config/queue.php 파일에 있습니다. 이 파일에는 프레임워크가 기본 제공하는 각 큐 드라이버의 커넥션 설정이 포함되어 있습니다. 지원 드라이버로는 database, Amazon SQS, Redis, Beanstalkd가 있으며, Job을 즉시 동기 실행하는 sync 드라이버(로컬 개발용)와 큐에 추가된 Job을 그냥 버리는 null 드라이버도 포함되어 있습니다.
NOTE
Laravel은 Redis 기반 큐를 위한 대시보드와 설정 시스템인 Horizon을 제공합니다. 자세한 내용은 Horizon 문서를 참고하세요.
커넥션과 큐의 차이
Laravel 큐를 사용하기 전에, 커넥션(connection) 과 큐(queue) 의 차이를 먼저 이해하는 것이 중요합니다.
config/queue.php 파일에는 connections 배열이 있습니다. 이 항목은 Amazon SQS, Beanstalkd, Redis 같은 큐 백엔드 서비스에 대한 연결 정보를 정의합니다. 하나의 커넥션 안에는 여러 개의 큐가 존재할 수 있으며, 각 큐는 Job이 쌓이는 별개의 작업 더미라고 생각하면 됩니다.
커넥션 (예: redis)├── 큐: default├── 큐: emails└── 큐: high설정 파일의 각 커넥션에는 queue 속성이 있는데, 이것이 해당 커넥션의 기본 큐입니다. Job을 디스패치할 때 큐를 명시하지 않으면 이 기본 큐로 전송됩니다.
use App\Jobs\ProcessPodcast;
// 기본 커넥션의 기본 큐로 전송됩니다.
ProcessPodcast::dispatch();
// 기본 커넥션의 "emails" 큐로 전송됩니다.
ProcessPodcast::dispatch()->onQueue('emails');대부분의 애플리케이션은 단순한 큐 하나만으로 충분하지만, 여러 큐를 활용하면 Job의 우선순위를 지정하거나 처리 방식을 구분할 수 있습니다. 예를 들어, high 큐에 쌓인 Job을 더 높은 우선순위로 처리하고 싶다면 워커를 다음과 같이 실행하면 됩니다.
php artisan queue:work --queue=high,default드라이버 참고사항 및 사전 요구사항
Database
database 큐 드라이버를 사용하려면 Job을 저장할 데이터베이스 테이블이 필요합니다. Laravel 기본 마이그레이션 파일(0001_01_01_000002_create_jobs_table.php)에 이미 포함되어 있는 경우가 많지만, 없다면 아래 명령어로 마이그레이션 파일을 생성할 수 있습니다.
php artisan make:queue-tablephp artisan migrateRedis
redis 큐 드라이버를 사용하려면 config/database.php에 Redis 커넥션이 설정되어 있어야 합니다.
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,
],블로킹(Blocking)
Redis 큐를 사용할 때 block_for 옵션을 설정하면, 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 같은 시스템 신호도 처리되지 않으니 주의하세요.
기타 드라이버 사전 요구사항
아래 큐 드라이버를 사용하려면 각각 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 모델을 생성자에 직접 주입하고 있다는 것입니다. Queueable 트레이트 덕분에 Eloquent 모델과 이미 로드된 관계(relationships)는 Job이 큐에 저장될 때 자동으로 직렬화(serialize)되고, 처리 시점에 다시 역직렬화(unserialize)됩니다.
큐에 저장될 때는 모델의 식별자(기본 키)만 직렬화됩니다. 실제로 Job이 실행될 때 큐 시스템이 데이터베이스에서 전체 모델과 관계를 다시 조회합니다. 이 방식 덕분에 큐 드라이버에 전송되는 Job 페이로드 크기를 크게 줄일 수 있습니다.
`handle` 메서드 의존성 주입
handle 메서드는 큐 워커가 Job을 처리할 때 호출됩니다. handle 메서드의 파라미터에 타입 힌트를 지정하면 Laravel 서비스 컨테이너가 해당 의존성을 자동으로 주입합니다.
컨테이너의 의존성 주입 방식을 직접 제어하고 싶다면 bindMethod를 사용할 수 있습니다. 이 메서드는 Job과 컨테이너를 받는 콜백을 인자로 받으며, 콜백 내에서 원하는 방식으로 handle을 호출할 수 있습니다. 보통 App\Providers\AppServiceProvider의 boot 메서드에서 등록합니다.
use App\Jobs\ProcessPodcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Foundation\Application;
$this->app->bindMethod([ProcessPodcast::class, 'handle'], function (ProcessPodcast $job, Application $app) {
return $job->handle($app->make(AudioProcessor::class));
});WARNING
이미지 원본 데이터처럼 바이너리 데이터를 Job에 전달할 때는 반드시 base64_encode를 통해 인코딩한 뒤 전달하세요. 그렇지 않으면 큐에 저장될 때 JSON 직렬화가 제대로 되지 않을 수 있습니다.
큐와 Eloquent 관계(Relationships)
Eloquent 모델에 로드된 관계도 함께 직렬화되기 때문에, 많은 관계가 로드된 모델을 Job에 전달하면 직렬화된 Job 문자열이 예상보다 커질 수 있습니다.
또한 Job이 역직렬화될 때 관계 데이터를 데이터베이스에서 다시 조회하는데, 이때는 큐에 저장되기 전에 적용했던 쿼리 제약 조건(예: where, limit 등)이 복원되지 않습니다. 특정 관계의 일부 데이터만 필요하다면 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,
) {}단일 모델이 아닌 Eloquent 모델의 컬렉션이나 배열을 Job에 전달하는 경우, 컬렉션 내 모델들의 관계는 역직렬화 시 복원되지 않습니다. 많은 수의 모델을 다루는 Job에서 리소스가 과도하게 사용되는 것을 방지하기 위한 동작입니다.
유니크 Job
WARNING
유니크 Job 기능은 원자적 잠금(atomic lock)을 지원하는 캐시 드라이버가 필요합니다. 현재 memcached, redis, dynamodb, database, file, array 캐시 드라이버가 원자적 잠금을 지원합니다. 단, 배치(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
use App\Models\Product;
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시간(3600초) 내에 처리되지 않으면 유니크 잠금이 해제되어 동일한 키로 새 Job을 디스패치할 수 있게 됩니다.
WARNING
여러 웹 서버나 컨테이너에서 Job을 디스패치하는 환경이라면, 모든 서버가 동일한 중앙 캐시 서버를 사용하도록 설정해야 Laravel이 유니크 여부를 정확하게 판단할 수 있습니다.
처리 시작 전까지만 유니크 유지하기
기본적으로 유니크 Job의 잠금은 Job 처리가 완료되거나 모든 재시도가 실패한 후 해제됩니다. 그런데 처리가 시작되는 시점에 즉시 잠금을 해제하고 싶은 경우도 있습니다. 이때는 ShouldBeUnique 대신 ShouldBeUniqueUntilProcessing 인터페이스를 구현하세요.
<?php
use App\Models\Product;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
// ...
}유니크 Job 잠금 방식
내부적으로 ShouldBeUnique Job이 디스패치되면 Laravel은 uniqueId 키로 잠금(lock) 획득을 시도합니다. 잠금 획득에 실패하면 Job은 디스패치되지 않습니다. 잠금은 Job 처리가 완료되거나 모든 재시도가 실패한 후 해제됩니다.
기본적으로 Laravel은 기본 캐시 드라이버를 사용해 잠금을 획득하지만, 다른 드라이버를 사용하고 싶다면 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이 동시에 여러 개 처리되는 것만 막으면 된다면(유니크 큐잉이 아닌 동시 실행 제한), ShouldBeUnique 대신 WithoutOverlapping 미들웨어를 사용하세요.
암호화된 Job
Job에 담긴 데이터의 프라이버시와 무결성을 보장하려면 ShouldBeEncrypted 인터페이스를 추가하세요. 이 인터페이스가 선언된 Job은 큐에 저장되기 전에 Laravel이 자동으로 암호화합니다. 자세한 내용은 암호화 문서를 참고하세요.
<?php
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
{
// ...
}큐
Job 미들웨어
Job 미들웨어를 사용하면 큐 Job 실행 전후에 공통 로직을 감싸는 레이어를 정의할 수 있습니다. 이를 통해 각 Job의 handle 메서드에 반복되는 코드를 줄일 수 있습니다. 예를 들어, 아래 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 미들웨어로 분리하는 것이 훨씬 깔끔합니다. Laravel은 Job 미들웨어의 기본 위치를 강제하지 않으므로 원하는 곳에 자유롭게 배치할 수 있습니다. 아래 예시에서는 app/Jobs/Middleware 디렉터리에 미들웨어를 생성합니다.
<?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 인스턴스와, 다음 단계로 넘어가기 위해 호출할 클로저를 인자로 받습니다.
Job 미들웨어를 만든 뒤에는 Job 클래스의 middleware 메서드에서 반환하여 연결합니다. 이 메서드는 make:job Artisan 명령으로 생성된 Job에는 포함되어 있지 않으므로, 직접 추가해야 합니다.
use App\Jobs\Middleware\RateLimited;
/**
* Job이 통과해야 할 미들웨어 목록을 반환합니다.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited];
}NOTE
Job 미들웨어는 큐에 등록 가능한 이벤트 리스너, Mailable, 알림(Notification)에도 동일하게 적용할 수 있습니다.
요청 제한 (Rate Limiting)
직접 요청 제한 미들웨어를 작성하는 방법을 살펴봤지만, Laravel에는 이미 사용할 수 있는 내장 요청 제한 미들웨어가 포함되어 있습니다. 라우트 요청 제한과 마찬가지로, Job 요청 제한도 RateLimiter 파사드의 for 메서드로 정의합니다.
예를 들어, 일반 사용자는 1시간에 한 번만 데이터를 백업할 수 있고, VIP 고객은 제한 없이 백업을 허용하고 싶다면 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 메서드에는 원하는 값을 전달할 수 있으며, 보통 사용자별로 제한을 구분하는 데 활용합니다.
return Limit::perMinute(50)->by($job->user->id);요청 제한기를 정의했으면, Illuminate\Queue\Middleware\RateLimited 미들웨어를 Job에 연결합니다. Job이 요청 제한을 초과하면 이 미들웨어는 제한 시간에 맞춰 적절한 지연 후 Job을 다시 큐에 반환합니다.
use Illuminate\Queue\Middleware\RateLimited;
/**
* Job이 통과해야 할 미들웨어 목록을 반환합니다.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited('backups')];
}요청 제한으로 인해 Job이 큐에 다시 반환되더라도 Job의 전체 시도 횟수(attempts)는 증가합니다. Job 클래스의 tries나 maxExceptions 속성을 상황에 맞게 조정하거나, retryUntil 메서드로 시도 가능한 최대 시간을 지정하는 것을 고려하세요.
요청 제한에 걸렸을 때 Job을 다시 시도하지 않으려면 dontRelease 메서드를 사용합니다.
/**
* Job이 통과해야 할 미들웨어 목록을 반환합니다.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new RateLimited('backups'))->dontRelease()];
}NOTE
Redis를 사용하는 경우, 기본 요청 제한 미들웨어보다 Redis에 최적화된 Illuminate\Queue\Middleware\RateLimitedWithRedis 미들웨어를 사용하는 것이 더 효율적입니다.
Job 중복 실행 방지
Laravel에는 Illuminate\Queue\Middleware\WithoutOverlapping 미들웨어가 내장되어 있어, 임의의 키를 기준으로 동일한 Job이 동시에 중복 실행되는 것을 막을 수 있습니다. 특정 리소스를 한 번에 하나의 Job만 수정해야 하는 경우에 유용합니다.
예를 들어, 사용자의 신용 점수를 업데이트하는 큐 Job이 있고 동일한 사용자에 대한 Job이 동시에 실행되지 않도록 하려면, middleware 메서드에서 WithoutOverlapping 미들웨어를 반환합니다.
use Illuminate\Queue\Middleware\WithoutOverlapping;
/**
* Job이 통과해야 할 미들웨어 목록을 반환합니다.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new WithoutOverlapping($this->user->id)];
}같은 타입의 겹치는 Job은 모두 큐에 반환됩니다. 반환된 Job이 다시 시도되기까지 기다려야 하는 초(seconds)를 지정할 수도 있습니다.
/**
* 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분 후에 WithoutOverlapping 락이 해제되도록 설정합니다.
/**
* 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(),
];
}
}예외 스로틀링
Laravel에는 Illuminate\Queue\Middleware\ThrottlesExceptions 미들웨어가 내장되어 있어, 예외 발생 횟수를 기준으로 Job 재시도를 제한할 수 있습니다. 지정한 횟수만큼 예외가 발생하면, 이후의 모든 시도는 설정한 시간 간격이 지날 때까지 지연됩니다. 불안정한 외부 API와 통신하는 Job에 특히 유용합니다.
예를 들어, 서드파티 API와 연동하는 큐 Job에서 예외가 간헐적으로 발생하는 상황을 생각해 보겠습니다. 예외 스로틀링을 적용하려면 middleware 메서드에서 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()->addMinutes(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
)];
}스로틀링된 예외를 애플리케이션의 예외 핸들러에 보고하려면 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
)];
}NOTE
Redis를 사용하는 경우, 기본 예외 스로틀링 미들웨어보다 Redis에 최적화된 Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis 미들웨어를 사용하는 것이 더 효율적입니다.
Job 건너뛰기
Skip 미들웨어를 사용하면 Job의 내부 로직을 수정하지 않고도 특정 조건에서 Job을 건너뛰거나(삭제) 할 수 있습니다. Skip::when 메서드는 조건이 true일 때 Job을 삭제하고, Skip::unless 메서드는 조건이 false일 때 Job을 삭제합니다.
use Illuminate\Queue\Middleware\Skip;
/**
* Job이 통과해야 할 미들웨어 목록을 반환합니다.
*/
public function middleware(): array
{
return [
Skip::when($someCondition),
];
}더 복잡한 조건이 필요하다면 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\Http\Controllers\Controller;
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 애플리케이션의 기본 큐 드라이버는 sync입니다. 이 드라이버는 Job을 현재 요청의 포어그라운드에서 동기적으로 실행하므로 로컬 개발 시 편리합니다. 실제로 백그라운드 처리를 원한다면 config/queue.php에서 다른 큐 드라이버를 지정하세요.
지연 디스패치
Job을 큐 워커가 즉시 처리하지 않도록 하려면 delay 메서드를 사용하세요. 예를 들어 디스패치 후 10분이 지나야 처리되도록 할 수 있습니다.
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
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()->addMinutes(10));
return redirect('/podcasts');
}
}Job 클래스에 기본 지연이 설정되어 있더라도 즉시 처리가 필요하다면 withoutDelay를 사용하세요.
ProcessPodcast::dispatch($podcast)->withoutDelay();WARNING
Amazon SQS 큐 서비스의 최대 지연 시간은 15분입니다.
브라우저 응답 전송 후 디스패치
웹 서버가 FastCGI를 사용하는 경우, dispatchAfterResponse 메서드를 사용하면 HTTP 응답이 사용자 브라우저로 전송된 후에 Job이 디스패치됩니다. 큐에 올라간 Job이 아직 실행 중이더라도 사용자는 곧바로 애플리케이션을 이용할 수 있습니다. 이 방식은 이메일 발송처럼 약 1초 내외로 처리되는 작업에 적합합니다. 현재 HTTP 요청 안에서 처리되므로 별도의 큐 워커가 없어도 실행됩니다.
use App\Jobs\SendNotification;
SendNotification::dispatchAfterResponse();클로저를 dispatch한 뒤 afterResponse를 체이닝하는 방식도 가능합니다.
use App\Mail\WelcomeMessage;
use Illuminate\Support\Facades\Mail;
dispatch(function () {
Mail::to('hello@example.com')->send(new WelcomeMessage);
})->afterResponse();동기 디스패치
Job을 큐에 넣지 않고 즉시 실행하려면 dispatchSync 메서드를 사용하세요. 현재 프로세스 안에서 바로 실행됩니다.
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
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');
}
}Job과 데이터베이스 트랜잭션
데이터베이스 트랜잭션 안에서 Job을 디스패치하는 것 자체는 문제없지만, 주의가 필요합니다. 트랜잭션이 커밋되기 전에 큐 워커가 Job을 처리하기 시작할 수 있고, 그러면 트랜잭션 안에서 변경한 데이터가 아직 데이터베이스에 반영되지 않은 상태일 수 있습니다.
이 문제를 해결하는 가장 간단한 방법은 큐 커넥션 설정에 after_commit 옵션을 추가하는 것입니다.
'redis' => [
'driver' => 'redis',
// ...
'after_commit' => true,
],after_commit이 true이면, 열려 있는 모든 데이터베이스 트랜잭션이 커밋된 후에 Job이 실제로 디스패치됩니다. 열린 트랜잭션이 없다면 즉시 디스패치됩니다.
트랜잭션 도중 예외가 발생해 롤백되면, 그 트랜잭션 안에서 디스패치된 Job은 버려집니다.
NOTE
after_commit 옵션을 true로 설정하면 큐에 올라간 이벤트 리스너, Mailable, 알림, 브로드캐스트 이벤트도 모두 열린 트랜잭션이 커밋된 후에 디스패치됩니다.
인라인으로 커밋 디스패치 동작 지정
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이 실패한 경우뿐입니다.
체인의 커넥션과 큐 지정
체인에 사용할 커넥션과 큐를 지정하려면 onConnection과 onQueue 메서드를 사용하세요. 각 Job에 명시적으로 다른 커넥션/큐가 지정되지 않는 한, 이 설정이 기본값으로 적용됩니다.
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->onConnection('redis')->onQueue('podcasts')->dispatch();체인에 Job 추가
체인을 실행하는 중에 현재 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
체인 콜백은 직렬화되어 나중에 Laravel 큐가 실행하므로, 콜백 안에서 $this를 사용하면 안 됩니다.
큐와 커넥션 커스터마이징
특정 큐로 디스패치
Job을 서로 다른 큐로 분류하면 워커 수를 큐별로 유연하게 조정하고 처리 우선순위를 설정할 수 있습니다. 이는 큐 설정 파일에 정의된 다른 "커넥션"으로 보내는 것이 아니라, 하나의 커넥션 안에서 큐를 나누는 것임에 주의하세요. 디스패치 시 onQueue로 큐를 지정합니다.
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
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');
}
}특정 커넥션으로 디스패치
애플리케이션이 여러 큐 커넥션을 사용한다면, onConnection 메서드로 사용할 커넥션을 지정할 수 있습니다.
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
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');
}
}최대 시도 횟수 / 타임아웃 설정
최대 시도 횟수
큐에 올라간 Job에서 오류가 발생할 때 무한히 재시도되는 것을 원하지 않을 것입니다. Laravel은 시도 횟수나 시간을 다양한 방법으로 제한할 수 있습니다.
가장 간단한 방법은 Artisan 명령어에 --tries 옵션을 사용하는 것입니다. 이 설정은 Job 클래스에 별도로 지정하지 않은 모든 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 메서드를 Job 클래스에 정의하고 DateTime 인스턴스를 반환하면 됩니다.
use DateTime;
/**
* Job 시도를 중단할 시각을 반환합니다.
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(10);
}NOTE
큐에 올라간 이벤트 리스너에도 tries 프로퍼티나 retryUntil 메서드를 정의할 수 있습니다.
최대 예외 횟수
시도 횟수는 많이 허용하되, 처리되지 않은 예외가 일정 횟수 이상 발생하면 실패로 처리하고 싶을 때는 maxExceptions 프로퍼티를 사용하세요. release로 직접 큐에 돌려보내는 경우는 카운트에 포함되지 않습니다.
<?php
namespace App\Jobs;
use Illuminate\Support\Facades\Redis;
class ProcessPodcast implements ShouldQueue
{
/**
* 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 락을 얻지 못하면 Job은 10초 후 재시도되며 최대 25번까지 시도합니다. 단, 처리되지 않은 예외가 3번 발생하면 즉시 실패로 처리됩니다.
타임아웃
큐 Job이 실행에 예상보다 오래 걸리는 상황에 대비해 타임아웃을 지정할 수 있습니다. 기본값은 60초입니다. 타임아웃을 초과하면 워커는 오류와 함께 종료되며, 보통 프로세스 매니저에 의해 자동으로 재시작됩니다.
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 등을 사용할 때는 해당 라이브러리의 API에서도 별도로 커넥션/요청 타임아웃을 지정하는 것이 좋습니다.
WARNING
Job 타임아웃을 사용하려면 pcntl PHP 확장이 설치되어 있어야 합니다. 또한 Job의 timeout 값은 반드시 "retry after" 값보다 작아야 합니다. 그렇지 않으면 Job이 실제로 완료되거나 타임아웃되기 전에 재시도될 수 있습니다.
타임아웃 시 실패 처리
타임아웃 발생 시 Job을 실패로 표시하려면 $failOnTimeout 프로퍼티를 정의하세요.
/**
* 타임아웃 시 Job을 실패로 표시할지 여부
*
* @var bool
*/
public $failOnTimeout = true;오류 처리
Job 처리 중 예외가 발생하면 Job은 자동으로 큐에 다시 올라가 재시도됩니다. 최대 시도 횟수에 도달할 때까지 이 과정이 반복됩니다. 최대 시도 횟수는 queue:work 명령어의 --tries 옵션이나 Job 클래스에서 지정합니다. 큐 워커 실행에 대한 자세한 내용은 아래를 참고하세요.
수동으로 Job 다시 큐에 올리기
상황에 따라 Job을 수동으로 큐에 돌려보내 나중에 다시 처리하도록 할 수 있습니다. release 메서드를 호출하면 됩니다.
/**
* Job을 실행합니다.
*/
public function handle(): void
{
// ...
$this->release();
}기본적으로 release를 호출하면 Job은 즉시 처리 가능한 상태로 큐에 돌아갑니다. 정수 초(秒) 값이나 날짜 인스턴스를 전달하면 지정한 시간이 지난 후에야 처리 가능해집니다.
$this->release(10);
$this->release(now()->addSeconds(10));수동으로 Job 실패 처리
특정 상황에서 Job을 직접 "실패"로 표시해야 할 때는 fail 메서드를 호출하세요.
/**
* Job을 실행합니다.
*/
public function handle(): void
{
// ...
$this->fail();
}잡아둔 예외를 전달하거나, 문자열 메시지를 전달할 수도 있습니다.
$this->fail($exception);
$this->fail('처리 중 오류가 발생했습니다.');NOTE
실패한 Job 처리에 대한 자세한 내용은 Job 실패 처리 문서를 참고하세요.
Job 배치 처리
Laravel의 Job 배치(Batch) 기능을 사용하면 여러 Job을 묶어서 실행하고, 배치가 완료된 후 특정 동작을 수행할 수 있습니다. 시작하기 전에, 배치의 완료율 등 메타 정보를 저장할 데이터베이스 테이블을 마이그레이션으로 생성해야 합니다. make:queue-batches-table Artisan 명령어로 마이그레이션 파일을 만들 수 있습니다:
php artisan make:queue-batches-tablephp artisan migrate배치 가능한 Job 정의하기
배치 처리를 지원하는 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 인스턴스를 전달받습니다.
아래 예시는 CSV 파일을 여러 구간으로 나누어 병렬로 가져오는 배치입니다:
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
배치 콜백은 직렬화(serialize)되어 나중에 큐 워커에서 실행됩니다. 따라서 콜백 내부에서 $this 변수를 사용하면 안 됩니다. 또한, 배치 Job은 데이터베이스 트랜잭션으로 감싸지므로, 암묵적 커밋(implicit commit)을 유발하는 데이터베이스 구문은 Job 내부에서 실행하지 않도록 주의하세요.
배치에 이름 지정하기
Laravel Horizon이나 Laravel Telescope 같은 도구에서는 배치에 이름을 지정하면 디버깅 정보를 더 알아보기 쉽게 표시합니다. 배치를 정의할 때 name 메서드를 호출하면 됩니다:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료...
})->name('CSV 가져오기')->dispatch();배치 연결 및 큐 지정하기
배치 Job에 사용할 연결(connection)과 큐(queue)를 지정하려면 onConnection과 onQueue 메서드를 사용합니다. 배치 내의 모든 Job은 동일한 연결과 큐에서 실행되어야 합니다:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료...
})->onConnection('redis')->onQueue('imports')->dispatch();체인과 배치 조합하기
배치 안에 체인된 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) {
// ...
})->dispatch();반대로, 체인 안에 배치를 넣는 것도 가능합니다. 예를 들어, 먼저 여러 팟캐스트를 공개하는 배치를 실행한 뒤, 릴리즈 알림을 보내는 배치를 순서대로 실행할 수 있습니다:
use App\Jobs\FlushPodcastCache;
use App\Jobs\ReleasePodcast;
use App\Jobs\SendPodcastReleaseNotification;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new FlushPodcastCache,
Bus::batch([
new ReleasePodcast(1),
new ReleasePodcast(2),
]),
Bus::batch([
new SendPodcastReleaseNotification(1),
new SendPodcastReleaseNotification(2),
]),
])->dispatch();실행 중인 배치에 Job 추가하기
배치 Job 내부에서 동일한 배치에 추가 Job을 더할 수 있습니다. 웹 요청 처리 중에 수천 개의 Job을 한 번에 디스패치하기 어려울 때 유용한 패턴입니다. 대신 "로더" Job을 먼저 배치로 디스패치하고, 그 로더 Job이 실제 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 응답을 얻을 수 있습니다. 이를 활용하면 프론트엔드 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 메서드를 호출해 배치 실행을 취소할 수 있습니다:
/**
* Job 실행
*/
public function handle(): void
{
if ($this->user->exceedsImportLimit()) {
return $this->batch()->cancel();
}
if ($this->batch()->cancelled()) {
return;
}
}앞선 예시처럼 배치 Job 내부에서 매번 취소 여부를 직접 확인할 수도 있지만, 편의를 위해 SkipIfBatchCancelled 미들웨어를 Job에 지정하는 방법도 있습니다. 이름 그대로, 배치가 취소된 경우 해당 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 재시도하기
특정 배치에서 실패한 Job을 일괄 재시도하려면 queue:retry-batch Artisan 명령어를 사용합니다. 재시도할 배치의 UUID를 인수로 전달합니다:
php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5배치 레코드 정리하기
정리 작업을 하지 않으면 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 테이블은 application이라는 문자열 파티션 키(partition key)와 id라는 문자열 정렬 키(sort key)를 가져야 합니다. application 값에는 app 설정 파일의 name 값이 사용됩니다. 애플리케이션 이름이 키에 포함되므로, 하나의 DynamoDB 테이블로 여러 Laravel 애플리케이션의 배치 데이터를 함께 관리할 수 있습니다.
자동 배치 정리를 활용하려면 테이블에 ttl 속성을 추가로 정의하세요.
DynamoDB 연동 설정
먼저 AWS SDK를 설치합니다:
composer require aws/aws-sdk-php그런 다음 queue.batching.driver 설정 값을 dynamodb로 지정하고, batching 배열에 key, secret, region 옵션을 추가합니다. 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 클래스를 디스패치하는 대신, 클로저(익명 함수)를 직접 큐에 디스패치할 수도 있습니다. 현재 요청 사이클 밖에서 실행해야 하는 간단한 작업에 유용합니다. 클로저를 큐에 디스패치할 때, 클로저의 코드 내용은 전송 중 변조를 방지하기 위해 암호화 서명됩니다.
$podcast = App\Podcast::find(1);
dispatch(function () use ($podcast) {
$podcast->publish();
});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와 같은 프로세스 모니터를 사용해야 합니다. Supervisor는 워커가 예기치 않게 종료되더라도 자동으로 재시작해 줍니다.
처리된 Job의 ID를 출력에 포함하고 싶다면 -v 플래그를 추가하세요.
php artisan queue:work -v큐 워커는 장기 실행 프로세스입니다. 처음 시작할 때 애플리케이션의 상태를 메모리에 올려두고, 이후에는 코드 변경을 자동으로 감지하지 않습니다. 따라서 배포 시에는 반드시 큐 워커를 재시작해야 합니다. 또한 애플리케이션에서 생성하거나 수정한 정적 상태는 Job 간에 자동으로 초기화되지 않으므로 주의가 필요합니다.
대안으로 queue:listen 명령어를 사용할 수도 있습니다. 이 명령어는 코드 변경 시 워커를 수동으로 재시작할 필요가 없고 애플리케이션 상태도 자동으로 초기화됩니다. 다만 queue:work에 비해 성능이 떨어지므로 운영 환경에서는 queue:work를 권장합니다.
php artisan queue:listen여러 큐 워커 실행하기
Job을 병렬로 처리하려면 queue:work 프로세스를 여러 개 실행하면 됩니다. 로컬에서는 터미널 탭을 여러 개 열어 실행할 수 있고, 운영 환경에서는 프로세스 매니저의 설정으로 관리합니다. Supervisor를 사용하는 경우 numprocs 설정값으로 워커 수를 지정할 수 있습니다.
커넥션과 큐 지정하기
워커가 사용할 큐 커넥션을 직접 지정할 수 있습니다. 커넥션 이름은 config/queue.php에 정의된 이름과 일치해야 합니다.
php artisan queue:work redis기본적으로 queue:work는 해당 커넥션의 기본 큐만 처리합니다. 특정 큐만 처리하도록 지정하려면 --queue 옵션을 사용하세요. 예를 들어 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 옵션을 사용하면 워커가 지정한 초(second) 동안만 Job을 처리하고 종료합니다. Supervisor와 함께 사용하면 일정 시간마다 워커를 재시작해 메모리 누수를 방지할 수 있습니다.
<h1 id="installing-supervisor">1시간 동안 Job을 처리하고 종료</h1>
php artisan queue:work --max-time=3600워커 슬립 시간 설정하기
큐에 처리할 Job이 있는 동안 워커는 쉬지 않고 계속 Job을 처리합니다. 처리할 Job이 없을 때 워커가 얼마나 대기할지는 --sleep 옵션으로 초(second) 단위로 지정할 수 있습니다. 슬립 중에는 새로운 Job을 처리하지 않습니다.
php artisan queue:work --sleep=3유지보수 모드와 큐
애플리케이션이 유지보수 모드 상태일 때는 큐에 쌓인 Job이 처리되지 않습니다. 유지보수 모드가 해제되면 정상적으로 다시 처리됩니다.
유지보수 모드 중에도 강제로 Job을 처리하려면 --force 옵션을 사용하세요.
php artisan queue:work --force리소스 관리 주의사항
데몬 큐 워커는 각 Job을 처리하기 전에 프레임워크를 재부팅하지 않습니다. 따라서 무거운 리소스는 각 Job이 완료된 후 반드시 직접 해제해야 합니다. 예를 들어 GD 라이브러리로 이미지를 처리하는 경우, 작업이 끝나면 imagedestroy를 호출해 메모리를 반환해야 합니다.
큐 우선순위
큐 처리 순서에 우선순위를 지정해야 하는 경우가 있습니다. 예를 들어 config/queue.php에서 redis 커넥션의 기본 큐를 low로 설정해두고, 특정 Job은 high 우선순위 큐에 디스패치할 수 있습니다.
dispatch((new Job)->onQueue('high'));high 큐의 Job을 모두 처리한 뒤에 low 큐를 처리하도록 워커를 시작하려면, 큐 이름을 콤마로 구분하여 --queue 옵션에 전달하세요.
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이 지정한 시간을 초과하면 워커가 오류와 함께 종료됩니다. 일반적으로 서버에 설정된 프로세스 매니저가 워커를 자동으로 재시작합니다.
php artisan queue:work --timeout=60retry_after 설정과 --timeout 옵션은 서로 다른 역할을 하지만, 함께 작동하여 Job이 유실되지 않고 정확히 한 번만 처리되도록 보장합니다.
아래 다이어그램은 두 옵션이 어떻게 상호작용하는지 보여줍니다.
WARNING
--timeout 값은 항상 retry_after 설정값보다 몇 초 이상 짧게 설정해야 합니다. 그래야 응답 없는 Job을 처리하는 워커가 Job이 재시도되기 전에 확실히 종료됩니다. --timeout이 retry_after보다 길면 같은 Job이 두 번 처리될 수 있습니다.
Supervisor 설정
프로덕션 환경에서는 queue:work 프로세스가 항상 실행 상태를 유지하도록 관리해야 합니다. queue:work 프로세스는 워커 타임아웃 초과, queue:restart 명령 실행 등 다양한 이유로 종료될 수 있습니다.
따라서 queue:work 프로세스가 종료되었을 때 이를 감지하고 자동으로 재시작해 주는 프로세스 모니터가 필요합니다. 프로세스 모니터를 사용하면 동시에 실행할 queue:work 프로세스 수도 지정할 수 있습니다. Supervisor는 Linux 환경에서 널리 사용되는 프로세스 모니터로, 아래에서 설정 방법을 안내합니다.
Supervisor 설치
Supervisor는 Linux 운영체제용 프로세스 모니터로, queue:work 프로세스가 예기치 않게 종료되면 자동으로 재시작해 줍니다. Ubuntu에서는 다음 명령으로 설치할 수 있습니다:
sudo apt-get install supervisorNOTE
Supervisor를 직접 설정하고 관리하는 것이 부담스럽다면 Laravel Forge 사용을 고려해 보세요. Forge는 프로덕션 Laravel 프로젝트에 Supervisor를 자동으로 설치하고 설정해 줍니다.
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 sqs --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이 실패하는 경우에는 이 테이블에 저장되지 않고, 예외가 즉시 애플리케이션에 의해 처리됩니다.
새로 생성한 Laravel 애플리케이션에는 failed_jobs 테이블을 만드는 마이그레이션이 이미 포함되어 있는 경우가 많습니다. 만약 해당 마이그레이션이 없다면 make:queue-failed-table 명령으로 생성할 수 있습니다:
php artisan make:queue-failed-tablephp artisan migrate큐 워커를 실행할 때 queue:work 명령의 --tries 옵션으로 최대 시도 횟수를 지정할 수 있습니다. --tries 값을 지정하지 않으면 Job 클래스의 $tries 프로퍼티에 정의된 횟수만큼, 또는 그 값도 없으면 1회만 시도합니다:
php artisan queue:work redis --tries=3--backoff 옵션을 사용하면 예외가 발생한 Job을 재시도하기 전에 Laravel이 대기할 시간(초)을 지정할 수 있습니다. 기본값은 0초로, 실패 즉시 큐에 다시 투입됩니다:
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번째 재시도는 1초, 2번째는 5초, 3번째 이후는 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 재시도
failed_jobs 테이블에 저장된 모든 실패 Job 목록을 확인하려면 queue:failed Artisan 명령을 사용하세요:
php artisan queue:failed이 명령은 Job ID, 연결명, 큐 이름, 실패 시각 등의 정보를 출력합니다. Job ID를 이용해 특정 실패 Job을 재시도할 수 있습니다:
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece여러 Job을 한 번에 재시도할 수도 있습니다:
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 91401d2c-0784-4f43-824c-34f94a33c24d특정 큐에 속한 실패 Job을 모두 재시도하려면:
php artisan queue:retry --queue=name모든 실패 Job을 한꺼번에 재시도하려면 ID 자리에 all을 전달하세요:
php artisan queue:retry all특정 실패 Job을 삭제하려면 queue:forget 명령을 사용하세요:
php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24dNOTE
Horizon을 사용하는 경우에는 queue:forget 대신 horizon:forget 명령으로 실패 Job을 삭제해야 합니다.
failed_jobs 테이블의 모든 레코드를 비우려면 queue:flush 명령을 사용하세요:
php artisan queue:flush존재하지 않는 모델 무시하기
Job에 Eloquent 모델을 주입하면, 큐에 저장될 때 자동으로 직렬화되고 처리 시 데이터베이스에서 다시 조회됩니다. 그런데 Job이 대기 중인 사이에 해당 모델이 삭제된 경우, ModelNotFoundException이 발생하며 Job이 실패할 수 있습니다.
이러한 상황을 자동으로 처리하려면 Job 클래스의 deleteWhenMissingModels 프로퍼티를 true로 설정하세요. 이 설정을 사용하면 모델이 없을 때 예외를 발생시키지 않고 Job을 조용히 삭제합니다:
/**
* 모델이 더 이상 존재하지 않으면 Job을 삭제합니다.
*
* @var bool
*/
public $deleteWhenMissingModels = true;실패 Job 레코드 정리(Pruning)
queue:prune-failed Artisan 명령으로 failed_jobs 테이블의 오래된 레코드를 정리할 수 있습니다:
php artisan queue:prune-failed기본적으로 24시간이 지난 레코드가 삭제됩니다. --hours 옵션을 사용하면 보존 기간을 직접 지정할 수 있습니다. 예를 들어, 48시간이 지난 레코드만 삭제하려면:
php artisan queue:prune-failed --hours=48DynamoDB에 실패 Job 저장하기
Laravel은 관계형 데이터베이스 대신 DynamoDB에 실패 Job 레코드를 저장하는 방식도 지원합니다. 단, DynamoDB 테이블은 직접 생성해야 합니다. 테이블 이름은 일반적으로 failed_jobs로 하되, 애플리케이션의 queue 설정 파일에서 queue.failed.table 값을 참고해 맞춰주세요.
테이블에는 다음 두 가지 키가 필요합니다:
- 파티션 키(Partition Key): 문자열 타입, 이름은
application - 정렬 키(Sort Key): 문자열 타입, 이름은
uuid
application 값에는 app 설정 파일의 name 값이 사용됩니다. 이 덕분에 하나의 DynamoDB 테이블로 여러 Laravel 애플리케이션의 실패 Job을 함께 관리할 수 있습니다.
먼저 AWS SDK를 설치하세요:
composer require aws/aws-sdk-php그런 다음 queue.failed.driver 설정을 dynamodb로 변경하고, AWS 인증에 필요한 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로 지정하면 실패한 Job을 저장하지 않고 바로 폐기합니다. 보통은 QUEUE_FAILED_DRIVER 환경 변수로 설정하는 것이 편리합니다:
QUEUE_FAILED_DRIVER=null실패 Job 이벤트
Job이 실패할 때 호출되는 이벤트 리스너를 등록하려면 Queue 파사드의 failing 메서드를 사용하세요. 예를 들어 AppServiceProvider의 boot 메서드에서 다음과 같이 클로저를 등록할 수 있습니다:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;
use Illuminate\Queue\Events\JobFailed;
class AppServiceProvider extends ServiceProvider
{
/**
* 애플리케이션 서비스 등록
*/
public function register(): void
{
// ...
}
/**
* 애플리케이션 서비스 부트스트랩
*/
public function boot(): void
{
Queue::failing(function (JobFailed $event) {
// $event->connectionName — 연결명
// $event->job — 실패한 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
큐 삭제 기능은 SQS, Redis, database 드라이버에서만 지원됩니다. 또한 SQS는 메시지 삭제 처리에 최대 60초가 걸릴 수 있으므로, 큐를 비운 직후 60초 이내에 전송된 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이 실제로 Push되는 것을 막을 수 있습니다. fake를 호출한 뒤에는 애플리케이션이 큐에 Job을 Push하려 했는지 여러 어서션 메서드로 확인할 수 있습니다.
Pest
<?php
use App\Jobs\AnotherJob;
use App\Jobs\FinalJob;
use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Queue;
test('주문을 배송할 수 있다', function () {
Queue::fake();
// 주문 배송 처리...
// 아무 Job도 Push되지 않았음을 검증...
Queue::assertNothingPushed();
// 특정 큐에 Job이 Push되었음을 검증...
Queue::assertPushedOn('queue-name', ShipOrder::class);
// Job이 2번 Push되었음을 검증...
Queue::assertPushed(ShipOrder::class, 2);
// Job이 Push되지 않았음을 검증...
Queue::assertNotPushed(AnotherJob::class);
// 클로저가 큐에 Push되었음을 검증...
Queue::assertClosurePushed();
// Push된 Job의 총 개수를 검증...
Queue::assertCount(3);
});PHPUnit
<?php
namespace Tests\Feature;
use App\Jobs\AnotherJob;
use App\Jobs\FinalJob;
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도 Push되지 않았음을 검증...
Queue::assertNothingPushed();
// 특정 큐에 Job이 Push되었음을 검증...
Queue::assertPushedOn('queue-name', ShipOrder::class);
// Job이 2번 Push되었음을 검증...
Queue::assertPushed(ShipOrder::class, 2);
// Job이 Push되지 않았음을 검증...
Queue::assertNotPushed(AnotherJob::class);
// 클로저가 큐에 Push되었음을 검증...
Queue::assertClosurePushed();
// Push된 Job의 총 개수를 검증...
Queue::assertCount(3);
}
}assertPushed 또는 assertNotPushed 메서드에 클로저를 전달하면, 특정 조건을 만족하는 Job이 Push되었는지 세밀하게 검증할 수 있습니다. 조건을 만족하는 Job이 하나라도 존재하면 어서션은 성공합니다.
Queue::assertPushed(function (ShipOrder $job) use ($order) {
return $job->order->id === $order->id;
});특정 Job만 Fake 처리하기
일부 Job만 Fake 처리하고 나머지 Job은 실제로 실행되도록 하려면, fake 메서드에 Fake할 Job의 클래스명을 배열로 전달합니다.
Pest
test('주문을 배송할 수 있다', function () {
Queue::fake([
ShipOrder::class,
]);
// 주문 배송 처리...
// Job이 2번 Push되었음을 검증...
Queue::assertPushed(ShipOrder::class, 2);
});PHPUnit
public function test_orders_can_be_shipped(): void
{
Queue::fake([
ShipOrder::class,
]);
// 주문 배송 처리...
// Job이 2번 Push되었음을 검증...
Queue::assertPushed(ShipOrder::class, 2);
}반대로, 특정 Job을 제외한 나머지를 모두 Fake 처리하려면 except 메서드를 사용합니다.
Queue::fake()->except([
ShipOrder::class,
]);Job 체인 테스트하기
Job 체인을 테스트할 때는 Bus 파사드의 Fake 기능을 활용해야 합니다. 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 인스턴스 배열을 전달할 수도 있습니다. 인스턴스를 전달하면 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 체인이 배치를 포함하는 경우, 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;
});특정 개수의 배치가 디스패치되었는지 검증하려면 assertBatchCount 메서드를 사용합니다.
Bus::assertBatchCount(3);배치가 전혀 디스패치되지 않았음을 검증하려면 assertNothingBatched를 사용합니다.
Bus::assertNothingBatched();Job과 배치 간의 상호작용 테스트하기
개별 Job이 자신이 속한 배치와 어떻게 상호작용하는지 테스트해야 할 때도 있습니다. 예를 들어, Job이 배치의 추가 처리를 취소했는지 확인하는 경우가 이에 해당합니다. 이때는 withFakeBatch 메서드를 통해 Job에 Fake 배치를 할당합니다. withFakeBatch는 Job 인스턴스와 Fake 배치를 튜플로 반환합니다.
[$job, $batch] = (new ShipOrder)->withFakeBatch();
$job->handle();
$this->assertTrue($batch->cancelled());
$this->assertEmpty($batch->added);Job과 큐 간의 상호작용 테스트하기
때로는 큐에 올라간 Job이 스스로를 다시 큐에 반환하거나 삭제했는지 테스트해야 할 수 있습니다. 이런 큐 상호작용은 Job 인스턴스를 생성한 후 withFakeQueueInteractions 메서드를 호출하여 테스트할 수 있습니다.
큐 상호작용을 Fake 처리한 뒤 handle 메서드를 실행하면, assertReleased, assertDeleted, assertNotDeleted, assertFailed, assertFailedWith, assertNotFailed 메서드로 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
{
/**
* 애플리케이션 서비스를 등록합니다.
*/
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 처리 여부와 관계없이 워커 루프가 반복될 때마다 호출됩니다. 따라서 무거운 작업을 등록하면 워커 성능에 영향을 줄 수 있으니 주의하세요.