> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 큐와 잡

> Laravel의 큐(Queue)와 잡(Job)을 사용해, 메일 발송이나 이미지 처리 등 무거운 처리를 비동기로 실행하는 방법을 설명합니다.

## 큐란

웹 애플리케이션에서는 메일 발송·이미지 리사이즈·외부 API에의 문의 등, 완료까지 수 초가 걸리는 처리가 발생할 수 있습니다.
이를 HTTP 리퀘스트 중에 동기적으로 수행하면, 사용자는 응답이 반환될 때까지 계속 기다려야 합니다.

Laravel의 큐를 사용하면, 이러한 무거운 처리를 **백그라운드에서 비동기로 실행**할 수 있습니다.
리퀘스트는 즉시 응답을 반환하고, 실제의 처리는 워커 프로세스가 별도로 처리해 줍니다.

<Info>
  큐는 데이터베이스·Redis·Amazon SQS 등 여러 백엔드에 대응하고 있습니다.
  개발 환경에서는 `sync` 드라이버를 사용하면, 큐를 사용하지 않고 잡을 즉시 실행할 수 있습니다.
</Info>

```mermaid theme={null}
flowchart LR
    A["잡 작성<br>dispatch()"] --> B["큐 드라이버<br>(Redis/DB 등)"]
    B --> C["워커<br>queue:work"]
    C --> D{"실행 성공?"}
    D -->|"Yes"| E["완료"]
    D -->|"No"| F{"리트라이 가능?"}
    F -->|"Yes"| B
    F -->|"No"| G["실패 기록<br>failed_jobs"]
```

## 큐 설정

### config/queue.php

큐의 설정은 `config/queue.php`에 집약되어 있습니다.
`QUEUE_CONNECTION` 환경 변수로 사용할 드라이버를 전환합니다.

```php theme={null}
// config/queue.php
'default' => env('QUEUE_CONNECTION', 'database'),
```

### .env 설정

```ini theme={null}
# 드라이버 선택
QUEUE_CONNECTION=database

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

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

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

```shell theme={null}
php artisan make:queue-table
php artisan migrate
```

### Redis 드라이버 준비

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

```shell theme={null}
composer require predis/predis
```

### SQS Overflow Storage

Amazon SQS는 메시지 페이로드의 최대 크기에 제한이 있습니다.
페이로드가 커지는 잡을 다루는 경우에는, 초과분을 캐시 스토어에 저장하고, SQS에는 포인터만 보내는 설정을 추가합니다.

```php theme={null}
'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 이상**의 페이로드를 지정한 캐시 스토어에 저장합니다.
* `always`를 `true`로 하면 크기에 관계없이 모든 SQS 페이로드를 캐시 스토어에 저장합니다.
* `delete_after_processing`은 잡 성공 후에 저장된 페이로드를 삭제합니다(기본값 `true`).
* `flush_on_clear`를 `true`로 하면 `queue:clear` 실행 시에 overflow용 스토어를 `flush`합니다. 통상의 캐시를 지우지 않도록, 전용 스토어와 조합해 사용해 주세요.

## 잡 클래스 작성

### make:job 명령

`make:job` Artisan 명령으로 잡 클래스의 템플릿을 생성합니다.

```shell theme={null}
php artisan make:job SendWelcomeEmail
```

`app/Jobs/SendWelcomeEmail.php`가 생성됩니다.

### 잡 클래스의 구조

```php theme={null}
<?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` 트레이트가 잡의 큐 조작에 필요한 메서드를 제공합니다.

<Tip>
  컨스트럭터에 Eloquent 모델을 전달하면, Laravel은 자동적으로 ID만을 시리얼라이즈합니다.
  실행 시에 데이터베이스로부터 최신 데이터를 다시 취득하기 때문에, 큐의 페이로드가 가벼워집니다.
</Tip>

## 잡의 디스패치

### dispatch()

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

```php theme={null}
use App\Jobs\SendWelcomeEmail;

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

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

    return redirect('/dashboard');
}
```

### 지연 디스패치

`delay()` 메서드로 잡의 실행을 지정 시간 후로 늦출 수 있습니다.

```php theme={null}
// 5분 후에 실행
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));
```

### dispatchAfterResponse()

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

```php theme={null}
SendWelcomeEmail::dispatchAfterResponse($user);
```

### 특정 큐로의 디스패치

```php theme={null}
SendWelcomeEmail::dispatch($user)->onQueue('emails');
```

### Queue Routing

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

```php theme={null}
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');
}
```

인터페이스·트레이트·부모 클래스도 지정할 수 있습니다. 그것들을 구현·사용·상속하는 모든 잡에 자동으로 적용됩니다.

여러 잡을 한꺼번에 라우팅하는 경우에는 배열을 전달합니다.

