메인 콘텐츠로 건너뛰기

큐란

웹 애플리케이션에서는 메일 발송·이미지 리사이즈·외부 API에의 문의 등, 완료까지 수 초가 걸리는 처리가 발생할 수 있습니다. 이를 HTTP 리퀘스트 중에 동기적으로 수행하면, 사용자는 응답이 반환될 때까지 계속 기다려야 합니다. Laravel의 큐를 사용하면, 이러한 무거운 처리를 백그라운드에서 비동기로 실행할 수 있습니다. 리퀘스트는 즉시 응답을 반환하고, 실제의 처리는 워커 프로세스가 별도로 처리해 줍니다.
큐는 데이터베이스·Redis·Amazon SQS 등 여러 백엔드에 대응하고 있습니다. 개발 환경에서는 sync 드라이버를 사용하면, 큐를 사용하지 않고 잡을 즉시 실행할 수 있습니다.

큐 설정

config/queue.php

큐의 설정은 config/queue.php에 집약되어 있습니다. QUEUE_CONNECTION 환경 변수로 사용할 드라이버를 전환합니다.
// config/queue.php
'default' => env('QUEUE_CONNECTION', 'database'),

.env 설정

# 드라이버 선택
QUEUE_CONNECTION=database

# Redis를 사용하는 경우
# QUEUE_CONNECTION=redis
# REDIS_HOST=127.0.0.1
# REDIS_PORT=6379

데이터베이스 드라이버 준비

database 드라이버를 사용하는 경우, 잡을 저장하는 테이블이 필요합니다. Laravel 11 이후의 신규 프로젝트에는 처음부터 마이그레이션이 포함되어 있지만, 포함되어 있지 않은 경우는 다음 명령으로 작성합니다.
php artisan make:queue-table
php artisan migrate

Redis 드라이버 준비

redis 드라이버를 사용하는 경우는 config/database.php에 Redis 접속 설정을 추가하고, Composer로 드라이버를 설치합니다.
composer require predis/predis

SQS Overflow Storage

Amazon SQS는 메시지 페이로드의 최대 크기에 제한이 있습니다. 페이로드가 커지는 잡을 다루는 경우에는, 초과분을 캐시 스토어에 저장하고, SQS에는 포인터만 보내는 설정을 추가합니다.
'sqs' => [
    // ...
    '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),
    ],
],
  • enabled를 활성화하면 1MB 이상의 페이로드를 지정한 캐시 스토어에 저장합니다.
  • alwaystrue로 하면 크기에 관계없이 모든 SQS 페이로드를 캐시 스토어에 저장합니다.
  • delete_after_processing은 잡 성공 후에 저장된 페이로드를 삭제합니다(기본값 true).
  • flush_on_cleartrue로 하면 queue:clear 실행 시에 overflow용 스토어를 flush합니다. 통상의 캐시를 지우지 않도록, 전용 스토어와 조합해 사용해 주세요.

잡 클래스 작성

make:job 명령

make:job Artisan 명령으로 잡 클래스의 템플릿을 생성합니다.
php artisan make:job SendWelcomeEmail
app/Jobs/SendWelcomeEmail.php가 생성됩니다.

잡 클래스의 구조

<?php

namespace App\Jobs;

use App\Models\User;
use App\Mail\WelcomeMail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail implements ShouldQueue
{
    use Queueable;

    /**
     * 잡의 인스턴스를 작성
     */
    public function __construct(
        public User $user,
    ) {}

    /**
     * 잡을 실행
     */
    public function handle(): void
    {
        Mail::to($this->user->email)->send(new WelcomeMail($this->user));
    }
}
ShouldQueue 인터페이스를 구현함으로써, 이 잡이 큐에서 비동기 처리됨을 Laravel에 전합니다. Queueable 트레이트가 잡의 큐 조작에 필요한 메서드를 제공합니다.
컨스트럭터에 Eloquent 모델을 전달하면, Laravel은 자동적으로 ID만을 시리얼라이즈합니다. 실행 시에 데이터베이스로부터 최신 데이터를 다시 취득하기 때문에, 큐의 페이로드가 가벼워집니다.

