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

# 큐 잡 실행 제어

> ShouldBeUnique·ShouldBeUniqueUntilProcessing·DebounceFor를 사용하여 큐 잡의 중복 실행·연속 디스패치를 제어하는 방법을 설명합니다.

## 개요

Laravel의 큐 기능에는 잡의 **중복 제거(Unique)** 와 **디바운스(Debounce)** 라는 두 가지 실행 제어 방식이 준비되어 있습니다. 둘 다 "같은 잡을 여러 번 디스패치했을 때 불필요한 실행을 줄이기 위한" 메커니즘이지만 동작 방식이 다릅니다.

| 기능                      | 인터페이스 / 어트리뷰트                   | 목적                               |
| ----------------------- | ------------------------------- | -------------------------------- |
| Unique Jobs             | `ShouldBeUnique`                | 큐에 동일한 잡이 하나만 존재하는 상태를 유지        |
| Unique Until Processing | `ShouldBeUniqueUntilProcessing` | 처리 시작까지만 유니크 제약을 유지              |
| Debounced Jobs          | `#[DebounceFor]`                | 짧은 시간 안에 연속 디스패치될 경우, 마지막 1건만 실행 |

<Warning>
  Unique Jobs와 Debounced Jobs는 **상호 배타적**입니다. `DebounceFor` 어트리뷰트를 사용하는 잡에 `ShouldBeUnique`를 구현하지 마세요.
</Warning>

***

## Unique Jobs — `ShouldBeUnique`

동일한 잡이 큐에 존재하는 동안에는 추가 디스패치를 무시합니다.

```php theme={null}
<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;

class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    // 추가 메서드는 불필요
}
```

`UpdateSearchIndex`가 큐에 쌓여 있는(또는 처리 중인) 동안, 동일한 잡을 디스패치하려고 해도 무시됩니다.

### 키로 유니크 제약 좁히기 — `UniqueFor` + `uniqueId()`

동일한 잡 클래스라도 "상품 A 업데이트"와 "상품 B 업데이트"를 별개의 잡으로 취급하고 싶다면, `uniqueId()` 메서드로 키를 정의합니다.

```php theme={null}
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Queue\Attributes\UniqueFor;

#[UniqueFor(3600)] // 1시간 후 락 자동 해제
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    public function __construct(public readonly int $productId)
    {
    }

    public function uniqueId(): string
    {
        return (string) $this->productId;
    }
}
```

* `uniqueId()`가 반환하는 값이 캐시 락의 키가 됩니다.
* `#[UniqueFor(초)]`를 지정하면 해당 시간이 경과한 후 락이 자동으로 해제됩니다(잡이 처리되지 않았을 경우의 페일세이프).

### 캐시 드라이버 지정 — `uniqueVia()`

기본 캐시 드라이버 외의 것을 사용하고 싶다면 `uniqueVia()`를 구현합니다.

```php theme={null}
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

public function uniqueVia(): Repository
{
    return Cache::driver('redis');
}
```

<Info>
  Unique Jobs는 원자적 락을 지원하는 캐시 드라이버(`redis`, `database`, `memcached`, `dynamodb`, `file`, `array`)가 필요합니다.
</Info>

***

## `ShouldBeUnique` vs `ShouldBeUniqueUntilProcessing`

`ShouldBeUnique`의 락은 **잡이 완료되거나 재시도 상한에 도달할 때까지** 유지됩니다. 이것이 문제가 되는 경우가 있습니다.

**예:** 큐에 `UpdateSearchIndex(product_id: 42)`가 1건 있고, 워커가 처리를 시작한 직후 동일한 잡을 다시 디스패치하고 싶은 경우. `ShouldBeUnique`에서는 처리 완료까지 2건째가 큐에 들어가지 않습니다.

이럴 때는 `ShouldBeUniqueUntilProcessing`을 사용합니다. 락이 **처리 시작 직전에 해제**되므로, 워커가 잡을 꺼내는 순간 다음 디스패치가 가능해집니다.

```php theme={null}
<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;

class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
    // ...
}
```

```mermaid theme={null}
sequenceDiagram
    participant D as 디스패처
    participant Q as 큐
    participant W as 워커

    D->>Q: dispatch() — 락 획득
    D->>Q: dispatch() — 락 있음 → 무시

    note over Q,W: ShouldBeUnique의 경우
    W->>Q: 잡 꺼냄
    W->>W: 처리 중 (락 유지)
    D->>Q: dispatch() — 락 있음 → 무시
    W->>W: 처리 완료 — 락 해제
    D->>Q: dispatch() — 여기서 비로소 큐잉 가능

    note over Q,W: ShouldBeUniqueUntilProcessing의 경우
    W->>Q: 잡 꺼냄 — 락 해제
    D->>Q: dispatch() — 락 없음 → 큐잉 가능
    W->>W: 처리 중
```

### 비교 정리

|              | `ShouldBeUnique` | `ShouldBeUniqueUntilProcessing` |
| ------------ | ---------------- | ------------------------------- |
| 락 해제 시점      | 처리 완료 / 실패 후     | 처리 시작 직전                        |
| 처리 중 중복 디스패치 | 무시               | 큐잉 가능                           |
| 유즈케이스        | 동시 실행을 완전히 막고 싶음 | 처리 후 즉시 다음 잡을 넣고 싶음             |

***

## Debounced Jobs — `#[DebounceFor]`

