
# 미들웨어

미들웨어는 작업 실행을 양파형 파이프라인으로 감쌉니다. 각 미들웨어는 작업 전후에 로직을 실행하거나 실행을 조기에 종료하거나 예외를 처리할 수 있습니다. Queuety에는 기본 미들웨어 5개가 포함되어 있으며 `Contracts\Middleware` 인터페이스를 통해 사용자 정의 미들웨어를 지원합니다.

미들웨어는 [디스패치 가능한 작업 클래스](https://queuety.inline0.dev/ko/docs/jobs#dispatchable-job-classes)에 사용할 수 있습니다(`Contracts\Job`를 구현하고 `Dispatchable` 트레이트를 사용하는 클래스).

## `Contracts\Middleware` 인터페이스

```php
use Queuety\Contracts\Middleware;

interface Middleware {
    public function handle( object $job, \Closure $next ): void;
}
```

`$job` 매개변수는 처리 중인 작업 인스턴스입니다. `$next( $job )`을 호출해 파이프라인의 다음 미들웨어로 실행을 넘기세요(마지막 미들웨어인 경우 작업의 `handle()` 메서드로 넘깁니다).

## 작업에 미들웨어 추가하기

작업 클래스에 미들웨어 인스턴스 배열을 반환하는 `middleware()` 메서드를 정의하세요.

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;
use Queuety\Middleware\RateLimited;
use Queuety\Middleware\Timeout;

class SyncInventoryJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly string $sku,
    ) {}

    public function handle(): void {
        // Sync inventory for this SKU...
    }

    public function middleware(): array {
        return [
            new Timeout( seconds: 30 ),
            new RateLimited( max: 100, window: 60 ),
        ];
    }
}
```

## 파이프라인 실행 순서

미들웨어는 배열 순서대로 적용되며 첫 번째 미들웨어가 가장 바깥 계층이 됩니다. `middleware()` 메서드가 `[A, B, C]`을 반환하면 실행 순서는 다음과 같습니다.

```
A::handle() -> B::handle() -> C::handle() -> $job->handle()
```

각 미들웨어는 다음 미들웨어를 감쌉니다. `A`는 작업 전에 가장 먼저 실행되고 작업 후에 가장 마지막으로 실행되므로 시간 측정이나 try/catch 래퍼에 적합합니다.

## 사용자 정의 미들웨어 작성

`Contracts\Middleware` 인터페이스를 구현하세요.

```php
use Queuety\Contracts\Middleware;

class LogExecution implements Middleware {
    public function handle( object $job, \Closure $next ): void {
        $class = get_class( $job );
        error_log( "[Queuety] Starting: {$class}" );

        $start = microtime( true );

        try {
            $next( $job );
        } finally {
            $elapsed = round( microtime( true ) - $start, 3 );
            error_log( "[Queuety] Finished: {$class} in {$elapsed}s" );
        }
    }
}
```

작업에 적용하세요.

```php
public function middleware(): array {
    return [
        new LogExecution(),
    ];
}
```

### 실행 조기 종료

`$next()`를 호출하지 않으면 미들웨어가 작업 실행을 완전히 건너뛸 수 있습니다.

```php
class SkipOnMaintenanceMode implements Middleware {
    public function handle( object $job, \Closure $next ): void {
        if ( wp_is_maintenance_mode() ) {
            return; // Job silently skipped.
        }

        $next( $job );
    }
}
```

## 기본 제공 미들웨어

### `ThrottlesExceptions`

작업 핸들러에서 발생하는 예외를 제한해 작업 폭주를 방지합니다. 일정 시간 동안 핸들러에서 예외가 너무 많이 발생하면 해당 핸들러의 후속 작업은 처리되지 않고 `RuntimeException`가 발생합니다. 이렇게 하면 실패한 외부 서비스 때문에 여러 작업의 재시도 횟수가 한꺼번에 모두 소진되는 일을 막을 수 있습니다.

```php
use Queuety\Middleware\ThrottlesExceptions;

public function middleware(): array {
    return [
        new ThrottlesExceptions( max_attempts: 10, decay_minutes: 10 ),
    ];
}
```

| 매개변수 | 유형 | 기본값 | 설명 |
|---|---|---|---|
| `$max_attempts` | `int` | `10` | 감소 기간 내 허용되는 최대 예외 수 |
| `$decay_minutes` | `int` | `10` | 기간(분) |

예외 수는 Queuety의 잠금 테이블에서 핸들러 클래스별로 추적되며 모든 작업자 프로세스에서 공유됩니다. 임계값에 도달하면 미들웨어는 작업을 호출하지 않고 `RuntimeException`를 발생시킵니다. 그러면 작업자는 이를 일반 실패와 동일하게 처리합니다. 시도 횟수는 `max_attempts`에 반영되고, 작업은 일반적인 백오프 간격으로 재시도되며, 시도 횟수를 모두 소진하면 보관 상태가 됩니다.

예: 외부 API가 다운되면 처음 10번의 실패는 정상적으로 처리되며 매번 예외 카운터가 증가합니다. 새 예외가 발생하지 않은 채 10분이 지날 때까지 후속 시도는 API를 호출하지 않고 빠르게 실패합니다. 기록된 예외가 발생할 때마다 기간이 다시 시작됩니다.

### `RateLimited`

일정 시간 내 실행 횟수의 최대값을 적용합니다. 미들웨어 내에서 한도를 초과하면 `RateLimitExceededException`가 발생하며, 작업자는 이를 일반적인 실패 시도처럼 처리해 일반 백오프 간격으로 재시도하고 `max_attempts`에 반영합니다. 미들웨어가 작업자의 속도 제한기에 한도를 등록하면 한도에 도달한 동안 이후 해당 핸들러의 작업도 실행 전에 지연 없이 `pending`로 되돌아갑니다([속도 제한](https://queuety.inline0.dev/ko/docs/features/rate-limiting) 참고).

```php
use Queuety\Middleware\RateLimited;