잡의 디스패치

dispatch()

컨트롤러나 서비스로부터 잡을 큐에 보내려면 dispatch()를 사용합니다.
use App\Jobs\SendWelcomeEmail;

// 라우트나 컨트롤러 안에서
public function register(Request $request): RedirectResponse
{
    $user = User::create($request->validated());

    // 잡을 큐에 쌓기
    SendWelcomeEmail::dispatch($user);

    return redirect('/dashboard');
}

지연 디스패치

delay() 메서드로 잡의 실행을 지정 시간 후로 늦출 수 있습니다.
// 5분 후에 실행
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));

dispatchAfterResponse()

dispatchAfterResponse()를 사용하면, HTTP 응답을 사용자에게 반환한 직후에 잡을 실행합니다. sync 드라이버에서도 동작하기 때문에, 전용 워커가 필요 없는 경량 용도에 적합합니다.
SendWelcomeEmail::dispatchAfterResponse($user);

특정 큐로의 디스패치

SendWelcomeEmail::dispatch($user)->onQueue('emails');

Queue Routing

특정 잡 클래스를 기본으로 정해진 접속·큐로 돌리려면, ServiceProvider의 boot()에서 Queue::route()를 사용합니다. 잡 클래스마다 onQueue() / onConnection()을 작성하는 대신 일원 관리할 수 있습니다.
use App\Concerns\RequiresVideo;
use App\Jobs\ProcessPodcast;
use App\Jobs\ProcessVideo;
use Illuminate\Support\Facades\Queue;

public function boot(): void
{
    Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts');
    Queue::route(RequiresVideo::class, queue: 'video');
}
인터페이스·트레이트·부모 클래스도 지정할 수 있습니다. 그것들을 구현·사용·상속하는 모든 잡에 자동으로 적용됩니다. 여러 잡을 한꺼번에 라우팅하는 경우에는 배열을 전달합니다.
Queue::route([
    ProcessPodcast::class => ['podcasts', 'redis'], // queue와 connection
    ProcessVideo::class => 'videos',                // queue만 (기본 접속을 사용)
]);
Queue Routing은 잡 측의 onQueue() / onConnection()으로 덮어쓸 수 있습니다.

동기 실행 (테스트·개발용)

dispatchSync()를 사용하면 큐를 경유하지 않고 즉시 실행합니다.
SendWelcomeEmail::dispatchSync($user);

벌크 디스패치

다수의 독립된 잡을 한 번에 디스패치하는 경우, Bus 파사드의 bulk() 메서드를 사용할 수 있습니다. 배치 처리와 같은 추적이나 콜백이 필요 없는 케이스에 최적입니다. Bus::bulk()는 잡을 설정된 큐 접속과 큐명으로 그룹화하고, 각 그룹을 한꺼번에 큐로 푸시하므로 효율적입니다.
use App\Jobs\ProcessUser;
use Illuminate\Support\Facades\Bus;

Bus::bulk(
    $users->map(fn ($user) => new ProcessUser($user))
);
Bus::bulk()는 잡을 배치로서 한꺼번에 큐에 송신합니다. 배치 처리(Bus::batch())와는 달리, 진행 추적이나 완료 콜백은 제공되지 않습니다. 독립된 대량의 잡을 심플하게 일괄 송신하고 싶은 경우에 적합합니다.

잡의 처리

queue:work 명령

큐 워커를 기동해 잡을 처리합니다.
php artisan queue:work
특정 드라이버나 큐를 지정할 수도 있습니다.
# Redis의 emails 큐만 처리
php artisan queue:work redis --queue=emails

# database 드라이버를 사용
php artisan queue:work database
queue:work는 기동한 채로 계속 동작합니다. 코드를 변경했을 때는 queue:restart로 워커를 재기동해 주세요. 프로덕션 환경에서는 Supervisor 등의 프로세스 매니저로 관리하는 것이 일반적입니다.