<Info>
  `DebounceFor` 어트리뷰트는 Laravel 13에서 추가된 기능입니다.
</Info>

짧은 시간에 같은 잡이 대량으로 디스패치될 경우, **마지막에 디스패치된 1건만** 실행합니다. 웹 프런트엔드의 디바운스와 같은 개념입니다.

```php theme={null}
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\DebounceFor;

#[DebounceFor(30)] // 30초 이내의 재디스패치는 무시 (최신 1건만 실행)
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;

    public function __construct(public readonly int $productId)
    {
    }

    public function debounceId(): string
    {
        return (string) $this->productId;
    }
}
```

* `debounceId()`가 반환하는 값으로 잡을 식별합니다(상품 ID별로 독립적인 디바운스가 적용됨).
* 30초 동안 같은 `productId`로 10번 디스패치되어도 마지막 1건만 실행됩니다.

### `maxWait` — 최대 대기 시간 상한

자주 갱신되는 데이터에서는 디바운스가 계속 이어져 잡이 영원히 실행되지 않을 가능성이 있습니다. `maxWait`로 최대 지연 시간을 설정할 수 있습니다.

```php theme={null}
#[DebounceFor(30, maxWait: 120)]
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;
    // ...
}
```

이 예에서는 최초 디스패치로부터 최대 120초 후에는 반드시 실행됩니다(30초 디바운스가 이어져도 120초에 타임아웃).

### 캐시 드라이버 지정 — `debounceVia()`

```php theme={null}
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

public function debounceVia(): Repository
{
    return Cache::driver('redis');
}
```

### `JobDebounced` 이벤트

후속 디스패치에 의해 덮어쓰인 잡은 `Illuminate\Queue\Events\JobDebounced` 이벤트를 발생시키고 큐에서 제거됩니다. 이 이벤트를 리스닝함으로써 디바운스된 잡의 추적과 모니터링이 가능합니다.

***

## 어느 것을 사용해야 할까

```mermaid theme={null}
flowchart TD
    A["같은 잡을 여러 번 디스패치할 가능성 있음"] --> B{"짧은 시간에 연속 디스패치<br>마지막 1건만 실행하고 싶음?"}
    B -->|Yes| C["#[DebounceFor]"]
    B -->|No| D{"큐에 1건만<br>존재시키고 싶음?"}
    D -->|Yes| E{"처리 중에도 중복을 막고 싶음?"}
    E -->|Yes| F["ShouldBeUnique"]
    E -->|No| G["ShouldBeUniqueUntilProcessing"]
    D -->|No| H["일반 잡"]
```

| 유즈케이스                           | 권장                              |
| ------------------------------- | ------------------------------- |
| 같은 처리가 큐에 2건 이상 있어도 의미가 없음      | `ShouldBeUnique`                |
| 처리 중도 포함하여 병렬 실행을 막고 싶음         | `ShouldBeUnique`                |
| 워커가 꺼내면 즉시 다음 잡을 넣고 싶음          | `ShouldBeUniqueUntilProcessing` |
| 사용자가 저장 버튼을 연타해도 한 번만 실행        | `#[DebounceFor]`                |
| 모델 업데이트마다 검색 인덱스 재구축(대량 업데이트 시) | `#[DebounceFor]` + `maxWait`    |

***

## 내부 구현

### Unique Jobs의 락 메커니즘

`ShouldBeUnique` 잡이 디스패치되면, Laravel은 내부적으로 캐시의 [원자적 락](/ko/cache#원자적-작업락)을 획득합니다. 락 키는 다음 형식입니다:

```
laravel_unique_job:{잡클래스명}:{uniqueId()}
```

락을 획득할 수 없는 경우(이미 다른 잡이 보유 중), 잡은 큐에 추가되지 않습니다.

### Debounced Jobs의 구현

`DebounceFor`는 내부적으로 "디바운스 윈도"를 관리하는 캐시 엔트리를 사용합니다. 새로운 디스패치가 올 때마다:

1. 기존 잡을 큐에서 삭제(`JobDebounced` 이벤트 발생)
2. 새로운 잡을 큐에 추가(디바운스 초 지연 포함)
3. 캐시 타이머 리셋

`maxWait`가 지정된 경우, 최초 디스패치의 타임스탬프도 기록하여 그 시각으로부터 `maxWait`초를 초과하는 디바운스를 방지합니다.

***

## 참고 링크

* [Laravel 공식 문서 — Unique Jobs](https://laravel.com/docs/queues#unique-jobs)
* [Laravel 공식 문서 — Debounced Jobs](https://laravel.com/docs/queues#debounced-jobs)
* [`Illuminate\Contracts\Queue\ShouldBeUnique`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Contracts/Queue/ShouldBeUnique.php)
* [`Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Contracts/Queue/ShouldBeUniqueUntilProcessing.php)
* [`Illuminate\Queue\Attributes\DebounceFor`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Queue/Attributes/DebounceFor.php)
* [`Illuminate\Queue\Attributes\UniqueFor`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Queue/Attributes/UniqueFor.php)


## Related topics

- [큐와 잡](/ko/queues.md)
- [Laravel Nightwatch 입문](/ko/blog/nightwatch-introduction.md)
- [Context(컨텍스트)](/ko/context.md)
- [Laravel Horizon](/ko/horizon.md)
- [Laravel 13 신기능 정리](/ko/blog/laravel-13-new-features.md)
