큐
업데이트됨번역일: 2026년 9월 18일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 9월 18일
- 번역 갱신
- 2026년 9월 18일
큐
소개
웹 애플리케이션을 만들다 보면 업로드된 CSV 파일을 파싱해서 저장하거나, 대용량 이미지를 리사이징하는 것처럼 일반적인 웹 요청 처리 시간 안에 끝내기에는 너무 오래 걸리는 작업들을 만나게 됩니다. 다행히 라라벨은 이런 작업들을 백그라운드에서 처리할 수 있는 큐 Job으로 손쉽게 만들 수 있는 방법을 제공합니다. 시간이 오래 걸리는 작업들을 큐로 옮기면 애플리케이션은 웹 요청에 훨씬 빠르게 응답할 수 있고, 결과적으로 사용자에게 더 나은 경험을 제공할 수 있습니다.
예를 들어 회원가입 시 인증 이메일을 발송하는 기능을 생각해봅시다. 메일 발송은 몇 초씩 걸릴 수 있는데, 사용자가 그 시간만큼 기다렸다가 다음 화면으로 넘어가게 하고 싶지는 않을 것입니다. 이때 이메일 발송 작업을 큐 Job으로 만들어두면, 애플리케이션은 Job을 큐에 넣어두기만 하고 즉시 사용자에게 응답을 돌려줄 수 있습니다.
큐를 사용하면 이렇게 무거운 작업을 별도의 프로세스(큐 워커)가 백그라운드에서 하나씩 처리하도록 위임할 수 있습니다. 라라벨을 처음 설치하면 로컬 개발 환경에서는 database 큐 드라이버가 이미 config/queue.php 설정 파일에 구성되어 있습니다. 이 드라이버는 큐에 등록된 Job들을 애플리케이션 데이터베이스에 저장하는 방식입니다.
하지만 실서비스 환경에서는 Amazon SQS, Redis, 혹은 MongoDB와 같이 더 강력한 드라이버를 사용하는 것이 일반적입니다. 자세한 각 드라이버 설정 방법은 아래 드라이버 사전 준비사항 섹션을 참고하세요.
NOTE
큐 관련 설정은 config/queue.php 파일에 저장되어 있습니다. 이 파일에는 라라벨에 포함된 각 큐 드라이버(데이터베이스, Amazon SQS, Redis, Beanstalkd 등)에 대한 연결 설정이 담겨 있으며, 즉시 Job을 실행해버리는(로컬 개발용) sync 드라이버도 포함되어 있습니다.
NOTE
라라벨은 Redis 기반 큐를 위한 아름다운 대시보드이자 설정 시스템인 Horizon도 제공합니다. 자세한 내용은 Horizon 공식 문서를 참고하세요.
연결(Connections) vs. 큐(Queues)
라라벨 큐 기능을 다루기 시작하면 "연결(connection)"과 "큐(queue)"라는 용어를 혼동하기 쉬우니 미리 짚고 넘어가겠습니다. config/queue.php 설정 파일에는 connections라는 배열 설정 항목이 있습니다. 이 설정은 Amazon SQS, Beanstalkd, Redis 같은 백엔드 큐 서비스에 대한 연결 정보를 정의합니다. 하지만 하나의 큐 연결(connection)은 여러 개의 "큐(queue)"를 가질 수 있는데, 이 "큐"는 대기 중인 Job들을 담아두는 서로 다른 더미(stack) 혹은 쌓아놓은 목록이라고 생각하면 됩니다.
queue.php 설정 파일의 각 연결 설정에는 queue라는 속성이 포함되어 있음을 알 수 있습니다. 이 속성은 해당 연결로 Job이 전송될 때 기본적으로 사용될 큐 이름입니다. 즉, Job을 디스패치할 때 어떤 큐로 보낼지 명시적으로 지정하지 않으면, 해당 연결 설정의 queue 속성에 정의된 기본 큐에 Job이 들어가게 됩니다.
use App\Jobs\ProcessPodcast;
// 이 Job은 기본 연결의 기본 큐로 전송됩니다...
ProcessPodcast::dispatch();
// 이 Job은 기본 연결의 "emails" 큐로 전송됩니다...
ProcessPodcast::dispatch()->onQueue('emails');일부 애플리케이션은 굳이 여러 개의 큐로 Job을 나눌 필요 없이, 하나의 단순한 큐만 사용해도 충분합니다. 하지만 Job을 여러 큐로 나눠서 관리하면, 예를 들어 우선순위를 정하거나 특정 종류의 작업을 분리해서 처리하는 데 매우 유용합니다. 라라벨 큐 워커는 어떤 우선순위로 어떤 큐를 처리할지 지정할 수 있기 때문입니다. 예를 들어 high 큐로 Job을 보낸 뒤, 다음과 같이 해당 큐에 더 높은 처리 우선순위를 부여하는 워커를 실행할 수 있습니다.
php artisan queue:work --queue=high,default드라이버 사전 준비사항
데이터베이스
database 큐 드라이버를 사용하려면 Job을 저장할 데이터베이스 테이블이 필요합니다. 보통은 라라벨의 기본 마이그레이션 파일인 0001_01_01_000002_create_jobs_table.php에 이미 포함되어 있습니다. 만약 애플리케이션에 이 마이그레이션이 없다면, 다음 Artisan 명령어로 생성할 수 있습니다.
php artisan make:queue-tablephp artisan migrateRedis
redis 큐 드라이버를 사용하려면 config/database.php 설정 파일에서 Redis 데이터베이스 연결을 먼저 구성해야 합니다.
WARNING
redis 큐 드라이버는 serializer와 compression Redis 옵션을 지원하지 않습니다.
Redis 클러스터
Redis 큐 연결이 Redis 클러스터를 사용하는 경우, 큐 이름에 키 해시 태그가 포함되어야 합니다. 이는 특정 큐에 속한 모든 Redis 키가 같은 해시 슬롯에 배치되도록 하기 위해서 필요합니다.
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => '{default}',
'retry_after' => 90,
],블로킹(Blocking)
Redis 큐를 사용할 때 block_for 설정 옵션을 사용하면, 워커가 Redis 드라이버에 작업이 들어올 때까지 얼마나 오래 대기(블로킹)할지 지정할 수 있습니다. 이 값을 상황에 맞게 조절하면, 계속해서 Redis 데이터베이스를 폴링(polling)하며 새 작업을 확인하는 것보다 훨씬 효율적입니다.
예를 들어 값을 5로 설정하면, 작업이 들어올 때까지 최대 5초간 블로킹하여 대기합니다.
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'retry_after' => 90,
'block_for' => 5,
],WARNING
block_for를 0으로 설정하면 큐 워커가 작업이 들어올 때까지 무한정 블로킹됩니다. 이 경우 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 ~5.0
큐
소개
웹 애플리케이션을 개발하다 보면 업로드된 CSV 파일을 파싱하고 저장하는 작업처럼, 일반적인 웹 요청 안에서 처리하기에는 시간이 너무 오래 걸리는 작업을 마주할 때가 있습니다. 다행히 Laravel에서는 이런 작업을 큐(Job)로 등록해서 백그라운드에서 처리할 수 있습니다. 시간이 오래 걸리는 작업을 큐로 넘기면 애플리케이션이 웹 요청에 훨씬 빠르게 응답할 수 있고, 결과적으로 사용자 경험도 크게 개선됩니다.
Laravel 큐는 Amazon SQS, Redis, 심지어 관계형 데이터베이스까지 다양한 큐 백엔드를 하나의 통일된 API로 다룰 수 있게 해줍니다.
큐 관련 설정은 애플리케이션의 config/queue.php 설정 파일에 들어 있습니다. 이 파일에는 프레임워크가 기본으로 제공하는 각 큐 드라이버(database, Amazon SQS, Redis, Beanstalkd)에 대한 커넥션 설정이 들어 있으며, Job을 즉시 실행해버리는 동기(sync) 드라이버(개발이나 테스트 환경에서 유용합니다)도 포함되어 있습니다. 또한 큐에 등록된 Job을 그냥 버려버리는 null 드라이버도 함께 제공됩니다.
NOTE
Laravel Horizon은 Redis 기반 큐를 위한 아름다운 대시보드이자 설정 관리 도구입니다. 자세한 내용은 Horizon 공식 문서를 참고하세요.
커넥션(Connection)과 큐(Queue)의 차이
Laravel 큐를 본격적으로 사용하기 전에 "커넥션(connection)"과 "큐(queue)"의 개념 차이를 짚고 넘어가는 것이 중요합니다. config/queue.php 설정 파일을 보면 connections라는 설정 배열이 있습니다. 여기에는 Amazon SQS, Beanstalk, Redis 같은 백엔드 큐 서비스에 대한 접속 정보가 정의되어 있습니다. 그런데 하나의 큐 커넥션 안에도 여러 개의 "큐"가 존재할 수 있는데, 이는 큐에 쌓인 Job들을 담아두는 서로 다른 대기열(스택)이라고 생각하면 됩니다.
queue 설정 파일의 각 커넥션 설정 예시에는 queue라는 속성이 포함되어 있습니다. 이는 해당 커넥션으로 Job을 dispatch할 때 기본으로 사용되는 큐를 의미합니다. 즉, Job을 dispatch할 때 어떤 큐로 보낼지 명시적으로 지정하지 않으면, 커넥션 설정의 queue 속성에 정의된 큐로 Job이 들어가게 됩니다:
use App\Jobs\ProcessPodcast;
// 이 Job은 기본 커넥션의 기본 큐로 전송됩니다...
ProcessPodcast::dispatch();
// 이 Job은 기본 커넥션의 "emails" 큐로 전송됩니다...
ProcessPodcast::dispatch()->onQueue('emails');애플리케이션에 따라서는 여러 큐를 사용할 필요 없이 단순히 하나의 큐만으로 충분한 경우도 있습니다. 하지만 Job 처리에 우선순위를 두거나 성격에 따라 나누고 싶다면 여러 큐를 활용하는 방식이 매우 유용합니다. Laravel 큐 워커는 어떤 큐를 어떤 우선순위로 처리할지 지정할 수 있기 때문입니다. 예를 들어 high 큐에 Job을 넣는다면, 다음과 같이 해당 큐를 더 높은 우선순위로 처리하는 워커를 실행할 수 있습니다:
php artisan queue:work --queue=high,default드라이버별 참고 사항 및 사전 준비
데이터베이스
database 큐 드라이버를 사용하려면 Job을 저장할 데이터베이스 테이블이 필요합니다. 보통 Laravel의 기본 0001_01_01_000002_create_jobs_table.php 마이그레이션에 포함되어 있지만, 만약 애플리케이션에 이 마이그레이션이 없다면 make:queue-table Artisan 명령어로 새로 생성할 수 있습니다:
php artisan make:queue-tablephp artisan migrateRedis
redis 큐 드라이버를 사용하려면 config/database.php 설정 파일에 Redis 데이터베이스 커넥션을 설정해야 합니다.
WARNING
redis 큐 드라이버에서는 Redis의 serializer, compression 옵션을 지원하지 않습니다.
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 설정 옵션으로, 워커가 다음 폴링(polling) 루프로 넘어가서 Redis 데이터베이스를 다시 조회하기 전까지 새로운 Job이 들어오길 얼마나 기다릴지 지정할 수 있습니다.
이 값을 큐의 부하 상황에 맞게 조정하면, Redis 데이터베이스를 계속해서 폴링하는 것보다 훨씬 효율적일 수 있습니다. 예를 들어 값을 5로 설정하면, 드라이버는 Job이 들어올 때까지 최대 5초 동안 대기하게 됩니다:
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => 5,
'after_commit' => false,
],WARNING
block_for를 0으로 설정하면 큐 워커는 Job이 들어올 때까지 무한정 대기하게 됩니다. 이 경우 SIGTERM 같은 시그널도 다음 Job이 처리될 때까지는 처리되지 않으니 주의해야 합니다.
SQS 오버플로우 스토리지
Amazon SQS는 큐에 담을 수 있는 메시지 페이로드의 최대 크기를 제한합니다. 이 제한을 초과할 수도 있는 페이로드를 가진 Job을 dispatch해야 한다면, Laravel에서 크기가 큰 SQS 페이로드를 캐시 스토어에 저장하고 SQS에는 그 위치를 가리키는 포인터만 전송하도록 설정할 수 있습니다. 이 기능을 사용하려면 SQS 큐 커넥션 설정에 overflow 배열을 추가하면 됩니다:
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
'overflow' => [
'enabled' => env('SQS_OVERFLOW_ENABLED', false),
'store' => env('SQS_OVERFLOW_STORE'),
'always' => false,
'delete_after_processing' => true,
'flush_on_clear' => env('SQS_OVERFLOW_FLUSH_ON_CLEAR', false),
],
],오버플로우 스토리지 기능을 활성화하면, Laravel은 크기가 1MB 이상인 페이로드를 설정된 캐시 스토어에 저장합니다. always 옵션을 true로 설정하면 크기와 상관없이 모든 SQS 페이로드가 캐시 스토어에 저장됩니다. 큐에 등록된 Job이 처리될 때 캐시 스토어에서 페이로드를 다시 가져와야 하므로, 워커가 Job을 처리할 때까지 데이터를 안전하게 보관할 수 있는 스토어를 선택해야 합니다. 기본적으로 저장된 페이로드는 해당 Job이 성공적으로 처리되어 SQS에서 삭제된 후 함께 삭제됩니다.
flush_on_clear 옵션을 true로 설정하면 queue:clear 명령어로 SQS 큐를 비울 때 설정된 오버플로우 캐시 스토어도 함께 비워집니다. 캐시 스토어를 플러시(flush)하면 해당 스토어의 모든 항목이 삭제될 수 있으므로, 이 옵션을 사용할 경우에는 SQS 오버플로우 전용 캐시 스토어를 별도로 구성하는 것을 권장합니다.
기타 드라이버별 사전 준비 사항
아래 큐 드라이버들을 사용하려면 다음 의존성 패키지가 필요합니다. 이 패키지들은 Composer로 설치할 수 있습니다:
- Amazon SQS:
aws/aws-sdk-php ~3.0 - Beanstalkd:
pda/pheanstalk ~5.0 - Redis:
predis/predis ~3.0또는 phpredis PHP 확장 - MongoDB:
mongodb/laravel-mongodb
Job(작업) 생성하기
Job 클래스 생성하기
애플리케이션에서 큐에 등록할 Job은 기본적으로 app/Jobs 디렉터리에 저장됩니다. 만약 app/Jobs 디렉터리가 아직 없다면, make:job Artisan 명령어를 실행할 때 자동으로 생성됩니다.
php artisan make:job ProcessPodcast이 명령어로 생성된 클래스는 Illuminate\Contracts\Queue\ShouldQueue 인터페이스를 구현합니다. 이 인터페이스는 Laravel에게 "이 Job은 큐에 등록되어 비동기로 실행되어야 한다"는 것을 알려주는 역할을 합니다.
NOTE
Job 스텁(stub)은 스텁 커스터마이징을 통해 원하는 형태로 변경할 수 있습니다.
클래스 구조
Job 클래스의 구조는 매우 단순합니다. 대부분 큐 워커가 Job을 처리할 때 호출되는 handle 메서드 하나만 가지고 있습니다. 팟캐스트 발행 서비스를 운영하면서, 업로드된 팟캐스트 파일을 발행 전에 가공해야 하는 상황을 예로 들어보겠습니다.
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* Job을 실행합니다.
*/
public function handle(AudioProcessor $processor): void
{
// 업로드된 팟캐스트 파일 처리...
}
}위 예제에서 눈여겨볼 점은 Eloquent 모델을 큐에 등록되는 Job의 생성자에 그대로 전달했다는 것입니다. Job이 사용하는 Queueable 트레이트 덕분에, Eloquent 모델과 그 안에 로드된 연관관계(relationship)까지도 Job이 처리될 때 자연스럽게 직렬화(serialize)/역직렬화(unserialize)됩니다.
큐에 등록되는 Job이 생성자에서 Eloquent 모델을 받을 경우, 실제로 큐에 직렬화되어 저장되는 것은 모델의 식별자(ID)뿐입니다. Job이 실제로 처리될 때 큐 시스템이 데이터베이스에서 전체 모델 인스턴스와 그 연관관계를 자동으로 다시 조회합니다. 이런 방식 덕분에 큐 드라이버로 전송되는 Job의 페이로드 크기를 훨씬 작게 유지할 수 있습니다.
NOTE
즉, "무거운 객체 자체"가 아니라 "그 객체를 다시 찾아올 수 있는 정보"만 큐에 저장된다고 이해하면 쉽습니다. 큐 워커가 실제로 Job을 실행하는 시점에 최신 데이터를 데이터베이스에서 다시 읽어오므로, 큐에 쌓여 있는 동안 데이터가 변경되었더라도 최신 상태를 기준으로 처리됩니다.
`handle` 메서드의 의존성 주입
handle 메서드는 큐 워커가 Job을 처리할 때 호출됩니다. 이 메서드의 인자에 타입힌트를 지정하면 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의 연관관계 처리
Job이 큐에 등록될 때 로드되어 있던 Eloquent 모델의 연관관계까지 모두 함께 직렬화되기 때문에, 직렬화된 Job 문자열의 크기가 예상보다 커질 수 있습니다. 게다가 Job이 역직렬화되어 데이터베이스에서 연관관계를 다시 조회할 때는 전체 데이터를 다시 가져오게 됩니다. 즉, Job이 큐에 등록되기 전 연관관계에 걸어두었던 제약 조건(where 조건 등)은 역직렬화 시점에는 적용되지 않습니다. 따라서 특정 연관관계의 일부 데이터만 다루고 싶다면, 큐에 등록되는 Job 내부에서 해당 연관관계를 다시 제약해야 합니다.
반대로 연관관계 자체가 직렬화되는 것을 막고 싶다면, 프로퍼티 값을 설정할 때 모델에 withoutRelations 메서드를 호출하면 됩니다. 이 메서드는 로드된 연관관계가 제거된 모델 인스턴스를 반환합니다.
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct(
Podcast $podcast,
) {
$this->podcast = $podcast->withoutRelations();
}특정 연관관계만 제거하고 나머지는 유지하고 싶다면 withoutRelation 메서드를 사용할 수 있습니다.
$this->podcast = $podcast->withoutRelation('comments');PHP 생성자 프로퍼티 승격(constructor property promotion) 문법을 사용하면서 특정 Eloquent 모델의 연관관계를 직렬화하지 않도록 지정하고 싶다면, WithoutRelations 속성(attribute)을 사용할 수 있습니다.
use Illuminate\Queue\Attributes\WithoutRelations;
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct(
#[WithoutRelations]
public Podcast $podcast,
) {}모든 모델의 연관관계를 한꺼번에 제외하고 싶다면, 각 모델마다 속성을 붙이는 대신 클래스 전체에 WithoutRelations 속성을 적용할 수도 있습니다.
<?php
namespace App\Jobs;
use App\Models\DistributionPlatform;
use App\Models\Podcast;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\WithoutRelations;
#[WithoutRelations]
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct(
public Podcast $podcast,
public DistributionPlatform $platform,
) {}
}Job이 단일 모델이 아니라 Eloquent 모델의 컬렉션이나 배열을 전달받는 경우, 해당 컬렉션 안의 모델들은 Job이 역직렬화되어 실행될 때 연관관계가 복원되지 않습니다. 이는 다수의 모델을 다루는 Job에서 리소스가 과도하게 소모되는 것을 방지하기 위한 조치입니다.
유일한(Unique) Job
WARNING
유일한 Job 기능을 사용하려면 락(lock)을 지원하는 캐시 드라이버가 필요합니다. 현재 memcached, redis, dynamodb, database, file, array 캐시 드라이버가 원자적 락(atomic lock)을 지원합니다.
WARNING
유일한 Job에 대한 제약 조건은 배치(batch) 내부의 Job에는 적용되지 않습니다.
특정 Job이 동시에 큐에 단 하나의 인스턴스만 존재하도록 보장하고 싶을 때가 있습니다. 이럴 때는 Job 클래스에 ShouldBeUnique 인터페이스를 구현하면 됩니다. 이 인터페이스는 별도로 구현해야 할 메서드가 없습니다.
<?php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
// ...
}위 예제에서 UpdateSearchIndex Job은 유일한 Job으로 취급됩니다. 즉, 이미 큐에 같은 Job의 인스턴스가 존재하고 아직 처리가 끝나지 않았다면, 새로 디스패치된 Job은 큐에 등록되지 않습니다.
경우에 따라 Job을 유일하게 만드는 기준이 될 특정 "키"를 직접 지정하고 싶거나, 일정 시간이 지나면 유일성 제약이 풀리도록 타임아웃을 지정하고 싶을 수 있습니다. 이럴 때는 UniqueFor 속성을 사용하면서 Job 클래스에 uniqueId 메서드를 정의하면 됩니다.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Queue\Attributes\UniqueFor;
#[UniqueFor(3600)]
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
/**
* 상품(product) 인스턴스.
*
* @var \App\Models\Product
*/
public $product;
/**
* Job의 유일성을 판별할 ID를 반환합니다.
*/
public function uniqueId(): string
{
return $this->product->id;
}
}위 예제에서 UpdateSearchIndex Job은 상품 ID를 기준으로 유일성이 결정됩니다. 즉, 동일한 상품 ID로 새롭게 디스패치되는 Job은 기존 Job의 처리가 끝날 때까지 무시됩니다. 또한 기존 Job이 1시간 이내에 처리되지 않으면 유일성 락이 자동으로 해제되어, 같은 유일 키를 가진 새 Job이 다시 큐에 등록될 수 있습니다.
WARNING
애플리케이션이 여러 웹 서버나 컨테이너에서 Job을 디스패치한다면, 모든 서버가 동일한 중앙 캐시 서버를 공유하도록 구성해야 합니다. 그래야 Laravel이 Job의 유일성 여부를 정확하게 판단할 수 있습니다.
처리가 시작되기 전까지만 유일성 유지하기
기본적으로 유일한 Job의 락은 Job 처리가 완료되거나 모든 재시도가 실패한 이후에 해제됩니다. 하지만 Job이 실제로 "처리를 시작하기 직전"에 곧바로 락을 해제하고 싶은 경우도 있습니다. 이럴 때는 ShouldBeUnique 대신 ShouldBeUniqueUntilProcessing 인터페이스를 구현하면 됩니다.
<?php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
// ...
}유일한 Job의 락 동작 방식
내부적으로 ShouldBeUnique Job이 디스패치되면, Laravel은 uniqueId 값을 키로 사용해 락을 획득하려고 시도합니다. 이미 락이 걸려 있다면 해당 Job은 디스패치되지 않습니다. 이 락은 Job 처리가 완료되거나 모든 재시도가 실패했을 때 해제됩니다. 기본적으로 Laravel은 이 락을 획득할 때 기본 캐시 드라이버를 사용하지만, 다른 드라이버를 사용하고 싶다면 uniqueVia 메서드를 정의해 사용할 캐시 드라이버를 지정할 수 있습니다.
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
// ...
/**
* 유일한 Job의 락에 사용할 캐시 드라이버를 반환합니다.
*/
public function uniqueVia(): Repository
{
return Cache::driver('redis');
}
}NOTE
단순히 Job의 동시 처리 개수만 제한하고 싶다면, 유일성 제약 대신 WithoutOverlapping Job 미들웨어를 사용하는 것이 더 적합합니다.
디바운스(Debounce) Job
짧은 시간 안에 같은 Job이 여러 번 디스패치되었을 때, 가장 마지막에 디스패치된 것만 실제로 실행되도록 하고 싶은 경우가 있습니다. 이럴 때는 Job 클래스에 DebounceFor 속성을 추가하면 됩니다.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\DebounceFor;
#[DebounceFor(30)]
class UpdateSearchIndex implements ShouldQueue
{
use Queueable;
/**
* 새 Job 인스턴스를 생성합니다.
*/
public function __construct(public int $productId)
{
}
/**
* Job의 디바운스 ID를 반환합니다.
*/
public function debounceId(): string
{
return (string) $this->productId;
}
}위 예제에서는 동일한 상품에 대한 UpdateSearchIndex Job이 30초 이내에 반복적으로 디스패치되면, 이 Job들이 디바운스 처리되어 가장 마지막에 디스패치된 것만 실행됩니다.
자주 재디스패치되는 Job이 무한정 뒤로 밀리지 않도록, 최대 대기 시간을 지정하고 싶다면 DebounceFor 속성에 maxWait 인자를 함께 지정할 수 있습니다.
#[DebounceFor(30, maxWait: 120)]
class UpdateSearchIndex implements ShouldQueue
{
use Queueable;
// ...
}디바운스 상태를 추적할 때 사용할 캐시 저장소를 변경하고 싶다면, Job 클래스에 debounceVia 메서드를 정의하면 됩니다.
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
public function debounceVia(): Repository
{
return Cache::driver('redis');
}디바운스된 Job이 더 최신 디스패치에 의해 대체되면, Laravel은 Illuminate\Queue\Events\JobDebounced 이벤트를 발생시키고 대체된 Job을 큐에서 제거합니다.
WARNING
디바운스 Job과 유일한(Unique) Job은 함께 사용할 수 없습니다. DebounceFor 속성을 사용하는 Job은 ShouldBeUnique를 구현해서는 안 됩니다.
WARNING
애플리케이션이 여러 웹 서버나 컨테이너에서 디바운스 Job을 디스패치한다면, 모든 서버가 동일한 중앙 캐시 서버를 공유하도록 구성해야 합니다.
암호화된 Job
Laravel은 암호화 기능을 통해 Job 데이터의 기밀성과 무결성을 보장할 수 있습니다. 사용 방법은 간단합니다. Job 클래스에 ShouldBeEncrypted 인터페이스를 추가하기만 하면, Laravel이 해당 Job을 큐에 등록하기 전에 자동으로 암호화합니다.
<?php
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
{
// ...
}Job 미들웨어
Job 미들웨어를 사용하면 큐에 등록된 Job이 실행되는 과정에 커스텀 로직을 씌울 수 있어, Job 클래스 내부의 중복 코드를 줄일 수 있습니다. 예를 들어, 다음은 Laravel의 Redis 속도 제한(rate limiting) 기능을 활용해 5초에 한 번만 Job을 처리하도록 만든 handle 메서드입니다.
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에도 동일한 속도 제한을 적용하려면 이 로직을 그대로 복사해야 합니다. 이럴 때는 handle 메서드에 직접 속도 제한 로직을 넣는 대신, 속도 제한을 처리하는 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과, Job 처리를 계속 진행하기 위해 호출해야 하는 콜백을 함께 전달받습니다.
make:job-middleware Artisan 명령어로 새로운 Job 미들웨어 클래스를 생성할 수 있습니다. Job 미들웨어를 작성한 후에는 Job의 middleware 메서드에서 이를 반환하면 해당 Job에 적용됩니다. 이 메서드는 make:job Artisan 명령어로 생성한 기본 Job 클래스에는 포함되어 있지 않으므로 직접 추가해야 합니다.
use App\Jobs\Middleware\RateLimited;
/**
* Job이 거쳐야 할 미들웨어 목록을 반환합니다.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited];
}NOTE
Job 미들웨어는 큐에 등록되는 이벤트 리스너, 메일러블, 알림에도 동일하게 적용할 수 있습니다.
속도 제한 (Rate Limiting)
앞서 속도 제한 Job 미들웨어를 직접 작성하는 방법을 살펴봤지만, 사실 Laravel에는 Job에 바로 적용할 수 있는 속도 제한 미들웨어가 내장되어 있습니다. 라우트 속도 제한기와 마찬가지로, Job 속도 제한기 역시 RateLimiter 파사드의 for 메서드로 정의합니다.
예를 들어, 일반 사용자는 데이터 백업을 시간당 한 번만 실행할 수 있게 하고, 프리미엄 고객은 이 제한을 두지 않는다고 가정해보겠습니다. 이를 위해 AppServiceProvider의 boot 메서드에서 RateLimiter를 다음과 같이 정의할 수 있습니다.
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
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을 더 이상 재시도하지 않을 시점을 시간 기준으로 정의할 수도 있습니다.
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를 사용 중이라면 기본 속도 제한 미들웨어보다 Redis에 최적화되어 더 효율적으로 동작하는 Illuminate\Queue\Middleware\RateLimitedWithRedis 미들웨어를 사용할 수 있습니다.
use Illuminate\Queue\Middleware\RateLimitedWithRedis;
public function middleware(): array
{
return [new RateLimitedWithRedis('backups')];
}connection 메서드를 사용하면 미들웨어가 사용할 Redis 커넥션을 지정할 수 있습니다.
return [(new RateLimitedWithRedis('backups'))->connection('limiter')];Job 중복 실행 방지
Laravel에는 임의의 키를 기준으로 Job이 중복 실행되는 것을 막아주는 Illuminate\Queue\Middleware\WithoutOverlapping 미들웨어가 포함되어 있습니다. 이는 한 번에 하나의 Job만 특정 리소스를 수정해야 하는 상황에서 유용합니다.
예를 들어, 사용자의 신용 점수를 업데이트하는 큐 Job이 있고, 같은 사용자 ID에 대해서는 이 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의 총 시도 횟수는 여전히 증가합니다. 따라서 Job 클래스의 Tries와 MaxExceptions 속성을 상황에 맞게 조정하는 것이 좋습니다. 예를 들어 Tries를 기본값인 1로 그대로 두면, 중복된 Job은 이후 재시도되지 않고 그대로 종료됩니다.
같은 종류의 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분이 지나면 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 메서드를 사용하면 여러 Job 클래스에 걸쳐 동일한 키를 적용하도록 설정할 수 있습니다.
use Illuminate\Queue\Middleware\WithoutOverlapping;
class ProviderIsDown
{
// ...
public function middleware(): array
{
return [
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
];
}
}
class ProviderIsUp
{
// ...
public function middleware(): array
{
return [
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
];
}
}예외 발생 제한 (Throttling Exceptions)
Laravel에는 예외 발생 빈도를 제한할 수 있는 Illuminate\Queue\Middleware\ThrottlesExceptions 미들웨어가 포함되어 있습니다. Job이 지정된 횟수만큼 예외를 던지면, 이후의 실행 시도는 지정된 시간 간격이 지날 때까지 지연됩니다. 이 미들웨어는 불안정한 외부 서비스와 연동하는 Job에 특히 유용합니다.
예를 들어, 예외를 자주 던지는 외부 API와 연동하는 큐 Job이 있다고 가정해보겠습니다. 예외 발생을 제한하려면 Job의 middleware 메서드에서 ThrottlesExceptions 미들웨어를 반환하면 됩니다. 보통 이 미들웨어는 시간 기반 시도를 구현한 Job과 함께 사용하는 것이 좋습니다.
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);
}첫 번째 생성자 인자는 Job이 제한에 걸리기 전까지 던질 수 있는 예외 횟수이며, 두 번째 인자는 제한에 걸린 이후 다시 시도되기까지 대기할 시간(초 단위)입니다. 위 예시에서는 Job이 연속으로 10번 예외를 던지면 5분을 기다린 뒤 다시 시도하며, 전체적으로는 30분이라는 시간 제한 안에서 이 과정이 반복됩니다.
Job이 예외를 던졌지만 아직 임계치에 도달하지 않았다면, 일반적으로 Job은 즉시 재시도됩니다. 하지만 미들웨어를 Job에 연결할 때 backoff 메서드를 호출하면, 재시도 전에 대기할 시간(분 단위)을 지정할 수 있습니다.
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Job이 거쳐야 할 미들웨어 목록을 반환합니다.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 5 * 60))->backoff(5)];
}backoff 메서드는 발생한 예외를 인자로 받는 클로저도 받을 수 있어, 지연 시간을 동적으로 결정할 수도 있습니다.
use App\Exceptions\RateLimitedException;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
use Throwable;
/**
* Job이 거쳐야 할 미들웨어 목록을 반환합니다.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 5 * 60))->backoff(
fn (Throwable $throwable) => $throwable instanceof RateLimitedException
? $throwable->retryAfterMinutes()
: 5
)];
}이 미들웨어는 내부적으로 Laravel의 캐시 시스템을 이용해 속도 제한을 구현하며, 기본적으로 Job의 클래스 이름을 캐시 "키"로 사용합니다. 미들웨어를 Job에 연결할 때 by 메서드를 호출하면 이 키를 원하는 값으로 재정의할 수 있습니다. 여러 Job이 동일한 외부 서비스와 연동되어 있어, 이들이 하나의 공유된 제한("버킷")을 함께 사용하도록 하고 싶을 때 유용합니다.
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Job이 거쳐야 할 미들웨어 목록을 반환합니다.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 10 * 60))->by('key')];
}기본적으로 이 미들웨어는 모든 예외를 제한 대상으로 취급합니다. Job에 미들웨어를 연결할 때 when 메서드를 호출하면 이 동작을 변경할 수 있습니다. when에 전달한 클로저가 true를 반환할 때만 해당 예외가 제한 대상이 됩니다.
use Illuminate\Http\Client\HttpClientException;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* Job이 거쳐야 할 미들웨어 목록을 반환합니다.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 10 * 60))->when(
fn (Throwable $throwable) => $throwable instanceof HttpClientException
)];
}when 메서드는 Job을 큐로 돌려보내거나 예외를 던지는 방식으로 동작하는 반면, 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)];
}제한에 걸린 예외를 애플리케이션의 예외 핸들러에도 보고하고 싶다면, 미들웨어를 Job에 연결할 때 report 메서드를 호출하면 됩니다. 필요하다면 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를 사용 중이라면, 기본 예외 제한 미들웨어보다 Redis에 최적화되어 더 효율적인 Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis 미들웨어를 사용할 수 있습니다.
use Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis;
public function middleware(): array
{
return [new ThrottlesExceptionsWithRedis(10, 10 * 60)];
}connection 메서드를 사용하면 미들웨어가 사용할 Redis 커넥션을 지정할 수 있습니다.
return [(new ThrottlesExceptionsWithRedis(10, 10 * 60))->connection('limiter')];Job 반환하기 (Releasing Jobs)
Release 미들웨어를 사용하면 Job을 실행하지 않고 그대로 큐로 다시 돌려보낼 수 있습니다. Release::when 메서드는 주어진 조건이 true일 때 Job을 큐로 돌려보내며, Release::unless 메서드는 조건이 false일 때 Job을 큐로 돌려보냅니다.
use Illuminate\Queue\Middleware\Release;
/**
* Job이 거쳐야 할 미들웨어 목록을 반환합니다.
*/
public function middleware(): array
{
return [
Release::when($condition, releaseAfter: 60),
];
}Job을 큐로 다시 돌려보내더라도 Job의 총 시도 횟수는 여전히 증가합니다. 따라서 Job 클래스의 Tries와 MaxExceptions 속성을 상황에 맞게 조정하는 것이 좋습니다.
더 복잡한 조건을 평가해야 한다면, when과 unless 메서드에 Closure를 전달할 수도 있습니다.
use Illuminate\Queue\Middleware\Release;
/**
* Job이 거쳐야 할 미들웨어 목록을 반환합니다.
*/
public function middleware(): array
{
return [
Release::when(function (): bool {
return ! $this->order->isPaid();
}, releaseAfter: 60),
];
}Job 건너뛰기 (Skipping Jobs)
Skip 미들웨어를 사용하면 Job 로직을 수정하지 않고도 특정 Job을 건너뛰거나 삭제하도록 지정할 수 있습니다. Skip::when 메서드는 주어진 조건이 true일 때 Job을 삭제하며, Skip::unless 메서드는 조건이 false일 때 Job을 삭제합니다.
use Illuminate\Queue\Middleware\Skip;
/**
* Job이 거쳐야 할 미들웨어 목록을 반환합니다.
*/
public function middleware(): array
{
return [
Skip::when($condition),
];
}더 복잡한 조건을 평가해야 한다면, when과 unless 메서드에 Closure를 전달할 수도 있습니다.
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
{
/**
* Store a new podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// ...
ProcessPodcast::dispatch($podcast);
return redirect('/podcasts');
}
}조건부로 Job을 디스패치하고 싶다면, dispatchIf와 dispatchUnless 메서드를 사용할 수 있습니다:
ProcessPodcast::dispatchIf($accountActive, $podcast);
ProcessPodcast::dispatchUnless($accountSuspended, $podcast);새로운 Laravel 애플리케이션에서는 database 연결이 기본 큐로 정의되어 있습니다. 애플리케이션의 .env 파일에서 QUEUE_CONNECTION 환경 변수를 변경하여 기본 큐 연결을 다르게 지정할 수 있습니다.
지연된 디스패치
큐 워커가 즉시 처리할 수 없도록 Job을 지정하고 싶다면, Job을 디스패치할 때 `delay` 메서드를 사용할 수 있습니다. 예를 들어, Job이 디스패치된 후 10분이 지나야 처리 가능하도록 지정해 봅시다:<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Store a new podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// ...
ProcessPodcast::dispatch($podcast)
->delay(now()->plus(minutes: 10));
return redirect('/podcasts');
}
}경우에 따라 Job에 기본 지연 시간이 설정되어 있을 수 있습니다. 이 지연을 건너뛰고 Job을 즉시 처리하도록 디스패치해야 한다면, withoutDelay 메서드를 사용할 수 있습니다:
ProcessPodcast::dispatch($podcast)->withoutDelay();WARNING
Amazon SQS 큐 서비스는 최대 지연 시간이 15분입니다.
동기 디스패치
Job을 즉시(동기적으로) 디스패치하고 싶다면, dispatchSync 메서드를 사용할 수 있습니다. 이 메서드를 사용하면 Job은 큐에 들어가지 않고 현재 프로세스 내에서 즉시 실행됩니다:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Store a new podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// Create podcast...
ProcessPodcast::dispatchSync($podcast);
return redirect('/podcasts');
}
}지연 디스패치(Deferred Dispatching)
지연 동기 디스패치를 사용하면, 현재 프로세스 동안 처리되지만 HTTP 응답이 사용자에게 전송된 이후에 처리되는 Job을 디스패치할 수 있습니다. 이를 통해 사용자의 애플리케이션 사용 경험을 느리게 하지 않으면서도 "큐에 등록된" Job을 동기적으로 처리할 수 있습니다. 동기 Job의 실행을 지연시키려면, deferred 커넥션으로 Job을 디스패치하면 됩니다:
RecordDelivery::dispatch($order)->onConnection('deferred');deferred 커넥션은 기본 장애 조치 큐(failover queue) 역할도 합니다.
마찬가지로 background 커넥션도 HTTP 응답이 사용자에게 전송된 이후에 Job을 처리하지만, 별도로 생성된 PHP 프로세스에서 Job이 처리되므로 PHP-FPM / 애플리케이션 워커가 다른 들어오는 HTTP 요청을 처리할 수 있게 됩니다:
RecordDelivery::dispatch($order)->onConnection('background');대량 디스패치(Bulk Dispatching)
한 번에 여러 개의 독립적인 Job을 디스패치하면서 [배치](#generating-job-classes) 추적이나 콜백이 필요하지 않은 경우, `Bus` 파사드의 `bulk` 메서드를 사용할 수 있습니다. Laravel은 설정된 큐 연결 및 큐 이름을 기준으로 Job들을 그룹화하여, 각 그룹을 해당 큐에 일괄로 푸시합니다:use App\Jobs\ProcessUser;
use Illuminate\Support\Facades\Bus;
Bus::bulk(
$users->map(fn ($user) => new ProcessUser($user))
);디스패치 전 Job 준비하기
Job이 큐에 푸시되기 전에 자신의 상태를 준비하거나 검사해야 하는 경우, 해당 Job은 Illuminate\Contracts\Queue\PreparesForDispatch 인터페이스를 구현할 수 있습니다. Laravel은 Job을 디스패치하기 전에 Job의 prepareForDispatch 메서드를 호출합니다. 이 메서드가 false를 반환하면 Job은 디스패치되지 않습니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\PreparesForDispatch;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Cache;
class SyncPodcasts implements PreparesForDispatch, ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(
public array $podcastIds,
) {}
/**
* Prepare the job before dispatching.
*/
public function prepareForDispatch(): bool
{
return collect($this->podcastIds)
->reject(fn (int $id) => Cache::has("podcast-syncing:{$id}"))
->isNotEmpty();
}
}Job과 데이터베이스 트랜잭션
데이터베이스 트랜잭션 내에서 Job을 디스패치하는 것은 전혀 문제가 없지만, Job이 실제로 성공적으로 실행될 수 있도록 특별히 주의를 기울여야 합니다. 트랜잭션 내에서 Job을 디스패치할 경우, 부모 트랜잭션이 커밋되기 전에 워커가 Job을 처리할 가능성이 있습니다. 이런 일이 발생하면, 데이터베이스 트랜잭션 중에 모델이나 데이터베이스 레코드에 가한 업데이트가 아직 데이터베이스에 반영되지 않았을 수 있습니다. 또한, 트랜잭션 내에서 생성된 모델이나 데이터베이스 레코드가 데이터베이스에 존재하지 않을 수도 있습니다.
다행히도 Laravel은 이 문제를 해결할 수 있는 몇 가지 방법을 제공합니다. 첫 번째로, 큐 연결 설정 배열에서 after_commit 연결 옵션을 설정할 수 있습니다:
'redis' => [
'driver' => 'redis',
// ...
'after_commit' => true,
],after_commit 옵션이 true인 경우, 데이터베이스 트랜잭션 내에서 Job을 디스패치할 수 있습니다. 하지만 Laravel은 실제로 Job을 디스패치하기 전에 열려 있는 부모 데이터베이스 트랜잭션이 커밋될 때까지 기다립니다. 물론, 현재 열려 있는 데이터베이스 트랜잭션이 없다면 Job은 즉시 디스패치됩니다.
트랜잭션 도중 발생한 예외로 인해 트랜잭션이 롤백되면, 해당 트랜잭션 중에 디스패치된 Job들은 폐기됩니다.
NOTE
after_commit 설정 옵션을 true로 설정하면, 대기 중인(큐에 등록된) 이벤트 리스너, 메일러블, 알림, 브로드캐스트 이벤트 또한 열려 있는 모든 데이터베이스 트랜잭션이 커밋된 후에 디스패치됩니다.
커밋 디스패치 동작을 인라인으로 지정하기
after_commit 큐 연결 설정 옵션을 true로 설정하지 않은 경우에도, 특정 Job이 열려 있는 모든 데이터베이스 트랜잭션이 커밋된 후에 디스패치되어야 함을 표시할 수 있습니다. 이를 위해 디스패치 작업에 afterCommit 메서드를 체이닝할 수 있습니다:
use App\Jobs\ProcessPodcast;
ProcessPodcast::dispatch($podcast)->afterCommit();마찬가지로, after_commit 설정 옵션이 true로 설정되어 있는 경우에도, 특정 Job이 열려 있는 데이터베이스 트랜잭션의 커밋을 기다리지 않고 즉시 디스패치되어야 함을 표시할 수 있습니다:
ProcessPodcast::dispatch($podcast)->beforeCommit();Job 체이닝
Job 체이닝을 사용하면 기본 Job이 성공적으로 실행된 후 순서대로 실행되어야 하는 큐에 등록된 Job 목록을 지정할 수 있습니다. 시퀀스 중 하나의 Job이 실패하면 나머지 Job들은 실행되지 않습니다. 큐에 등록된 Job 체인을 실행하려면 Bus 파사드에서 제공하는 chain 메서드를 사용할 수 있습니다. Laravel의 커맨드 버스는 큐에 등록된 Job 디스패치가 기반을 두고 있는 더 낮은 수준의 컴포넌트입니다:
use App\Jobs\OptimizePodcast;
use App\Jobs\ProcessPodcast;
use App\Jobs\ReleasePodcast;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->dispatch();Job 클래스 인스턴스를 체이닝하는 것 외에도, 클로저를 체이닝할 수도 있습니다:
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
function () {
Podcast::update(/* ... */);
},
])->dispatch();WARNING
Job 내에서 $this->delete() 메서드를 사용하여 Job을 삭제해도 체이닝된 Job의 처리가 중단되지는 않습니다. 체인은 체인 내의 Job이 실패한 경우에만 실행을 중단합니다.
체인 연결 및 큐
체이닝된 Job에 사용할 연결 및 큐를 지정하려면 onConnection 및 onQueue 메서드를 사용할 수 있습니다. 이 메서드는 큐에 등록된 Job에 명시적으로 다른 연결 / 큐가 지정되지 않는 한 사용할 큐 연결과 큐 이름을 지정합니다:
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->onConnection('redis')->onQueue('podcasts')->dispatch();체인에 Job 추가하기
경우에 따라 체인 내의 다른 Job에서 기존 Job 체인의 앞이나 뒤에 Job을 추가해야 할 수도 있습니다. prependToChain 및 appendToChain 메서드를 사용하여 이를 수행할 수 있습니다:
/**
* Job을 실행합니다.
*/
public function handle(): void
{
```php
// ...
// 현재 체인의 앞에 추가하여, 현재 Job 바로 다음에 실행...
$this->prependToChain(new TranscribePodcast);
// 현재 체인의 끝에 추가하여, 체인의 마지막에 실행...
$this->appendToChain(new TranscribePodcast);
}체인 실패
Job을 체이닝할 때, 체인 내의 Job이 실패했을 때 호출될 클로저를 지정하기 위해 catch 메서드를 사용할 수 있습니다. 주어진 콜백은 Job 실패를 발생시킨 Throwable 인스턴스를 전달받습니다:
use Illuminate\Support\Facades\Bus;
use Throwable;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->catch(function (Throwable $e) {
// 체인 내의 Job이 실패했습니다...
})->dispatch();WARNING
체인 콜백은 직렬화되어 나중에 Laravel 큐에 의해 실행되므로, 체인 콜백 내에서 $this 변수를 사용해서는 안 됩니다.
큐와 커넥션 커스터마이징하기
특정 큐로 디스패치하기
Job을 서로 다른 큐에 푸시함으로써, 큐에 등록된 Job들을 "분류"할 수 있으며 각 큐에 몇 개의 워커를 할당할지 우선순위를 정할 수도 있습니다. 이때 큐 설정 파일에서 정의한 서로 다른 큐 "커넥션"으로 Job을 푸시하는 것이 아니라, 하나의 커넥션 내에서 특정 큐로만 푸시한다는 점을 유의해야 합니다. 큐를 지정하려면, Job을 디스패치할 때 `onQueue` 메서드를 사용하세요:<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Store a new podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// Create podcast...
ProcessPodcast::dispatch($podcast)->onQueue('processing');
return redirect('/podcasts');
}
}또는, Job의 생성자 안에서 onQueue 메서드를 호출하여 해당 Job의 큐를 지정할 수도 있습니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct()
{
$this->onQueue('processing');
}
}특정 커넥션으로 디스패치하기
애플리케이션이 여러 큐 연결과 상호작용하는 경우, `onConnection` 메서드를 사용하여 Job을 어느 연결로 푸시할지 지정할 수 있습니다:<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* Store a new podcast.
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// Create podcast...
ProcessPodcast::dispatch($podcast)->onConnection('sqs');
return redirect('/podcasts');
}
}onConnection과 onQueue 메서드를 함께 체이닝하여 Job의 연결과 큐를 모두 지정할 수도 있습니다:
ProcessPodcast::dispatch($podcast)
->onConnection('sqs')
->onQueue('processing');또는, Job의 생성자 내에서 onConnection 메서드를 호출하여 Job의 연결을 지정할 수도 있습니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct()
{
$this->onConnection('sqs');
}
}큐 라우팅
`Queue` 파사드의 `route` 메서드를 사용하면 특정 Job 클래스에 대한 기본 연결과 큐를 정의할 수 있습니다. 이는 Job에서 연결이나 큐를 직접 지정하지 않고도 특정 Job이 항상 지정된 큐를 사용하도록 하고 싶을 때 유용합니다.특정 Job 클래스를 라우팅하는 것 외에도, route 메서드에 인터페이스, 트레이트, 또는 부모 클래스를 전달할 수도 있습니다. 이렇게 하면 해당 인터페이스를 구현하거나, 해당 트레이트를 사용하거나, 해당 부모 클래스를 확장하는 모든 Job이 자동으로 설정된 연결과 큐를 사용하게 됩니다.
일반적으로 route 메서드는 서비스 프로바이더의 boot 메서드에서 호출해야 합니다:
use App\Concerns\RequiresVideo;
use App\Jobs\ProcessPodcast;
use App\Jobs\ProcessVideo;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Support\Facades\Queue;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts');
Queue::route(RequiresVideo::class, queue: 'video');
Queue::route(ShouldBroadcast::class, queue: 'events');
}큐 없이 연결만 지정하면 해당 Job은 기본 큐로 전송됩니다:
Queue::route(ProcessPodcast::class, connection: 'redis');배열을 route 메서드에 전달하여 여러 Job 클래스를 한 번에 라우팅할 수도 있습니다:
Queue::route([
ProcessPodcast::class => ['redis', 'podcasts'], // Connection and queue
```php
ProcessVideo::class => 'videos', // 큐만 해당 (기본 연결 사용)
]);NOTE
큐 라우팅은 여전히 Job 단위로 재정의될 수 있습니다.
forward 메서드를 사용하여 Job을 한 큐에서 다른 큐 및/또는 연결로 전달할 수 있습니다. 이는 개별 Job이나 디스패치 위치를 수정하지 않고도 큐 인프라를 변경해야 할 때 유용합니다.
Queue::forward('reports', 'reports.fifo', 'sqs');
Queue::forward('payments', connection: 'sqs');
Queue::forward('updates', 'notifications');배열을 전달하여 여러 큐를 한 번에 전달할 수도 있습니다.
Queue::forward([
'reports' => 'reports.fifo',
'emails' => 'emails.fifo',
], connection: 'sqs');Job에 설정된 명시적인 연결은 전달된 연결보다 우선합니다.
최대 Job 시도 횟수 / 타임아웃 값 지정하기
최대 시도 횟수
Job 시도 횟수는 Laravel 큐 시스템의 핵심 개념이며 여러 고급 기능의 기반이 됩니다. 처음에는 헷갈릴 수 있지만, 기본 설정을 변경하기 전에 그 동작 방식을 이해하는 것이 중요합니다.
Job이 디스패치되면 큐에 등록됩니다. 그러면 워커가 이를 가져와 실행을 시도합니다. 이것이 바로 Job 시도입니다.
하지만 시도가 반드시 Job의 handle 메서드가 실행되었음을 의미하지는 않습니다. 시도는 여러 방식으로 "소비"될 수도 있습니다.
- Job이 실행 중에 처리되지 않은 예외를 만난 경우.
$this->release()를 사용하여 Job이 수동으로 큐로 다시 반환된 경우.WithoutOverlapping또는RateLimited와 같은 미들웨어가 잠금을 획득하지 못하여 Job을 반환한 경우.- Job이 타임아웃된 경우.
- Job의
handle메서드가 실행되어 예외를 던지지 않고 완료된 경우.
여러분은 아마도 Job을 무한정 계속 시도하고 싶지는 않을 것입니다. 따라서 Laravel은 Job을 몇 번 또는 얼마나 오랫동안 시도할 수 있는지 지정할 수 있는 다양한 방법을 제공합니다.
NOTE
기본적으로 Laravel은 Job을 단 한 번만 시도합니다. WithoutOverlapping 또는 RateLimited와 같은 미들웨어를 사용하는 경우나, Job을 수동으로 반환하는 경우에는 tries 옵션을 통해 허용되는 시도 횟수를 늘려야 할 가능성이 높습니다.
Job을 시도할 수 있는 최대 횟수를 지정하는 한 가지 방법은 Artisan 명령줄에서 --tries 스위치를 사용하는 것입니다. 이는 처리 중인 Job이 시도할 수 있는 횟수를 별도로 지정하지 않는 한, 워커가 처리하는 모든 Job에 적용됩니다:
php artisan queue:work --tries=3Job이 최대 시도 횟수를 초과하면 "실패한(failed)" Job으로 간주됩니다. 실패한 Job을 처리하는 방법에 대한 더 자세한 내용은 실패한 Job 문서를 참고하십시오. queue:work 명령에 --tries=0을 지정하면 Job은 무한정 재시도됩니다.
작업 클래스 자체에 Tries 속성을 사용하여 작업이 시도될 수 있는 최대 횟수를 정의함으로써 더 세밀한 방식으로 접근할 수도 있습니다. 작업에 최대 시도 횟수가 지정된 경우, 이는 명령줄에서 제공된 --tries 값보다 우선합니다:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Tries;
#[Tries(5)]
class ProcessPodcast implements ShouldQueue
{
// ...
}특정 Job의 최대 시도 횟수를 동적으로 제어해야 하는 경우, Job에 tries 메서드를 정의할 수 있습니다:
/**
* Determine number of times the job may be attempted.
*/
public function tries(): int
{
return 5;
}시간 기반 시도
Job이 시도될 수 있는 횟수를 정의하는 대신, Job이 더 이상 시도되지 않아야 하는 시점을 정의할 수도 있습니다. 이를 통해 주어진 시간 범위 내에서 Job이 여러 번 시도될 수 있습니다. Job이 더 이상 시도되지 않아야 하는 시점을 정의하려면, Job 클래스에 retryUntil 메서드를 추가하십시오. 이 메서드는 DateTime 인스턴스를 반환해야 합니다:
use DateTime;
/**
* Determine the time at which the job should timeout.
*/
public function retryUntil(): DateTime
{
return now()->plus(minutes: 10);
}retryUntil과 tries가 모두 정의된 경우, Laravel은 retryUntil 메서드를 우선시합니다.
최대 예외 횟수 (Max Exceptions)
때로는 Job이 여러 번 시도될 수 있지만, release 메서드에 의해 직접 릴리스되는 것이 아니라 처리되지 않은 예외가 주어진 횟수만큼 발생했을 때 실패하도록 지정하고 싶을 수 있습니다. 이를 위해 Job 클래스에 Tries와 MaxExceptions 속성을 사용할 수 있습니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Support\Facades\Redis;
#[Tries(25)]
#[MaxExceptions(3)]
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* Execute the job.
*/
public function handle(): void
{
Redis::throttle('key')->allow(10)->every(60)->then(function () {
// Lock obtained, process the podcast...
}, function () {
// Unable to obtain lock...
return $this->release(10);
});
}
}이 예제에서는 애플리케이션이 Redis 락을 획득하지 못한 경우 Job이 10초 동안 지연 후 릴리스되며, 최대 25번까지 재시도를 계속합니다. 하지만 Job에서 처리되지 않은 예외가 세 번 발생하면 Job은 실패하게 됩니다.
예외에 의한 재시도 중지
때로는 특정 예외가 발생하면 큐에 등록된 Job이 다시 시도되지 않고 즉시 실패해야 하는 경우가 있습니다. 애플리케이션의 bootstrap/app.php 파일에서 dontRetry 예외 메서드를 사용하여 Job 재시도를 중지해야 하는 예외 타입을 설정할 수 있습니다:
use App\Exceptions\InvalidPodcastSourceException;
use Illuminate\Foundation\Configuration\Exceptions;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->dontRetry([
InvalidPodcastSourceException::class,
]);
})재시도를 중지할 시점을 더 세밀하게 제어해야 하는 경우, dontRetryWhen 메서드에 클로저를 제공할 수 있습니다. 클로저가 true를 반환하면 Job은 실패한 것으로 표시되며 다시 시도되지 않습니다:
use App\Exceptions\PodcastProcessingException;
use Illuminate\Foundation\Configuration\Exceptions;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->dontRetryWhen(function (PodcastProcessingException $e) {
return $e->reason() === 'Subscription expired';
});
})타임아웃
대부분의 경우, 큐에 등록된 Job이 대략 얼마나 걸릴지 미리 알고 있을 것입니다. 이러한 이유로 Laravel에서는 "timeout" 값을 지정할 수 있습니다. 기본적으로 timeout 값은 60초입니다. Job이 timeout 값으로 지정된 초 수보다 오래 처리되면, 해당 Job을 처리하던 워커는 오류와 함께 종료됩니다. 일반적으로 워커는 [서버에 설정된 프로세스 관리자](#unique-jobs)에 의해 자동으로 재시작됩니다.Job이 실행될 수 있는 최대 초 수는 Artisan 명령줄에서 --timeout 스위치를 사용하여 지정할 수 있습니다:
php artisan queue:work --timeout=30Job이 계속 timeout이 발생하여 최대 시도 횟수를 초과하면, 해당 Job은 실패한 것으로 표시됩니다.
Job이 실행될 수 있는 최대 초 수는 Job 클래스에서 Timeout 속성(attribute)을 사용하여 정의할 수도 있습니다. Job에 timeout이 지정되어 있으면, 명령줄에 지정된 timeout보다 우선 적용됩니다:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Timeout;
#[Timeout(120)]
class ProcessPodcast implements ShouldQueue
{
// ...
}때로는 소켓이나 아웃바운드 HTTP 연결과 같은 IO 블로킹 프로세스가 지정한 timeout을 따르지 않을 수 있습니다. 따라서 이러한 기능을 사용할 때는 해당 API를 사용하여 항상 timeout 값을 함께 지정하도록 해야 합니다. 예를 들어 Guzzle을 사용할 때는 항상 연결(connection) 및 요청(request) timeout 값을 지정해야 합니다.
WARNING
job의 timeout을 지정하려면 PCNTL PHP 확장이 설치되어 있어야 합니다. 또한, job의 "timeout" 값은 항상 재시도 대기 시간 값보다 작아야 합니다. 그렇지 않으면, 실제로 실행이 완료되거나 timeout되기 전에 job이 다시 시도될 수 있습니다. --timeout 옵션은 queue:work 명령어가 --once 옵션과 함께 실행될 때는 아무런 효과가 없습니다.
Timeout 시 실패 처리
timeout이 발생했을 때 job이 실패로 표시되도록 하려면, job 클래스에 FailOnTimeout 속성을 사용하면 됩니다:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\FailOnTimeout;
#[FailOnTimeout]
class ProcessPodcast implements ShouldQueue
{
// ...
}NOTE
기본적으로, job이 timeout되면 한 번의 시도를 소모하고 (재시도가 허용된 경우) 큐에 다시 릴리즈됩니다. 하지만, job이 timeout 시 실패하도록 설정한 경우, tries에 설정된 값과 관계없이 재시도되지 않습니다.
SQS FIFO와 Fair 큐
Laravel는 [Amazon SQS FIFO(First-In-First-Out)](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-fifo-queues.html) 큐와 [fair](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-fair-queues.html) 큐를 지원합니다. FIFO 큐를 사용하면 메시지 중복 제거를 통해 정확히 한 번 처리를 보장하면서 Job을 전송된 순서 그대로 처리할 수 있습니다.FIFO 큐는 어떤 Job을 병렬로 처리할 수 있는지 결정하기 위해 메시지 그룹 ID가 필요합니다. 동일한 그룹 ID를 가진 Job은 순차적으로 처리되며, 서로 다른 그룹 ID를 가진 메시지는 동시에 처리될 수 있습니다.
Laravel은 Job을 디스패치할 때 메시지 그룹 ID를 지정할 수 있도록 플루언트한 onGroup 메서드를 제공합니다:
ProcessOrder::dispatch($order)
->onGroup("customer-{$order->customer_id}");메시지 그룹을 지정하지 않고 SQS FIFO 큐에 Job을 디스패치하면, Laravel은 큐 이름을 메시지 그룹 ID로 사용합니다.
SQS FIFO 큐는 정확히 한 번 처리를 보장하기 위해 메시지 중복 제거를 지원합니다. Job 클래스에 deduplicationId 메서드를 구현하여 커스텀 중복 제거 ID를 제공할 수 있습니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessSubscriptionRenewal implements ShouldQueue
{
use Queueable;
// ...
/**
* Get the job's deduplication ID.
*/
public function deduplicationId(): string
{
```php
return "renewal-{$this->subscription->id}";
}
}공정 큐 (Fair Queues)
SQS 표준 큐를 사용 중이라면, 메시지 그룹을 설정하면 공정 큐잉(fair queueing)이 활성화됩니다. 즉, 그룹을 할당하면 SQS가 이를 사용하여 테넌트 / 워크로드 간 공정한 전달을 유지합니다. 추가적인 Laravel 설정은 필요하지 않습니다.
디스패치 시점에 onGroup을 호출하는 대신, Job에 직접 messageGroup 메서드를 정의할 수도 있습니다:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessOrder implements ShouldQueue
{
use Queueable;
// ...
/**
* Get the job's message group.
*/
public function messageGroup(): string
{
return "customer-{$this->order->customer_id}";
}
}FIFO 리스너, 메일, 알림
FIFO 큐를 사용할 때는 리스너, 메일, 알림에도 메시지 그룹을 정의해야 합니다. 또는 이러한 객체의 큐잉된 인스턴스를 비-FIFO 큐로 디스패치할 수도 있습니다.
큐에 등록된 이벤트 리스너의 메시지 그룹을 정의하려면, 리스너에 messageGroup 메서드를 정의하세요. 선택적으로 deduplicationId 메서드도 정의할 수 있습니다:
<?php
namespace App\Listeners;
class SendShipmentNotification
{
// ...
/**
* Get the job's message group.
*/public function messageGroup(): string { return 'shipments'; }
/**
* Get the job's deduplication ID.
*/
public function deduplicationId(): string
{
return "shipment-notification-{$this->shipment->id}";
}}
FIFO 큐에 큐잉될 [메일 메시지](/docs/13.x/services/mail)를 전송할 때는, 알림을 전송할 때 `onGroup` 메서드와 선택적으로 `withDeduplicator` 메서드를 호출해야 합니다:
```php
use App\Mail\InvoicePaid;
use Illuminate\Support\Facades\Mail;
$invoicePaid = (new InvoicePaid($invoice))
->onGroup('invoices')
->withDeduplicator(fn () => 'invoices-'.$invoice->id);
Mail::to($request->user())->send($invoicePaid);FIFO 큐에 큐잉될 알림을 전송할 때는, 알림을 전송할 때 onGroup 메서드와 선택적으로 withDeduplicator 메서드를 호출해야 합니다:
use App\Notifications\InvoicePaid;
$invoicePaid = (new InvoicePaid($invoice))
->onGroup('invoices')
->withDeduplicator(fn () => 'invoices-'.$invoice->id);
$user->notify($invoicePaid);큐 페일오버(Queue Failover)
`failover` 큐 드라이버는 큐에 Job을 push할 때 자동 페일오버(failover) 기능을 제공합니다. `failover` 설정의 기본 큐 연결이 어떤 이유로든 실패하면, Laravel은 자동으로 목록에 설정된 다음 연결로 Job을 push하려고 시도합니다. 이는 큐의 신뢰성이 매우 중요한 프로덕션 환경에서 고가용성을 보장하는 데 특히 유용합니다.페일오버 큐 연결을 설정하려면, failover 드라이버를 지정하고 순서대로 시도할 연결 이름 배열을 제공하면 됩니다. 기본적으로 Laravel은 애플리케이션의 config/queue.php 설정 파일에 예제 페일오버 설정을 포함하고 있습니다:
'failover' => [
'driver' => 'failover',
'connections' => [
'redis',
'database',
'sync',
],
],failover 드라이버를 사용하는 연결을 설정했다면, 페일오버 기능을 사용하기 위해 애플리케이션의 .env 파일에서 페일오버 연결을 기본 큐 연결로 설정해야 합니다:
QUEUE_CONNECTION=failover다음으로, 페일오버 연결 목록에 있는 각 연결에 대해 최소 하나의 워커를 실행하세요:
php artisan queue:work redisphp artisan queue:work databaseNOTE
sync, background, deferred 큐 드라이버를 사용하는 연결의 경우 해당 드라이버가 현재 PHP 프로세스 내에서 Job을 처리하기 때문에 워커를 실행할 필요가 없습니다.
큐 연결 작업이 실패하고 페일오버가 활성화되면, Laravel은 Illuminate\Queue\Events\QueueFailedOver 이벤트를 디스패치하여 큐 연결이 실패했음을 보고하거나 기록할 수 있게 해줍니다.
NOTE
Laravel Horizon을 사용하는 경우, Horizon은 Redis 큐만 관리한다는 점을 기억하세요. 페일오버 목록에 database가 포함되어 있다면, Horizon과 함께 일반 php artisan queue:work database 프로세스를 실행해야 합니다.
에러 처리
Job이 처리되는 동안 예외가 발생하면, 해당 Job은 자동으로 큐에 다시 반환되어 재시도됩니다. Job은 애플리케이션에서 허용하는 최대 시도 횟수만큼 시도될 때까지 계속 큐에 반환됩니다. 최대 시도 횟수는 queue:work Artisan 명령어에서 사용되는 --tries 스위치로 정의됩니다. 또는 Job 클래스 자체에서 최대 시도 횟수를 정의할 수도 있습니다. 큐 워커 실행에 대한 더 자세한 내용은 아래에서 확인할 수 있습니다.
수동으로 Job 반환하기
때때로 Job을 나중에 다시 시도할 수 있도록 수동으로 큐에 반환하고 싶을 수 있습니다. release 메서드를 호출하여 이를 수행할 수 있습니다:
/**
* Execute the job.
*/
public function handle(): void
{
// ...
$this->release();
}기본적으로 release 메서드는 Job을 즉시 처리할 수 있도록 다시 큐로 되돌려 놓습니다. 하지만 정수나 날짜 인스턴스를 release 메서드에 전달하면, 지정한 초가 경과할 때까지 해당 Job이 처리 대상이 되지 않도록 지시할 수 있습니다:
$this->release(10);
$this->release(now()->plus(seconds: 10));Job을 수동으로 실패 처리하기
경우에 따라 Job을 수동으로 "실패"로 표시해야 할 수도 있습니다. 이를 위해 fail 메서드를 호출할 수 있습니다:
/**
* Execute the job.
*/
public function handle(): void
{
// ...
$this->fail();
}포착한 예외로 인해 Job을 실패로 표시하고 싶다면, 해당 예외를 fail 메서드에 전달할 수 있습니다. 또는 편의를 위해 문자열 오류 메시지를 전달하면 이것이 예외로 변환됩니다:
$this->fail($exception);
$this->fail('Something went wrong.');NOTE
실패한 Job에 대한 자세한 내용은 Job 실패 처리에 대한 문서를 확인하세요.
특정 예외 발생 시 Job 실패 처리하기
FailOnException Job 미들웨어를 사용하면 특정 예외가 발생했을 때 재시도를 중단시킬 수 있습니다. 이를 통해 외부 API 오류와 같은 일시적인 예외에 대해서는 재시도를 수행하면서도, 사용자 권한 취소와 같은 지속적인 예외가 발생하면 Job을 영구적으로 실패 처리할 수 있습니다:
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Middleware\FailOnException;
use Illuminate\Support\Facades\Http;
#[Tries(3)]
class SyncChatHistory implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct(
public User $user,
) {}
/**
* Execute the job.
*/
public function handle(): void
{
$this->user->authorize('sync-chat-history');
$response = Http::throw()->get(
"https://chat.laravel.test/?user={$this->user->uuid}"
);
// ...
}
/**
* Get the middleware the job should pass through.
*/
public function middleware(): array
{
return [
new FailOnException([AuthorizationException::class])
];
}
}큐
Job 배치(Batching)
Laravel의 Job 배치 기능을 사용하면 여러 Job을 그룹으로 묶어 병렬로 실행하고, 배치에 속한 모든 Job이 완료되었을 때 특정 동작을 수행할 수 있습니다.
시작하기 전에, Job 배치의 완료율 등 메타 정보를 저장할 테이블을 만들기 위한 마이그레이션을 생성해야 합니다. 이 마이그레이션은 make:queue-batches-table Artisan 명령어로 생성할 수 있습니다.
php artisan make:queue-batches-tablephp artisan migrate배치 가능한 Job 정의하기
배치 가능한 Job을 정의하려면 평소처럼 큐잉 가능한 Job을 생성한 뒤, Illuminate\Bus\Batchable 트레이트를 Job 클래스에 추가하면 됩니다. 이 트레이트는 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 파일의 일부분을 임포트합니다...
}
}배치 디스패치하기
Job 배치를 디스패치하려면 Bus 파사드의 batch 메서드를 사용합니다. 배치는 완료 콜백과 함께 사용할 때 진가를 발휘합니다. then, catch, finally 메서드를 사용하여 배치의 완료 콜백을 정의할 수 있으며, 각 콜백이 호출될 때는 Illuminate\Bus\Batch 인스턴스가 전달됩니다.
큐 워커를 여러 개 실행하는 경우, 배치에 속한 Job들은 병렬로 처리됩니다. 따라서 Job이 완료되는 순서는 배치에 추가된 순서와 다를 수 있습니다. 여러 Job을 순차적으로 실행하는 방법이 필요하다면 Job 체이닝과 배치 문서를 참고하세요.
다음 예제에서는 CSV 파일의 특정 행 범위를 각각 처리하는 Job들을 배치로 큐에 등록합니다.
use App\Jobs\ImportCsv;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Throwable;
$batch = Bus::batch([
new ImportCsv(1, 100),
new ImportCsv(101, 200),
new ImportCsv(201, 300),
new ImportCsv(301, 400),
new ImportCsv(401, 500),
])->before(function (Batch $batch) {
// 배치가 생성되었지만 아직 Job은 추가되지 않았습니다...
})->progress(function (Batch $batch) {
// 하나의 Job이 성공적으로 완료되었습니다...
})->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료되었습니다...
})->catch(function (Batch $batch, Throwable $e) {
// 배치 내에서 Job 실패가 감지되었습니다...
})->finally(function (Batch $batch) {
// 배치 실행이 종료되었습니다...
})->dispatch();
return $batch->id;$batch->id 속성으로 접근할 수 있는 배치의 ID는, 배치가 디스패치된 이후 Laravel 커맨드 버스에 배치 정보를 조회할 때 사용할 수 있습니다.
WARNING
배치 콜백은 직렬화되어 나중에 Laravel 큐에 의해 실행되므로, 콜백 내부에서 $this 변수를 사용해서는 안 됩니다. 또한 배치된 Job들은 데이터베이스 트랜잭션으로 감싸져 실행되므로, 암묵적 커밋(implicit commit)을 발생시키는 SQL 구문은 Job 내부에서 실행하지 않아야 합니다.
배치에 이름 지정하기
Laravel Horizon이나 Laravel Telescope 같은 도구는 배치에 이름이 지정되어 있으면 더 친절한 디버그 정보를 보여줍니다. 배치를 정의할 때 name 메서드를 호출하여 임의의 이름을 지정할 수 있습니다.
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료되었습니다...
})->name('Import CSV')->dispatch();배치 커넥션과 큐
배치된 Job들에 사용할 커넥션과 큐를 지정하고 싶다면 onConnection, onQueue 메서드를 사용하세요. 한 배치 안의 모든 Job은 동일한 커넥션과 큐에서 실행되어야 합니다.
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료되었습니다...
})->onConnection('redis')->onQueue('imports')->dispatch();체인과 배치의 조합
Job 체인을 배열로 묶어서 배치 안에 정의할 수도 있습니다. 예를 들어, 두 개의 Job 체인을 병렬로 실행하고 두 체인이 모두 끝났을 때 콜백을 실행할 수 있습니다.
use App\Jobs\ReleasePodcast;
use App\Jobs\SendPodcastReleaseNotification;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
Bus::batch([
[
new ReleasePodcast(1),
new SendPodcastReleaseNotification(1),
],
[
new ReleasePodcast(2),
new SendPodcastReleaseNotification(2),
],
])->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료되었습니다...
})->dispatch();반대로 체인 안에 배치를 정의하여, 배치 단위의 Job 그룹을 순차적으로 실행할 수도 있습니다. 예를 들어 먼저 여러 팟캐스트를 공개하는 Job 배치를 실행한 뒤, 공개 알림을 보내는 Job 배치를 실행하도록 만들 수 있습니다.
use App\Jobs\FlushPodcastCache;
use App\Jobs\ReleasePodcast;
use App\Jobs\SendPodcastReleaseNotification;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new FlushPodcastCache,
Bus::batch([
new ReleasePodcast(1),
new ReleasePodcast(2),
]),
Bus::batch([
new SendPodcastReleaseNotification(1),
new SendPodcastReleaseNotification(2),
]),
])->dispatch();배치에 Job 추가하기
경우에 따라 배치된 Job 내부에서 같은 배치에 추가 Job을 등록해야 할 때가 있습니다. 이 패턴은 수천 개의 Job을 배치해야 하는데, 이를 웹 요청 도중 한 번에 모두 디스패치하려면 시간이 너무 오래 걸리는 상황에서 유용합니다. 이럴 때는 먼저 배치를 채우는 역할을 하는 "로더" Job들을 초기 배치로 디스패치할 수 있습니다.
$batch = Bus::batch([
new LoadImportBatch,
new LoadImportBatch,
new LoadImportBatch,
])->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료되었습니다...
})->name('Import Contacts')->dispatch();이 예제에서는 LoadImportBatch Job을 사용해 배치에 추가 Job을 채워 넣습니다. 이를 위해 Job의 batch 메서드로 접근할 수 있는 배치 인스턴스의 add 메서드를 사용합니다.
use App\Jobs\ImportContacts;
use Illuminate\Support\Collection;
/**
* 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()) {
$this->batch()->cancel();
return;
}
if ($this->batch()->cancelled()) {
return;
}
}앞선 예제들에서 볼 수 있듯이, 배치된 Job은 일반적으로 작업을 계속 진행하기 전에 해당 배치가 취소되었는지 직접 확인해야 합니다. 하지만 편의를 위해 SkipIfBatchCancelled 미들웨어를 Job에 할당할 수도 있습니다. 이름 그대로, 이 미들웨어는 해당 배치가 취소된 경우 Job을 처리하지 않도록 Laravel에 지시합니다.
use Illuminate\Queue\Middleware\SkipIfBatchCancelled;
/**
* Job이 거쳐야 할 미들웨어를 반환합니다.
*/
public function middleware(): array
{
return [new SkipIfBatchCancelled];
}배치 실패 처리
배치 내 Job이 실패하면 (지정되어 있는 경우) catch 콜백이 호출됩니다. 이 콜백은 배치 내에서 처음으로 실패한 Job에 대해서만 한 번 호출됩니다.
실패 허용하기
배치 내 Job이 실패하면 Laravel은 자동으로 해당 배치를 "취소됨(cancelled)" 상태로 표시합니다. 원한다면 이 동작을 비활성화하여, Job이 실패해도 배치가 자동으로 취소되지 않도록 만들 수 있습니다. 배치를 디스패치할 때 allowFailures 메서드를 호출하면 됩니다.
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// 모든 Job이 성공적으로 완료되었습니다...
})->allowFailures()->dispatch();allowFailures 메서드에 클로저를 전달하면, Job이 실패할 때마다 해당 클로저가 실행됩니다.
$batch = Bus::batch([
// ...
])->allowFailures(function (Batch $batch, $exception) {
// 개별 Job 실패를 처리합니다...
})->dispatch();실패한 배치 Job 재시도하기
Laravel은 편의를 위해 queue:retry-batch Artisan 명령어를 제공합니다. 이 명령어는 특정 배치의 실패한 모든 Job을 손쉽게 재시도할 수 있게 해줍니다. 재시도할 배치의 UUID를 인자로 전달하면 됩니다.
php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5배치 데이터 정리(Pruning)
정리 작업을 하지 않으면 job_batches 테이블은 매우 빠르게 커질 수 있습니다. 이를 방지하려면 queue:prune-batches Artisan 명령어를 매일 실행되도록 스케줄링해야 합니다.
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches')->daily();기본적으로 완료된 지 24시간이 지난 배치가 모두 정리됩니다. 명령어 실행 시 hours 옵션을 사용하면 배치 데이터를 얼마나 오래 보관할지 조정할 수 있습니다. 예를 들어 아래 명령어는 완료된 지 48시간이 지난 배치를 모두 삭제합니다.
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches --hours=48')->daily();경우에 따라 job_batches 테이블에는 Job이 실패한 채 재시도되지 않아 끝내 완료되지 못한 배치 레코드가 쌓일 수 있습니다. queue:prune-batches 명령어의 unfinished 옵션을 사용하면 이러한 미완료 배치 레코드를 정리할 수 있습니다.
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches --hours=48 --unfinished=72')->daily();마찬가지로 취소된 배치 레코드도 job_batches 테이블에 쌓일 수 있습니다. 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과, 역시 문자열 타입의 기본 정렬 키인 id를 가져야 합니다. application 키에는 애플리케이션의 app 설정 파일 내 name 설정 값으로 정의된 애플리케이션 이름이 저장됩니다. 애플리케이션 이름이 DynamoDB 테이블 키의 일부이므로, 여러 Laravel 애플리케이션의 배치 정보를 같은 테이블에 함께 저장할 수 있습니다.
또한 배치 자동 정리 기능을 활용하려면 테이블에 ttl 속성을 정의할 수 있습니다.
DynamoDB 설정
먼저 Laravel 애플리케이션이 Amazon DynamoDB와 통신할 수 있도록 AWS SDK를 설치합니다.
composer require aws/aws-sdk-php그런 다음 queue.batching.driver 설정 값을 dynamodb로 지정합니다. 추가로 batching 설정 배열 안에 key, secret, region 설정 값을 정의해야 합니다. 이 값들은 AWS 인증에 사용됩니다. dynamodb 드라이버를 사용할 때는 queue.batching.database 설정 값이 필요하지 않습니다.
'batching' => [
'driver' => env('QUEUE_BATCHING_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'job_batches',
],DynamoDB에서 배치 정리하기
DynamoDB를 사용해 배치 정보를 저장하는 경우, 관계형 데이터베이스에서 사용하는 일반적인 정리 명령어는 동작하지 않습니다. 대신 DynamoDB의 네이티브 TTL 기능을 활용해 오래된 배치 레코드를 자동으로 제거할 수 있습니다.
DynamoDB 테이블에 ttl 속성을 정의했다면, Laravel에 배치 레코드를 어떻게 정리할지 지시하는 설정 값을 추가로 정의할 수 있습니다. queue.batching.ttl_attribute 설정 값은 TTL을 저장하는 속성의 이름을 정의하고, queue.batching.ttl 설정 값은 레코드가 마지막으로 업데이트된 시점을 기준으로 몇 초 후에 DynamoDB 테이블에서 배치 레코드를 제거할지 정의합니다.
'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 콜백은 직렬화되어 나중에 라라벨 큐 시스템에 의해 실행되므로, catch 콜백 내부에서는 $this 변수를 사용해서는 안 됩니다.
큐
큐 워커 실행하기
`queue:work` 명령어
Laravel에는 큐 워커를 실행하고, 큐에 새로운 Job이 들어올 때마다 이를 처리해 주는 Artisan 명령어가 내장되어 있습니다. queue:work 명령어로 워커를 실행할 수 있으며, 한 번 실행된 이 명령어는 수동으로 중지하거나 터미널을 닫기 전까지 계속 동작한다는 점에 유의하세요:
php artisan queue:workNOTE
queue:work 프로세스를 백그라운드에서 영구적으로 실행 상태로 유지하려면 Supervisor와 같은 프로세스 모니터링 도구를 사용해 큐 워커가 중단 없이 계속 동작하도록 해야 합니다.
queue:work 명령어를 실행할 때 -v 플래그를 함께 사용하면, 처리된 Job의 ID, 커넥션 이름, 큐 이름이 명령어 출력에 포함됩니다:
php artisan queue:work -v큐 워커는 한 번 시작되면 계속 살아있는(long-lived) 프로세스이며, 애플리케이션이 부팅된 상태를 메모리에 유지한다는 점을 꼭 기억하세요. 그래서 워커가 시작된 이후 작성한 코드의 변경 사항은 워커에 자동으로 반영되지 않습니다. 따라서 배포 과정에서는 반드시 큐 워커를 재시작해야 합니다. 또한 애플리케이션에서 생성하거나 수정한 정적(static) 상태 값들은 Job 간에 자동으로 초기화되지 않는다는 점도 유념해야 합니다.
코드나 애플리케이션 상태가 변경될 때마다 워커를 수동으로 재시작하고 싶지 않다면 queue:listen 명령어를 사용할 수도 있습니다. 다만 이 명령어는 queue:work에 비해 효율이 현저히 떨어집니다:
php artisan queue:listen여러 개의 큐 워커 실행하기
한 큐에 여러 워커를 배정해 동시에 여러 Job을 처리하고 싶다면, queue:work 프로세스를 여러 개 실행하기만 하면 됩니다. 로컬 환경에서는 터미널 탭을 여러 개 열어서, 프로덕션 환경에서는 프로세스 매니저의 설정을 통해 실행할 수 있습니다. Supervisor를 사용하는 경우라면 numprocs 설정값을 활용하면 됩니다.
커넥션과 큐 지정하기
워커가 사용할 큐 커넥션을 지정할 수도 있습니다. work 명령어에 전달하는 커넥션 이름은 config/queue.php 설정 파일에 정의된 커넥션 중 하나와 일치해야 합니다:
php artisan queue:work redisqueue: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을 처리한 뒤 정상적으로(gracefully) 종료하도록 지시할 수 있습니다. 이 옵션은 Docker 컨테이너 안에서 Laravel 큐를 처리하다가 큐가 비었을 때 컨테이너를 종료시키고 싶은 경우에 유용합니다:
php artisan queue:work --stop-when-empty지정한 시간(초) 동안 Job 처리하기
--max-time 옵션을 사용하면 워커가 지정한 시간(초) 동안 Job을 처리한 뒤 종료하도록 지시할 수 있습니다. 이 옵션 역시 Supervisor와 함께 사용하면, 일정 시간 동안 Job을 처리한 워커가 자동으로 재시작되면서 누적된 메모리를 해제할 수 있어 유용합니다:
<h1 id="pausing-and-resuming-queue-workers">1시간 동안 Job을 처리한 뒤 종료...</h1>
php artisan queue:work --max-time=3600워커의 대기(Sleep) 시간
큐에 처리할 Job이 있는 경우, 워커는 Job 사이에 지연 없이 계속해서 Job을 처리합니다. 반면 sleep 옵션은 처리할 Job이 없을 때 워커가 몇 초 동안 "대기(sleep)"할지를 결정합니다. 물론 대기하는 동안에는 새로운 Job이 들어와도 처리하지 않습니다:
php artisan queue:work --sleep=3점검 모드와 큐
애플리케이션이 점검 모드에 있는 동안에는 큐에 쌓인 Job이 처리되지 않습니다. 애플리케이션이 점검 모드에서 벗어나면 Job은 다시 평소처럼 처리됩니다.
점검 모드가 활성화되어 있어도 큐 워커가 Job을 강제로 처리하도록 하려면 --force 옵션을 사용하면 됩니다:
php artisan queue:work --force리소스 관련 주의사항
데몬 방식의 큐 워커는 각 Job을 처리하기 전에 프레임워크를 "재부팅"하지 않습니다. 따라서 Job 처리가 끝난 후에는 사용한 무거운 리소스를 직접 해제해 주어야 합니다. 예를 들어 GD 라이브러리를 사용해 이미지 처리 작업을 한다면, 처리가 끝난 뒤 imagedestroy로 메모리를 해제해야 합니다.
큐 우선순위
경우에 따라 큐가 처리되는 우선순위를 조정하고 싶을 수 있습니다. 예를 들어 config/queue.php 설정 파일에서 redis 커넥션의 기본 queue 값을 low로 지정해 둘 수 있습니다. 그런데 상황에 따라 특정 Job은 high처럼 우선순위가 높은 큐로 보내고 싶을 수 있습니다:
dispatch((new Job)->onQueue('high'));low 큐의 Job을 처리하기 전에 high 큐의 Job을 모두 우선 처리하는 워커를 실행하려면, work 명령어에 큐 이름을 쉼표로 구분해 전달하면 됩니다:
php artisan queue:work --queue=high,low큐 워커와 배포
큐 워커는 계속 살아있는 프로세스이기 때문에, 재시작하지 않으면 코드 변경 사항을 인지하지 못합니다. 따라서 큐 워커를 사용하는 애플리케이션을 배포할 때 가장 간단한 방법은 배포 과정 중에 워커를 재시작하는 것입니다. queue:restart 명령어를 실행하면 모든 워커를 정상적으로(gracefully) 재시작할 수 있습니다:
php artisan queue:restart이 명령어는 모든 큐 워커에게 현재 처리 중인 Job을 마친 후 정상적으로 종료하도록 지시하므로, 처리 중이던 Job이 유실되지 않습니다. queue:restart 명령어가 실행되면 큐 워커들이 종료되기 때문에, Supervisor와 같은 프로세스 매니저를 함께 운영하여 큐 워커를 자동으로 다시 시작하도록 해야 합니다.
NOTE
큐는 재시작 신호를 저장하기 위해 캐시를 사용합니다. 따라서 이 기능을 사용하기 전에 애플리케이션에 캐시 드라이버가 올바르게 설정되어 있는지 확인해야 합니다.
워커 시그널에 대응하기
큐 워커가 Job을 처리하는 도중 SIGQUIT, SIGTERM, SIGINT와 같은 종료 시그널을 받으면, 워커는 현재 처리 중인 Job을 끝마친 후 종료됩니다. 그런데 경우에 따라서는 서버나 컨테이너 오케스트레이터가 프로세스를 중지시키기 전에, Job 자체가 해당 시그널에 반응해야 할 때가 있습니다. 예를 들어 오랜 시간 실행되는 가져오기(import) 작업이라면, 시그널을 받았을 때 새로운 레코드 처리를 멈추고 현재까지의 진행 상황을 저장해야 할 수 있습니다.
Job 내부에서 워커 시그널에 반응하려면 Illuminate\Contracts\Queue\Interruptible 인터페이스를 구현하고 interrupted 메서드를 정의하면 됩니다. 워커가 받은 시그널 번호는 interrupted 메서드의 인자로 전달됩니다:
<?php
namespace App\Jobs;
use App\Models\Import;
use Illuminate\Contracts\Queue\Interruptible;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ImportProducts implements ShouldQueue, Interruptible
{
use Queueable;
protected bool $shouldStop = false;
/**
* 새로운 Job 인스턴스를 생성합니다.
*/
public function __construct(
public Import $import,
) {}
/**
* Job을 실행합니다.
*/
public function handle(): void
{
foreach ($this->import->pendingRows() as $row) {
if ($this->shouldStop) {
break;
}
// 상품(row) 데이터를 가져오는 처리...
}
$this->import->saveProgress();
}
/**
* 큐 워커로부터 받은 시그널을 처리합니다.
*/
public function interrupted(int $signal): void
{
$this->shouldStop = true;
}
}interrupted 메서드는 Job이 실행 중인 상태에서 워커가 프로세스 시그널을 받았을 때만 호출됩니다. 타임아웃이나 Job의 failed 메서드를 대체하는 기능이 아니라는 점에 유의하세요.
Job 만료와 타임아웃
Job 만료
config/queue.php 설정 파일에서 각 큐 커넥션은 retry_after 옵션을 가지고 있습니다. 이 옵션은 처리 중인 Job을 재시도하기까지 큐 커넥션이 몇 초를 기다려야 하는지를 지정합니다. 예를 들어 retry_after 값을 90으로 설정했다면, 해당 Job이 90초 동안 처리 완료(delete)되지도, 다시 큐에 반환(release)되지도 않은 경우 자동으로 다시 큐에 돌아갑니다. 일반적으로 retry_after 값은 Job이 정상적으로 완료되기까지 걸릴 것으로 예상되는 최대 시간(초)으로 설정하는 것이 좋습니다.
WARNING
retry_after 값을 갖지 않는 유일한 큐 커넥션은 Amazon SQS입니다. SQS는 AWS 콘솔에서 관리하는 Default Visibility Timeout 설정에 따라 Job을 재시도합니다.
워커 타임아웃
queue:work Artisan 명령어는 --timeout 옵션을 제공합니다. 기본값은 60초입니다. Job 처리 시간이 이 타임아웃 값보다 길어지면, 해당 Job을 처리하던 워커는 오류와 함께 종료됩니다. 보통은 서버에 설정된 프로세스 매니저에 의해 워커가 자동으로 다시 시작됩니다:
php artisan queue:work --timeout=60retry_after 설정 옵션과 --timeout CLI 옵션은 서로 다른 설정이지만, 함께 작동하면서 Job이 유실되지 않고 정확히 한 번만 성공적으로 처리되도록 보장합니다.
WARNING
--timeout 값은 항상 retry_after 설정값보다 몇 초 이상 짧게 설정해야 합니다. 이렇게 해야 멈춰버린(frozen) Job을 처리하던 워커가 해당 Job이 재시도되기 전에 확실히 종료됩니다. 만약 --timeout 옵션 값이 retry_after 설정값보다 길다면, Job이 두 번 처리될 수도 있습니다.
큐 워커 일시정지 및 재개하기
경우에 따라서는 큐 워커 자체를 완전히 중지하지 않으면서, 새로운 Job 처리만 일시적으로 막고 싶을 때가 있습니다. 예를 들어 시스템 점검 중에는 Job 처리를 잠시 멈추고 싶을 수 있습니다. Laravel은 큐 워커를 일시정지하고 재개할 수 있는 queue:pause, queue:continue Artisan 명령어를 제공합니다.
특정 큐를 일시정지하려면 큐 커넥션 이름과 큐 이름을 함께 지정합니다:
php artisan queue:pause database:default위 예시에서 database는 큐 커넥션 이름이고 default는 큐 이름입니다. 큐가 일시정지되면, 해당 큐에서 Job을 처리 중이던 워커는 현재 처리 중인 Job은 마저 끝내지만, 큐가 재개되기 전까지는 새로운 Job을 가져가지 않습니다.
모든 커넥션의 모든 큐에 대한 Job 처리를 일시정지하려면 --all 옵션을 사용합니다:
php artisan queue:pause --all일시정지된 큐의 Job 처리를 다시 시작하려면 queue:continue 명령어를 사용합니다:
php artisan queue:continue database:default모든 커넥션의 모든 큐에 대한 Job 처리를 재개하려면 queue:resume 명령어에 --all 옵션을 사용합니다:
php artisan queue:resume --all큐를 재개하면 워커는 즉시 해당 큐에서 새로운 Job을 처리하기 시작합니다. 단, 모든 큐를 한꺼번에 재개하더라도 개별적으로 일시정지된 큐는 재개되지 않는다는 점에 유의하세요. 또한 큐를 일시정지하는 것은 워커 프로세스 자체를 중지시키는 것이 아니라, 단지 해당 큐에서 새로운 Job을 가져가지 못하도록 막는 것일 뿐입니다.
워커 재시작과 일시정지 시그널
큐 워커는 기본적으로 Job을 하나 처리할 때마다 캐시 드라이버를 조회하여 재시작 및 일시정지 신호가 있는지 확인합니다. 이러한 폴링(polling)은 queue:restart와 queue:pause 명령어에 워커가 반응할 수 있도록 해주는 핵심 기능이지만, 약간의 성능 오버헤드를 발생시킵니다.
만약 성능을 최적화하고 싶고 이러한 중단(interruption) 관련 기능이 필요하지 않다면, Queue 파사드의 withoutInterruptionPolling 메서드를 호출해 이 폴링을 전역적으로 비활성화할 수 있습니다. 보통 이 설정은 AppServiceProvider의 boot 메서드 안에서 수행합니다:
use Illuminate\Support\Facades\Queue;
/**
* 애플리케이션의 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Queue::withoutInterruptionPolling();
}또는 Illuminate\Queue\Worker 클래스의 정적(static) 속성인 $restartable, $pausable을 설정하여 재시작 폴링과 일시정지 폴링을 개별적으로 비활성화할 수도 있습니다:
use Illuminate\Queue\Worker;
/**
* 애플리케이션의 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Worker::$restartable = false;
Worker::$pausable = false;
}WARNING
중단(interruption) 폴링을 비활성화하면, 비활성화한 기능에 따라 워커가 queue:restart 또는 queue:pause 명령어에 반응하지 않게 됩니다.
Supervisor 설정
운영 환경에서는 queue:work 프로세스를 계속 실행 상태로 유지할 방법이 필요합니다. queue:work 프로세스는 워커 타임아웃 초과, queue:restart 명령어 실행 등 여러 이유로 중단될 수 있습니다.
이러한 이유로, queue:work 프로세스가 종료된 것을 감지하고 자동으로 재시작해 줄 프로세스 모니터를 설정해야 합니다. 또한 프로세스 모니터를 사용하면 동시에 실행할 queue:work 프로세스의 개수도 지정할 수 있습니다. Supervisor는 Linux 환경에서 흔히 사용되는 프로세스 모니터로, 아래에서 설정 방법을 자세히 알아보겠습니다.
Supervisor 설치하기
Supervisor는 Linux 운영체제용 프로세스 모니터로, queue:work 프로세스가 실패하면 자동으로 재시작해 줍니다. Ubuntu에 Supervisor를 설치하려면 다음 명령어를 사용하면 됩니다.
sudo apt-get install supervisorNOTE
Supervisor를 직접 설정하고 관리하는 것이 부담스럽게 느껴진다면, Laravel 큐 워커 실행을 위한 완전 관리형 플랫폼인 Laravel Cloud를 사용하는 것도 좋은 방법입니다.
Supervisor 설정하기
Supervisor 설정 파일은 보통 /etc/supervisor/conf.d 디렉토리에 저장됩니다. 이 디렉토리 안에는 Supervisor가 어떻게 프로세스를 모니터링해야 하는지를 지시하는 설정 파일을 원하는 만큼 만들 수 있습니다. 예를 들어, queue:work 프로세스를 시작하고 모니터링하는 laravel-worker.conf 파일을 다음과 같이 만들어 보겠습니다.
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/app.com/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/home/forge/app.com/worker.log
stopwaitsecs=3600이 예시에서 numprocs 항목은 Supervisor에게 queue:work 프로세스를 8개 실행하고 모두 모니터링하며, 실패하면 자동으로 재시작하라고 지시합니다. 실제 사용하시는 큐 커넥션과 워커 옵션에 맞게 command 항목의 값을 수정해야 합니다.
WARNING
stopwaitsecs 값은 가장 오래 실행되는 Job의 처리 시간보다 커야 합니다. 그렇지 않으면 Supervisor가 Job이 완료되기 전에 강제로 종료시킬 수 있습니다.
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 옵션으로 Job의 최대 시도 횟수를 지정할 수 있습니다. --tries 옵션 값을 지정하지 않으면, Job은 한 번만 시도되거나 Job 클래스의 Tries 속성(attribute)에 지정된 횟수만큼 시도됩니다.
php artisan queue:work redis --tries=3--backoff 옵션을 사용하면 예외가 발생한 Job을 재시도하기 전에 Laravel이 몇 초를 대기할지 지정할 수 있습니다. 기본적으로는 Job이 즉시 큐로 다시 반환되어 곧바로 재시도됩니다.
php artisan queue:work redis --tries=3 --backoff=3Job별로 재시도 대기 시간을 다르게 설정하고 싶다면, Job 클래스에 Backoff 속성(attribute)을 사용하면 됩니다.
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Backoff;
#[Backoff(3)]
class ProcessPodcast implements ShouldQueue
{
// ...
}재시도 대기 시간을 계산하는 데 더 복잡한 로직이 필요하다면, Job 클래스에 backoff 메서드를 정의할 수 있습니다.
/**
* Job을 재시도하기 전 대기할 초 수를 계산합니다.
*/
public function backoff(): int
{
return 3;
}배열 형태로 백오프 값을 지정하면 "지수적(exponential)" 백오프도 손쉽게 구현할 수 있습니다. 아래 예시에서는 첫 번째 재시도는 1초, 두 번째 재시도는 5초, 세 번째 재시도는 10초 대기하며, 그 이후 남은 시도가 있다면 계속 10초씩 대기합니다.
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Backoff;
#[Backoff([1, 5, 10])]
class ProcessPodcast implements ShouldQueue
{
// ...
}실패한 Job 이후 뒷정리하기
특정 Job이 실패했을 때 사용자에게 알림을 보내거나, Job이 부분적으로 완료했던 작업을 되돌리고 싶을 수 있습니다. 이럴 때는 Job 클래스에 failed 메서드를 정의하면 됩니다. Job이 실패한 원인이 된 Throwable 인스턴스가 failed 메서드에 전달됩니다.
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Throwable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* 새로운 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이 수동으로 또는 미들웨어에 의해 큐로 다시 반환(release)된 경우
마지막 시도에서 Job 실행 중 예외가 발생하여 실패한 경우, 해당 예외가 failed 메서드로 전달됩니다. 반면 최대 시도 횟수에 도달해 실패한 경우에는 $exception이 Illuminate\Queue\MaxAttemptsExceededException의 인스턴스가 됩니다. 마찬가지로 설정된 타임아웃을 초과해서 실패한 경우에는 $exception이 Illuminate\Queue\TimeoutExceededException의 인스턴스가 됩니다.
실패한 Job 재시도하기
failed_jobs 테이블에 저장된 실패한 Job 목록을 확인하려면 queue:failed Artisan 명령어를 사용합니다.
php artisan queue:failedqueue:failed 명령어는 Job ID, 커넥션, 큐, 실패 시각 등 여러 정보를 함께 보여줍니다. 이 Job ID를 이용해 실패한 Job을 재시도할 수 있습니다. 예를 들어 ID가 ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece인 실패한 Job을 재시도하려면 다음과 같이 실행합니다.
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece필요하다면 여러 개의 ID를 한 번에 전달할 수도 있습니다.
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 91401d2c-0784-4f43-824c-34f94a33c24d특정 큐에 속한 실패한 Job을 전부 재시도할 수도 있습니다.
php artisan queue:retry --queue=name모든 실패한 Job을 재시도하려면 queue:retry 명령어에 ID 대신 all을 전달하면 됩니다.
php artisan queue:retry all실패한 Job을 삭제하고 싶다면 queue:forget 명령어를 사용합니다.
php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24dNOTE
Horizon을 사용 중이라면, 실패한 Job을 삭제할 때 queue:forget 대신 horizon:forget 명령어를 사용해야 합니다.
failed_jobs 테이블에 저장된 모든 실패한 Job을 삭제하려면 queue:flush 명령어를 사용합니다.
php artisan queue:flushqueue:flush 명령어는 실패한 지 얼마나 오래되었는지와 관계없이 모든 실패한 Job 레코드를 삭제합니다. --hours 옵션을 사용하면 지정한 시간 이전에 실패한 Job만 삭제할 수 있습니다.
php artisan queue:flush --hours=48존재하지 않는 모델 무시하기
Job에 Eloquent 모델을 주입하면, 해당 모델은 큐에 등록되기 전 자동으로 직렬화되고, Job이 실제로 처리될 때 데이터베이스에서 다시 조회됩니다. 그런데 Job이 워커에서 처리되기를 기다리는 동안 해당 모델이 삭제되어 버렸다면, Job은 ModelNotFoundException과 함께 실패할 수 있습니다.
이런 경우를 대비해, Job 클래스에 DeleteWhenMissingModels 속성(attribute)을 지정하면 모델이 존재하지 않을 때 해당 Job을 자동으로 삭제하도록 설정할 수 있습니다. 이 속성이 지정되어 있으면 Laravel은 예외를 발생시키지 않고 조용히 Job을 폐기합니다.
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\DeleteWhenMissingModels;
#[DeleteWhenMissingModels]
class ProcessPodcast implements ShouldQueue
{
// ...
}실패한 Job 정리(Pruning)하기
queue:prune-failed Artisan 명령어를 실행하면 failed_jobs 테이블의 레코드를 정리할 수 있습니다.
php artisan queue:prune-failed기본적으로는 24시간이 지난 실패한 Job 레코드가 모두 삭제됩니다. --hours 옵션을 지정하면 지정한 시간 이내에 등록된 레코드만 남기고 나머지는 삭제됩니다. 예를 들어 다음 명령어는 48시간 이전에 등록된 실패한 Job 레코드를 모두 삭제합니다.
php artisan queue:prune-failed --hours=48실패한 Job을 DynamoDB에 저장하기
Laravel은 실패한 Job 레코드를 관계형 데이터베이스 테이블 대신 DynamoDB에 저장하는 기능도 지원합니다. 다만 이 경우 실패한 Job 레코드를 저장할 DynamoDB 테이블을 직접 생성해야 합니다. 일반적으로 테이블 이름은 failed_jobs로 지정하지만, 애플리케이션의 queue 설정 파일에 있는 queue.failed.table 설정값에 맞춰 테이블 이름을 지어야 합니다.
failed_jobs 테이블에는 application이라는 문자열 파티션 키(primary partition key)와 uuid라는 문자열 정렬 키(primary sort key)가 필요합니다. application 키에는 애플리케이션의 app 설정 파일에 정의된 name 설정값, 즉 애플리케이션 이름이 저장됩니다. 애플리케이션 이름이 DynamoDB 테이블 키의 일부이기 때문에, 여러 Laravel 애플리케이션의 실패한 Job을 하나의 테이블에서 함께 관리할 수 있습니다.
또한 Laravel 애플리케이션이 Amazon DynamoDB와 통신할 수 있도록 AWS SDK를 설치해야 합니다.
composer require aws/aws-sdk-php그다음 queue.failed.driver 설정값을 dynamodb로 지정합니다. 그리고 실패한 Job 설정 배열 안에 key, secret, region 설정값도 함께 정의해야 합니다. 이 값들은 AWS 인증에 사용됩니다. dynamodb 드라이버를 사용할 때는 queue.failed.database 설정값은 필요하지 않습니다.
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'failed_jobs',
],실패한 Job 저장 비활성화하기
queue.failed.driver 설정값을 null로 지정하면, 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을 삭제해야 합니다.
기본 연결(connection)의 기본 큐에 있는 모든 Job을 삭제하고 싶다면 queue:clear Artisan 명령어를 사용하면 됩니다:
php artisan queue:clear특정 연결과 큐를 지정해서 Job을 삭제하고 싶다면 connection 인자와 queue 옵션을 함께 전달하면 됩니다:
php artisan queue:clear redis --queue=emailsWARNING
큐에서 Job을 삭제하는 기능은 SQS, Redis, database 큐 드라이버에서만 사용할 수 있습니다. 또한 SQS의 메시지 삭제 처리는 최대 60초까지 걸릴 수 있으므로, 큐를 비운 후 60초 이내에 SQS 큐로 전송된 Job은 함께 삭제될 수 있습니다.
큐 모니터링
큐에 갑자기 많은 Job이 몰리면 큐가 과부하 상태가 되어 Job이 처리될 때까지 오랜 시간이 걸릴 수 있습니다. 이런 상황을 미리 파악할 수 있도록, Laravel은 큐에 쌓인 Job 개수가 지정한 임계치를 초과했을 때 알림을 받을 수 있는 기능을 제공합니다.
이 기능을 사용하려면 먼저 queue:monitor 명령어를 매 분마다 실행되도록 스케줄링해야 합니다. 이 명령어에는 모니터링하고 싶은 큐 이름들과, 원하는 Job 개수 임계치를 함께 전달합니다:
php artisan queue:monitor redis:default,redis:deployments --max=100다만 이 명령어를 스케줄링하는 것만으로는 큐 과부하 상태를 알려주는 알림이 자동으로 발송되지는 않습니다. 이 명령어가 실행되었을 때 임계치를 초과한 큐를 발견하면 Illuminate\Queue\Events\QueueBusy 이벤트가 디스패치됩니다. 애플리케이션의 AppServiceProvider에서 이 이벤트를 리스닝하여, 본인이나 개발팀에게 알림을 전송하도록 처리할 수 있습니다:
use App\Notifications\QueueHasLongWaitTime;
use Illuminate\Queue\Events\QueueBusy;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Event::listen(function (QueueBusy $event) {
Notification::route('mail', 'dev@example.com')
->notify(new QueueHasLongWaitTime(
$event->connectionName,
$event->queue,
$event->size
));
});
}NOTE
실무에서는 큐별로 서로 다른 임계치를 설정하는 경우가 많습니다. 예를 들어 실시간성이 중요한 알림 큐는 임계치를 낮게(예: 10) 잡고, 리포트 생성처럼 시간이 걸려도 되는 큐는 임계치를 여유 있게(예: 500) 잡는 식으로 운영 특성에 맞게 조정하는 것이 좋습니다.
큐
테스트
Job을 디스패치하는 코드를 테스트할 때는 Job 자체를 실행하지 않도록 Laravel에 지시하는 편이 좋습니다. Job의 로직은 그 자체로 별도 테스트하면 충분하고, 디스패치하는 코드는 "Job이 큐에 잘 등록되는지"만 검증하면 되기 때문입니다. 물론 Job 자체를 테스트하고 싶다면 Job 인스턴스를 직접 생성하여 테스트에서 handle 메서드를 바로 호출하면 됩니다.
Queue 파사드의 fake 메서드를 사용하면 큐에 등록되는 Job이 실제로 실행되지 않도록 막을 수 있습니다. Queue::fake()를 호출한 뒤에는 애플리케이션이 큐에 Job을 등록하려고 시도했는지 다양한 방식으로 검증할 수 있습니다.
Pest
<?php
use App\Jobs\AnotherJob;
use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Queue;
test('orders can be shipped', function () {
Queue::fake();
// 주문 배송 처리...
// Job이 하나도 등록되지 않았는지 확인...
Queue::assertNothingPushed();
// 특정 큐에 Job이 등록되었는지 확인...
Queue::assertPushedOn('queue-name', ShipOrder::class);
// Job이 등록되었는지 확인
Queue::assertPushed(ShipOrder::class);
// Job이 정확히 한 번 등록되었는지 확인...
Queue::assertPushedOnce(ShipOrder::class);
// Job이 두 번 등록되었는지 확인...
Queue::assertPushedTimes(ShipOrder::class, 2);
// Job이 등록되지 않았는지 확인...
Queue::assertNotPushed(AnotherJob::class);
// 클로저가 큐에 등록되었는지 확인...
Queue::assertClosurePushed();
// 클로저가 등록되지 않았는지 확인...
Queue::assertClosureNotPushed();
// 등록된 Job의 총 개수를 확인...
Queue::assertCount(3);
});PHPUnit
<?php
namespace Tests\Feature;
use App\Jobs\AnotherJob;
use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_orders_can_be_shipped(): void
{
Queue::fake();
// 주문 배송 처리...
// Job이 하나도 등록되지 않았는지 확인...
Queue::assertNothingPushed();
// 특정 큐에 Job이 등록되었는지 확인...
Queue::assertPushedOn('queue-name', ShipOrder::class);
// Job이 등록되었는지 확인
Queue::assertPushed(ShipOrder::class);
// Job이 정확히 한 번 등록되었는지 확인...
Queue::assertPushedOnce(ShipOrder::class);
// Job이 두 번 등록되었는지 확인...
Queue::assertPushedTimes(ShipOrder::class, 2);
// Job이 등록되지 않았는지 확인...
Queue::assertNotPushed(AnotherJob::class);
// 클로저가 큐에 등록되었는지 확인...
Queue::assertClosurePushed();
// 클로저가 등록되지 않았는지 확인...
Queue::assertClosureNotPushed();
// 등록된 Job의 총 개수를 확인...
Queue::assertCount(3);
}
}assertPushed, assertNotPushed, assertClosurePushed, assertClosureNotPushed 메서드에는 클로저를 전달하여 특정 "조건"을 만족하는 Job이 등록되었는지 검증할 수도 있습니다. 전달한 조건을 만족하는 Job이 하나라도 등록되었다면 어서션은 성공합니다.
use Illuminate\Queue\CallQueuedClosure;
Queue::assertPushed(function (ShipOrder $job) use ($order) {
return $job->order->id === $order->id;
});
Queue::assertClosurePushed(function (CallQueuedClosure $job) {
return $job->name === 'validate-order';
});일부 Job만 페이크 처리하기
특정 Job만 페이크 처리하고 나머지 Job은 정상적으로 실행되도록 하고 싶다면, 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만 제외하고 나머지 모든 Job을 페이크 처리하고 싶다면 except 메서드를 사용합니다.
Queue::fake()->except([
ShipOrder::class,
]);Job 체인 테스트하기
Job 체인을 테스트하려면 Bus 파사드가 제공하는 페이크 기능을 사용해야 합니다. Bus 파사드의 assertChained 메서드를 사용하면 Job 체인이 디스패치되었는지 검증할 수 있습니다. assertChained 메서드의 첫 번째 인자로는 체인에 포함된 Job들의 배열을 전달합니다.
use App\Jobs\RecordShipment;
use App\Jobs\ShipOrder;
use App\Jobs\UpdateInventory;
use Illuminate\Support\Facades\Bus;
Bus::fake();
// ...
Bus::assertChained([
ShipOrder::class,
RecordShipment::class,
UpdateInventory::class
]);위 예시처럼 체인에 포함된 Job 배열에는 클래스명을 넣을 수도 있지만, 실제 Job 인스턴스를 넣을 수도 있습니다. 이 경우 Laravel은 애플리케이션에서 디스패치된 체인의 Job과 클래스가 동일하고 프로퍼티 값도 일치하는지까지 확인합니다.
Bus::assertChained([
new ShipOrder,
new RecordShipment,
new UpdateInventory,
]);assertDispatchedWithoutChain 메서드를 사용하면 특정 Job이 체인 없이 단독으로 등록되었는지 검증할 수 있습니다.
Bus::assertDispatchedWithoutChain(ShipOrder::class);체인 수정 사항 테스트하기
체인 내의 Job이 기존 체인에 Job을 추가하는 경우, 해당 Job의 assertHasChain 메서드를 사용해 남은 체인이 예상한 내용과 일치하는지 검증할 수 있습니다.
$job = new ProcessPodcast;
$job->handle();
$job->assertHasChain([
new TranscribePodcast,
new OptimizePodcast,
new ReleasePodcast,
]);assertDoesntHaveChain 메서드를 사용하면 Job의 남은 체인이 비어 있는지 확인할 수 있습니다.
$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;
});hasJobs 메서드를 pending batch에서 사용하면 배치에 예상한 Job들이 포함되어 있는지 확인할 수 있습니다. 이 메서드는 Job 인스턴스, 클래스명, 클로저의 배열을 인자로 받습니다.
Bus::assertBatched(function (PendingBatch $batch) {
return $batch->hasJobs([
new ProcessCsvRow(row: 1),
new ProcessCsvRow(row: 2),
new ProcessCsvRow(row: 3),
]);
});클로저를 사용하는 경우, 클로저는 Job 인스턴스를 인자로 전달받습니다. 이때 기대하는 Job 타입은 클로저의 타입힌트로부터 추론됩니다.
Bus::assertBatched(function (PendingBatch $batch) {
return $batch->hasJobs([
fn (ProcessCsvRow $job) => $job->row === 1,
fn (ProcessCsvRow $job) => $job->row === 2,
fn (ProcessCsvRow $job) => $job->row === 3,
]);
});assertBatchCount 메서드를 사용하면 지정한 개수만큼의 배치가 디스패치되었는지 확인할 수 있습니다.
Bus::assertBatchCount(3);assertNothingBatched 메서드를 사용하면 어떤 배치도 디스패치되지 않았는지 확인할 수 있습니다.
Bus::assertNothingBatched();Job과 배치 간의 상호작용 테스트하기
경우에 따라 개별 Job이 자신이 속한 배치와 어떻게 상호작용하는지 테스트해야 할 때가 있습니다. 예를 들어 어떤 Job이 배치의 나머지 처리를 취소했는지 확인하고 싶을 수 있습니다. 이를 위해서는 withFakeBatch 메서드를 사용해 Job에 페이크 배치를 할당해야 합니다. withFakeBatch 메서드는 Job 인스턴스와 페이크 배치로 이루어진 튜플을 반환합니다.
[$job, $batch] = (new ShipOrder)->withFakeBatch();
$job->handle();
$this->assertTrue($batch->cancelled());
$this->assertEmpty($batch->added);Job과 큐 간의 상호작용 테스트하기
큐에 등록된 Job이 스스로를 다시 큐로 돌려놓는지 테스트해야 할 때가 있습니다. 혹은 Job이 스스로를 삭제하는지 테스트해야 할 수도 있습니다. 이러한 큐 상호작용을 테스트하려면 Job 인스턴스를 생성한 뒤 withFakeQueueInteractions 메서드를 호출하면 됩니다.
Job의 큐 상호작용이 페이크로 처리되면 이제 Job의 handle 메서드를 호출할 수 있습니다. Job을 실행한 후에는 다양한 어서션 메서드를 사용해 큐 상호작용을 검증할 수 있습니다.
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
// $event->job->payload()
});
Queue::after(function (JobProcessed $event) {
// $event->connectionName
// $event->job
// $event->job->payload()
});
}
}Queue 파사드의 looping 메서드를 사용하면, 워커가 큐에서 Job을 가져오기 시작하기 직전에 실행할 콜백을 지정할 수 있습니다. 예를 들어, 이전에 실패한 Job이 커밋되지 않은 채로 남겨둔 트랜잭션을 롤백하는 클로저를 등록할 수 있습니다.
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
Queue::looping(function () {
while (DB::transactionLevel() > 0) {
DB::rollBack();
}
});또한 큐 워커가 큐에서 Job을 가져오지 못했을 때, Laravel은 Illuminate\Queue\Events\WorkerIdle 이벤트를 발생시킵니다.
use Illuminate\Queue\Events\WorkerIdle;
use Illuminate\Support\Facades\Event;
Event::listen(function (WorkerIdle $event) {
// $event->connectionName
// $event->queue
// $event->workerOptions
});NOTE
before, after, looping 콜백은 워커가 살아있는 동안 계속 재사용되므로, 큐 모니터링 대시보드나 알림 시스템을 만들 때 이 이벤트들을 활용하면 별도의 폴링 로직 없이도 Job 처리 현황을 실시간으로 추적할 수 있습니다.