큐 워커의 모니터링 옵션

자주 사용하는 옵션을 조합해 워커를 세밀하게 제어할 수 있습니다.
php artisan queue:work --tries=3 --timeout=60 --sleep=3
옵션설명
--tries=N잡의 최대 시도 횟수. 넘으면 실패 잡으로 기록된다
--timeout=N잡 1건의 최대 실행 초수. 넘으면 워커를 강제 종료한다
--sleep=N큐가 비었을 때 다음 폴링까지 대기하는 초수 (기본값: 3)
--max-jobs=NN건 처리하면 워커를 종료한다
--max-time=NN초 경과하면 워커를 종료한다
--queue=A,B우선순위를 붙여 큐를 처리한다 (A를 우선)

잡 클래스에 리트라이 설정을 작성

커맨드라인 옵션보다, 잡 클래스 자체에 설정을 작성하는 편이 관리하기 쉬운 경우가 있습니다.
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\Timeout;

#[Tries(3)]
#[Timeout(60)]
class SendWelcomeEmail implements ShouldQueue
{
    use Queueable;

    // ...
}

잡의 릴리스 (Release 미들웨어)

특정 조건을 만족할 때에 잡을 실행하지 않고 큐로 되돌리고 싶은 경우, Release 미들웨어를 사용하면 간결하게 작성할 수 있습니다.
use Illuminate\Queue\Middleware\Release;

/**
 * 잡이 통과할 미들웨어를 반환
 */
public function middleware(): array
{
    return [
        // $condition이 true일 때 60초 후에 릴리스
        Release::when($this->order->isPending(), releaseAfter: 60),
    ];
}
Release::unless()는 조건이 false일 때에 릴리스합니다.
return [
    // 주문이 지불 완료가 아닐 때 60초 후에 릴리스
    Release::unless($this->order->isPaid(), releaseAfter: 60),
];
클로저를 사용하면 보다 복잡한 조건을 기술할 수 있습니다.
return [
    Release::when(function (): bool {
        return ! $this->order->isPaid();
    }, releaseAfter: 60),
];
잡을 릴리스해도 잡의 시도 횟수는 카운트업됩니다. #[Tries]$tries 프로퍼티를 적절히 설정해 주세요.

실패한 잡의 처리

failed_jobs 테이블 준비

잡이 최대 시도 횟수를 넘으면 failed_jobs 테이블에 기록됩니다. 테이블이 없는 경우는 다음 명령으로 작성합니다.
php artisan make:queue-failed-table
php artisan migrate

실패 시의 클린업

잡에 failed() 메서드를 정의하면, 실패했을 때의 뒷처리를 기술할 수 있습니다.
use Throwable;

public function failed(?Throwable $exception): void
{
    // 관리자에게 Slack 통지를 보내는 등
    // Notification::route('slack', config('app.slack_webhook'))
    //     ->notify(new JobFailedNotification($this, $exception));
}

예외에 의한 리트라이 정지

예외의 종류에 따라서는 리트라이시키지 않고 즉시 실패시키고 싶은 경우가 있습니다. bootstrap/app.phpwithExceptions() 내에서 dontRetry를 사용해 대상 예외 클래스를 지정합니다.
use App\Exceptions\InvalidPodcastSourceException;
use Illuminate\Foundation\Configuration\Exceptions;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontRetry([
        InvalidPodcastSourceException::class,
    ]);
})
보다 세밀한 제어가 필요한 경우는 dontRetryWhen에 클로저를 전달합니다. 클로저가 true를 반환하면 잡은 즉시 실패로서 마크되고 리트라이되지 않습니다.
use App\Exceptions\PodcastProcessingException;
use Illuminate\Foundation\Configuration\Exceptions;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontRetryWhen(function (PodcastProcessingException $e) {
        return $e->reason() === 'Subscription expired';
    });
})
검증 에러나 결제 실패(서브스크립션 기한 만료 등)처럼, 몇 번 리트라이해도 결과가 바뀌지 않는 예외는 이 방법으로 즉시 실패시키면 효율적입니다.