```php theme={null}
Queue::route([
    ProcessPodcast::class => ['podcasts', 'redis'], // queue와 connection
    ProcessVideo::class => 'videos',                // queue만 (기본 접속을 사용)
]);
```

<Info>
  Queue Routing은 잡 측의 `onQueue()` / `onConnection()`으로 덮어쓸 수 있습니다.
</Info>

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

`dispatchSync()`를 사용하면 큐를 경유하지 않고 즉시 실행합니다.

```php theme={null}
SendWelcomeEmail::dispatchSync($user);
```

### 벌크 디스패치

다수의 독립된 잡을 한 번에 디스패치하는 경우, `Bus` 파사드의 `bulk()` 메서드를 사용할 수 있습니다. 배치 처리와 같은 추적이나 콜백이 필요 없는 케이스에 최적입니다.

`Bus::bulk()`는 잡을 설정된 큐 접속과 큐명으로 그룹화하고, 각 그룹을 한꺼번에 큐로 푸시하므로 효율적입니다.

```php theme={null}
use App\Jobs\ProcessUser;
use Illuminate\Support\Facades\Bus;

Bus::bulk(
    $users->map(fn ($user) => new ProcessUser($user))
);
```

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

## 잡의 처리

### queue:work 명령

큐 워커를 기동해 잡을 처리합니다.

```shell theme={null}
php artisan queue:work
```

특정 드라이버나 큐를 지정할 수도 있습니다.

```shell theme={null}
# Redis의 emails 큐만 처리
php artisan queue:work redis --queue=emails

# database 드라이버를 사용
php artisan queue:work database
```

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

## 큐 워커의 모니터링 옵션

자주 사용하는 옵션을 조합해 워커를 세밀하게 제어할 수 있습니다.

```shell theme={null}
php artisan queue:work --tries=3 --timeout=60 --sleep=3
```

| 옵션             | 설명                                |
| -------------- | --------------------------------- |
| `--tries=N`    | 잡의 최대 시도 횟수. 넘으면 실패 잡으로 기록된다      |
| `--timeout=N`  | 잡 1건의 최대 실행 초수. 넘으면 워커를 강제 종료한다   |
| `--sleep=N`    | 큐가 비었을 때 다음 폴링까지 대기하는 초수 (기본값: 3) |
| `--max-jobs=N` | N건 처리하면 워커를 종료한다                  |
| `--max-time=N` | N초 경과하면 워커를 종료한다                  |
| `--queue=A,B`  | 우선순위를 붙여 큐를 처리한다 (A를 우선)          |

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

커맨드라인 옵션보다, 잡 클래스 자체에 설정을 작성하는 편이 관리하기 쉬운 경우가 있습니다.

```php theme={null}
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` 미들웨어를 사용하면 간결하게 작성할 수 있습니다.

```php theme={null}
use Illuminate\Queue\Middleware\Release;

/**
 * 잡이 통과할 미들웨어를 반환
 */
public function middleware(): array
{
    return [
        // $condition이 true일 때 60초 후에 릴리스
        Release::when($this->order->isPending(), releaseAfter: 60),
    ];
}
```

`Release::unless()`는 조건이 `false`일 때에 릴리스합니다.

```php theme={null}
return [
    // 주문이 지불 완료가 아닐 때 60초 후에 릴리스
    Release::unless($this->order->isPaid(), releaseAfter: 60),
];
```

클로저를 사용하면 보다 복잡한 조건을 기술할 수 있습니다.

```php theme={null}
return [
    Release::when(function (): bool {
        return ! $this->order->isPaid();
    }, releaseAfter: 60),
];
```

<Warning>
  잡을 릴리스해도 잡의 시도 횟수는 카운트업됩니다. `#[Tries]`나 `$tries` 프로퍼티를 적절히 설정해 주세요.
</Warning>

## 실패한 잡의 처리

### failed\_jobs 테이블 준비

잡이 최대 시도 횟수를 넘으면 `failed_jobs` 테이블에 기록됩니다.
테이블이 없는 경우는 다음 명령으로 작성합니다.

```shell theme={null}
php artisan make:queue-failed-table
php artisan migrate
```

### 실패 시의 클린업

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

```php theme={null}
use Throwable;

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

### 예외에 의한 리트라이 정지

예외의 종류에 따라서는 리트라이시키지 않고 즉시 실패시키고 싶은 경우가 있습니다. `bootstrap/app.php`의 `withExceptions()` 내에서 `dontRetry`를 사용해 대상 예외 클래스를 지정합니다.

```php theme={null}
use App\Exceptions\InvalidPodcastSourceException;
use Illuminate\Foundation\Configuration\Exceptions;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontRetry([
        InvalidPodcastSourceException::class,
    ]);
})
```

보다 세밀한 제어가 필요한 경우는 `dontRetryWhen`에 클로저를 전달합니다. 클로저가 `true`를 반환하면 잡은 즉시 실패로서 마크되고 리트라이되지 않습니다.

```php theme={null}
use App\Exceptions\PodcastProcessingException;
use Illuminate\Foundation\Configuration\Exceptions;