public function middleware(): array {
    return [
        new RateLimited( max: 60, window: 60 ), // 60 per minute
    ];
}
```

| 매개변수 | 유형 | 설명 |
|---|---|---|
| `$max` | `int` | 기간 내 허용되는 최대 실행 횟수 |
| `$window` | `int` | 기간(초) |

속도 제한기는 Queuety의 내부 `RateLimiter` 인스턴스를 사용하므로 모든 작업자 프로세스에 한도가 적용됩니다.

### `Timeout`

`pcntl_alarm`를 사용해 작업별 실행 시간 제한을 설정하고 작업이 너무 오래 실행되면 `TimeoutException`를 발생시키도록 설계되었습니다. 현재는 시간 제한이 적용되지 않습니다([#10](https://github.com/inline0/queuety/issues/10)). Queuety가 비동기 신호를 활성화하지 않으므로 작업 실행 중에는 알람 핸들러가 작동하지 않습니다. 또한 미들웨어가 반환될 때 알람을 초기화해 작업자 자체의 작업 시간 제한 알람을 취소합니다.

```php
use Queuety\Middleware\Timeout;

public function middleware(): array {
    return [
        new Timeout( seconds: 30 ),
    ];
}
```

| 매개변수 | 유형 | 설명 |
|---|---|---|
| `$seconds` | `int` | 최대 실행 시간(초) |

`pcntl` PHP 확장 기능이 필요합니다. `pcntl`를 사용할 수 없으면 미들웨어는 아무 동작 없이 넘어갑니다(시간 제한은 적용되지 않음).

### `UniqueJob`

같은 키를 가진 작업이 동시에 실행되는 것을 방지합니다. 데이터베이스 잠금 테이블을 사용해 한 번에 하나의 인스턴스만 실행되도록 합니다. 잠금이 이미 설정된 경우 작업을 조용히 건너뛰며 재시도하지 않습니다.

```php
use Queuety\Middleware\UniqueJob;

public function middleware(): array {
    return [
        new UniqueJob(),                         // Lock key defaults to the job class name
        new UniqueJob( key: 'sync_inventory' ),   // Custom lock key
    ];
}
```

| 매개변수 | 유형 | 기본값 | 설명 |
|---|---|---|---|
| `$key` | `string\|null` | `null`(클래스 이름 사용) | 고유성을 위한 잠금 키 |

### `WithoutOverlapping`

같은 논리 키를 공유하는 작업의 중복 실행을 방지하며, 작업자 충돌로 인한 교착 상태를 막기 위해 잠금이 자동으로 만료됩니다. `UniqueJob`와 비슷하지만 시간 초과 후 잠금이 자동 해제되어야 하는 장시간 실행 작업을 위해 설계되었습니다.

```php
use Queuety\Middleware\WithoutOverlapping;

public function middleware(): array {
    return [
        new WithoutOverlapping( key: 'import_products', release_after: 600 ),
    ];
}
```

| 매개변수 | 유형 | 기본값 | 설명 |
|---|---|---|---|
| `$key` | `string` | (필수) | 논리 작업 키 |
| `$release_after` | `int` | `300` | 자동 해제 전 최대 잠금 시간(초) |

잠금이 이미 설정된 경우 작업을 조용히 건너뜁니다. 만료된 잠금은 잠금을 획득할 때마다 자동으로 정리됩니다.

## 예시: 미들웨어 조합

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;
use Queuety\Middleware\RateLimited;
use Queuety\Middleware\Timeout;
use Queuety\Middleware\WithoutOverlapping;

class ImportProductsJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly int $supplier_id,
    ) {}

    public function handle(): void {
        // Fetch and import products from the supplier...
    }

    public function middleware(): array {
        return [
            new Timeout( seconds: 300 ),
            new WithoutOverlapping(
                key: "import_supplier_{$this->supplier_id}",
                release_after: 600,
            ),
            new RateLimited( max: 5, window: 60 ),
        ];
    }
}
```

이 작업은 다음과 같이 동작합니다.

1. 5분 넘게 실행되면 종료됩니다(Timeout)
2. 같은 공급업체의 다른 가져오기가 이미 실행 중이면 건너뜁니다(WithoutOverlapping)
3. 모든 공급업체를 통틀어 분당 가져오기를 5회로 제한합니다(RateLimited)