실패한 잡의 일람 확인

php artisan queue:failed

실패한 잡의 리트라이

# 특정한 잡 ID를 지정해 리트라이
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece

# 모든 실패 잡을 리트라이
php artisan queue:retry all

실패한 잡의 삭제

# 특정한 잡을 삭제
php artisan queue:forget ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece

# 모든 실패 잡을 삭제
php artisan queue:flush

자주 사용하는 큐 드라이버

database 드라이버

추가의 미들웨어 없이 사용하기 시작할 수 있는 심플한 드라이버입니다. jobs 테이블에 잡을 저장하고, 워커가 폴링하여 처리합니다.
  • 장점: 셋업이 간단, 기존의 RDBMS를 그대로 사용할 수 있음
  • 단점: 데이터베이스에의 부하가 높기 때문에, 대량의 잡에는 부적합
QUEUE_CONNECTION=database

redis 드라이버

프로덕션 환경에서 가장 자주 사용되는 고속 드라이버입니다. 인메모리로 동작하기 때문에 데이터베이스보다도 처리량이 높고, 대량의 잡을 처리할 수 있습니다.
  • 장점: 고속, 스케일 가능
  • 단점: Redis 서버의 준비가 필요
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
Redis 큐를 프로덕션 운용하는 경우에는 Laravel Horizon의 도입을 검토해 주세요. 아름다운 대시보드에서 잡의 상황을 실시간으로 모니터링할 수 있습니다.

Supervisor에 의한 프로덕션 운용

프로덕션 환경에서는 queue:work 프로세스가 어떠한 이유로 정지했을 때에 자동으로 재기동하는 구조가 필요합니다. Linux 환경에서는 Supervisor를 사용하는 것이 일반적입니다.
# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/your-app/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/your-app/storage/logs/worker.log
stopwaitsecs=3600
numprocs=2로 2개의 워커 프로세스를 병렬 기동합니다. 설정 후에 Supervisor를 재로드합니다.
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*

실천 예: 메일 발송을 큐로 처리

1

잡 클래스를 작성

php artisan make:job SendOrderConfirmation
2

잡의 처리를 구현

<?php

namespace App\Jobs;

use App\Models\Order;
use App\Mail\OrderConfirmed;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Support\Facades\Mail;

#[Tries(3)]
#[Timeout(30)]
class SendOrderConfirmation implements ShouldQueue
{
    use Queueable;

    public function __construct(
        public Order $order,
    ) {}

    public function handle(): void
    {
        Mail::to($this->order->user->email)
            ->send(new OrderConfirmed($this->order));
    }
}
3

컨트롤러로부터 디스패치

use App\Jobs\SendOrderConfirmation;

public function store(Request $request): RedirectResponse
{
    $order = Order::create($request->validated());

    SendOrderConfirmation::dispatch($order);

    return redirect()->route('orders.show', $order)
        ->with('success', '주문을 접수했습니다.');
}
4

워커를 기동

php artisan queue:work --tries=3 --timeout=30

정리

  • 메일·SMS 발송
  • 이미지·동영상의 리사이즈나 변환
  • 외부 API에의 리퀘스트
  • 리포트의 생성이나 CSV 익스포트
  • Webhook의 송신
.envQUEUE_CONNECTION=sync로 하면, 잡은 큐를 경유하지 않고 즉시 실행됩니다. 워커를 기동하지 않아도 동작 확인할 수 있기 때문에, 개발 중에는 편리합니다.
QUEUE_CONNECTION=sync
# 워커 기동
php artisan queue:work

# 워커 재기동 (배포 후)
php artisan queue:restart

# 실패 잡 일람
php artisan queue:failed

# 실패 잡을 모두 리트라이
php artisan queue:retry all

# 실패 잡을 모두 삭제
php artisan queue:flush
마지막 수정일 2026년 7월 13일