->withExceptions(function (Exceptions $exceptions): void {
    $exceptions->dontRetryWhen(function (PodcastProcessingException $e) {
        return $e->reason() === 'Subscription expired';
    });
})
```

<Tip>
  검증 에러나 결제 실패(서브스크립션 기한 만료 등)처럼, 몇 번 리트라이해도 결과가 바뀌지 않는 예외는 이 방법으로 즉시 실패시키면 효율적입니다.
</Tip>

### 실패한 잡의 일람 확인

```shell theme={null}
php artisan queue:failed
```

### 실패한 잡의 리트라이

```shell theme={null}
# 특정한 잡 ID를 지정해 리트라이
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece

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

### 실패한 잡의 삭제

```shell theme={null}
# 특정한 잡을 삭제
php artisan queue:forget ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece

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

## 자주 사용하는 큐 드라이버

### database 드라이버

추가의 미들웨어 없이 사용하기 시작할 수 있는 심플한 드라이버입니다.
`jobs` 테이블에 잡을 저장하고, 워커가 폴링하여 처리합니다.

* **장점**: 셋업이 간단, 기존의 RDBMS를 그대로 사용할 수 있음
* **단점**: 데이터베이스에의 부하가 높기 때문에, 대량의 잡에는 부적합

```ini theme={null}
QUEUE_CONNECTION=database
```

### redis 드라이버

프로덕션 환경에서 가장 자주 사용되는 고속 드라이버입니다.
인메모리로 동작하기 때문에 데이터베이스보다도 처리량이 높고, 대량의 잡을 처리할 수 있습니다.

* **장점**: 고속, 스케일 가능
* **단점**: Redis 서버의 준비가 필요

```ini theme={null}
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
```

<Tip>
  Redis 큐를 프로덕션 운용하는 경우에는 [Laravel Horizon](https://laravel.com/docs/horizon)의 도입을 검토해 주세요.
  아름다운 대시보드에서 잡의 상황을 실시간으로 모니터링할 수 있습니다.
</Tip>

## Supervisor에 의한 프로덕션 운용

프로덕션 환경에서는 `queue:work` 프로세스가 어떠한 이유로 정지했을 때에 자동으로 재기동하는 구조가 필요합니다.
Linux 환경에서는 **Supervisor**를 사용하는 것이 일반적입니다.

```ini theme={null}
# /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를 재로드합니다.

```shell theme={null}
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
```

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

<Steps>
  <Step title="잡 클래스를 작성">
    ```shell theme={null}
    php artisan make:job SendOrderConfirmation
    ```
  </Step>

  <Step title="잡의 처리를 구현">
    ```php theme={null}
    <?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));
        }
    }
    ```
  </Step>

  <Step title="컨트롤러로부터 디스패치">
    ```php theme={null}
    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', '주문을 접수했습니다.');
    }
    ```
  </Step>

  <Step title="워커를 기동">
    ```shell theme={null}
    php artisan queue:work --tries=3 --timeout=30
    ```
  </Step>
</Steps>

## 정리

<AccordionGroup>
  <Accordion title="큐를 사용해야 할 타이밍">
    * 메일·SMS 발송
    * 이미지·동영상의 리사이즈나 변환
    * 외부 API에의 리퀘스트
    * 리포트의 생성이나 CSV 익스포트
    * Webhook의 송신
  </Accordion>

  <Accordion title="개발 시의 힌트">
    `.env`로 `QUEUE_CONNECTION=sync`로 하면, 잡은 큐를 경유하지 않고 즉시 실행됩니다.
    워커를 기동하지 않아도 동작 확인할 수 있기 때문에, 개발 중에는 편리합니다.

    ```ini theme={null}
    QUEUE_CONNECTION=sync
    ```
  </Accordion>

  <Accordion title="자주 사용하는 명령 정리">
    ```shell theme={null}
    # 워커 기동
    php artisan queue:work

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

    # 실패 잡 일람
    php artisan queue:failed

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

    # 실패 잡을 모두 삭제
    php artisan queue:flush
    ```
  </Accordion>
</AccordionGroup>


## Related topics

- [병행 처리](/ko/concurrency.md)
- [Laravel Horizon](/ko/horizon.md)
- [PHP 어트리뷰트](/ko/advanced/php-attributes.md)
- [브로드캐스팅](/ko/broadcasting.md)
- [이벤트와 리스너](/ko/events.md)
