> ## 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 13의 이벤트 등록과 event:cache 구현을 읽어 보고, 패키지의 명시적 등록, 자동 감지와의 이중 등록, 의존성 해결, 업데이트 시 검증 방법을 해설합니다.

패키지에 이벤트 기반 확장 지점을 마련하려면 **이벤트 클래스 제공**과 **리스너 등록**을 나누어 설계합니다. 사용자가 `vendor` 내의 디렉터리를 자동 감지 대상에 추가하지 않아도 동작하는 구성으로 만들면, 설치·업데이트·캐시 생성 시의 전제 조건을 줄일 수 있습니다.

이 페이지에서는 Laravel Framework `v13.36.0`의 구현을 확인하고, [이벤트](/ko/events)의 기본 조작을 패키지의 장기 유지보수에 적용합니다. 프로바이더 로드는 [패키지 자동 감지](/ko/advanced/package-discovery)를, 테스트 환경은 [패키지 테스트](/ko/advanced/package-testing)를 참조하세요.

## 패키지의 리스너를 명시적으로 등록하기

패키지가 기본으로 실행하는 처리는 일반적인 `Illuminate\Support\ServiceProvider`의 `boot()`에서 `Event::listen()`으로 등록할 수 있습니다. Laravel의 기본 리스너 자동 감지 대상은 애플리케이션의 `app/Listeners`이며, 패키지 내의 `src/Listeners`까지 자동으로 탐색하지는 않습니다.

예로, 배송 완료 이벤트를 로그에 기록하는 패키지를 만듭니다. 이벤트는 알림에 필요한 값만 가지는 클래스입니다.

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

namespace Acme\Courier\Events;

final class ShipmentDelivered
{
    public function __construct(public readonly string $shipmentId)
    {
    }
}
```

리스너는 생성자에서 로거를 받고, `handle()`에서 이벤트를 처리합니다.

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

namespace Acme\Courier\Listeners;

use Acme\Courier\Events\ShipmentDelivered;
use Psr\Log\LoggerInterface;

final class LogDeliveredShipment
{
    public function __construct(private readonly LoggerInterface $logger)
    {
    }

    public function handle(ShipmentDelivered $event): void
    {
        $this->logger->info('Shipment delivered', [
            'shipment_id' => $event->shipmentId,
        ]);
    }
}
```

프로바이더에는 이벤트와 리스너의 클래스 이름을 등록합니다.

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

namespace Acme\Courier;

use Acme\Courier\Events\ShipmentDelivered;
use Acme\Courier\Listeners\LogDeliveredShipment;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Event::listen(
            ShipmentDelivered::class,
            LogDeliveredShipment::class,
        );
    }
}
```

이 구성은 프로바이더 자체가 로드된다는 것을 전제로 합니다. Composer의 `extra.laravel.providers` 또는 사용자에 의한 명시적 등록을 준비하세요. 이벤트가 HTTP 처리와 큐 워커 양쪽에서 필요하다면 `runningInConsole()`로 등록을 제한하지 않습니다.

<Warning>
  리스너 등록만을 이유로 이 프로바이더에 `DeferrableProvider`를 구현하지 마세요. 지연 대상 서비스가 해결되지 않으면 리스너도 등록되지 않습니다. 무거운 서비스 바인딩은 필요에 따라 별도의 [지연 프로바이더](/ko/advanced/deferred-provider)로 분리합니다.
</Warning>

## 등록과 인스턴스 생성은 다른 시점

`Dispatcher::listen()`은 일반적인 이벤트 이름에 대한 리스너를 내부 배열에 추가합니다. 위와 같이 클래스 이름을 전달하는 경우, 이 시점에서는 리스너의 인스턴스를 생성하지 않습니다.

동기 리스너에서는 이벤트 발생 후 `createClassCallable()`이 컨테이너의 `make()`를 사용해 클래스를 해결합니다. 따라서 생성자의 의존성 주입을 이용할 수 있습니다.

```mermaid theme={null}
flowchart TD
    A["프로바이더의 boot()"] --> B["Event::listen()으로 클래스 이름 저장"]
    B --> C["ShipmentDelivered 발생"]
    C --> D["Dispatcher가 리스너 선택"]
    D --> E["컨테이너로 동기 리스너 해결"]
    E --> F["handle()에 이벤트 전달"]
```

이 경로는 `handle()`에 대해 컨테이너의 `call()`을 수행하지 않습니다. **임의의 서비스는 생성자에 주입하고, `handle()`에서는 이벤트를 받는** 설계로 만듭니다. [Artisan 명령어의 `handle()`에 대한 의존성 주입](/ko/advanced/package-commands)과 같은 구조라고 생각하지 마세요.

여기서 설명하는 것은 동기 리스너입니다. `ShouldQueue`를 구현하는 경우에는 큐용 별도 경로가 되므로, 트랜잭션, 재시도, 의존성 해결에 대해 [이벤트의 큐 처리](/ko/events)와 [큐](/ko/queues)도 확인합니다.

## event:cache는 명시적 등록을 막지 않는다

`event:cache`는 이벤트 Dispatcher에 등록된 모든 리스너를 통째로 직렬화하는 명령어가 아닙니다.

`EventCacheCommand::getEvents()`는 `Illuminate\Foundation\Support\Providers\EventServiceProvider`와 그 파생 프로바이더를 열거하고, 각 프로바이더의 자동 감지 결과와 `listens()`의 매핑을 저장합니다. 일반적인 `ServiceProvider::boot()`에 작성한 `Event::listen()`은 이 수집 대상에 포함되지 않습니다.

한편, 캐시가 있는 부팅에서도 일반 프로바이더의 `boot()`는 실행됩니다. **캐시에 포함되지 않는 것과, 캐시가 있으면 등록되지 않는 것은 별개입니다.**

| 등록 방법 | `event:cache`에 저장되는 내용 | 캐시가 있는 부팅 시 |
| - | - | - |
| 일반 프로바이더의 `boot()`에서 `Event::listen()` | 이 등록 자체는 수집 대상 외 | `boot()`에서 다시 등록한다 |
| Foundation `EventServiceProvider`의 `$listen` | 프로바이더 클래스 이름별로 매핑을 저장 | `getEvents()`가 캐시를 읽는다 |
| 자동 감지 대상이 된 리스너 | 감지 결과를 매핑에 저장 | 디렉터리 재탐색이 아닌 캐시를 사용한다 |

Foundation의 `EventServiceProvider`를 상속한 패키지에서 `$listen`을 사용하는 방법도 있습니다. 다만 확인한 구현에서 `shouldDiscoverEvents()`의 기본값은 그 기반 클래스 자신에 대한 판정입니다. **상속하는 것만으로 그 패키지의 리스너 디렉터리가 자동 감지되는 것은 아닙니다.**

또한 같은 프로바이더의 `$subscribe`와 `$observers`는 `getEvents()`의 결과를 등록하는 루프와는 별도로 처리됩니다. `$listen`의 캐시만 보고 이러한 등록까지 생략된다고 판단하지 마세요.

<Tip>
  `Event::subscribe(SomeSubscriber::class)`는 등록 시 컨테이너로 구독자를 생성하여 `subscribe()`를 호출합니다. 클래스 이름에 의한 `Event::listen()`과 같은 지연성을 갖지 않습니다. 구독자의 생성자나 `subscribe()`에서는 외부 통신 등의 부작용을 피합니다.
</Tip>

## 자동 감지와의 이중 등록 피하기

사용자가 `withEvents(discover: [...])`로 패키지의 리스너 디렉터리를 추가하고, 패키지의 프로바이더도 같은 리스너를 명시적으로 등록하면, 하나의 이벤트에서 처리가 여러 번 실행될 수 있습니다.

`Dispatcher::listen()`은 일반 이벤트에 대해 배열에 추가를 수행하며, Dispatcher 전체에서 같은 클래스 이름을 자동으로 중복 제거하지 않습니다. Foundation의 `EventServiceProvider`가 자체 매핑 내에서 수행하는 `array_unique()`도 다른 프로바이더에 의한 명시적 등록까지 합치는 구조는 아닙니다.

등록 책임을 한쪽으로 정합니다.

* **패키지가 기본 리스너를 등록하는** 경우, 사용자에게 해당 디렉터리를 자동 감지 대상에 추가하도록 요구하지 않습니다.
* **사용자가 필요한 리스너를 선택하는** 경우, 패키지는 이벤트와 리스너 클래스를 제공하고 같은 리스너를 기본으로 등록하지 않습니다.
* 자동 감지에서 명시적 등록으로 전환하는 경우, 사용자 측의 감지 설정과 오래된 이벤트 캐시도 업데이트 대상으로 안내합니다.

업무 처리의 멱등성도 중요하지만, 이중 등록을 남겨 둔 채 멱등성만으로 감추지 말고 등록 경로를 정리하세요.

## 업데이트 전후의 동작 검증하기

등록 상태를 확인하려면 패키지를 도입한 검증용 애플리케이션에서 `event:list`를 실행합니다. 캐시가 없을 때와 있을 때 모두 대상 리스너가 존재하는지 확인합니다.

```bash theme={null}
php artisan event:clear
php artisan event:list
php artisan event:cache
php artisan event:list
```

이것은 검증용 앱에서의 절차입니다. `event:list`의 표시만으로는 리스너가 처리를 완료하는지, 한 번만 실행되는지까지 보장할 수 없습니다. 실제 이벤트 발생을 사용하는 테스트도 추가합니다.

예로, [Testbench의 기본 TestCase](/ko/advanced/package-testing)에서 `CourierServiceProvider`를 등록한 뒤 로거가 한 번만 호출되는지 검증합니다.

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

namespace Acme\Courier\Tests\Feature;

use Acme\Courier\Events\ShipmentDelivered;
use Acme\Courier\Tests\TestCase;
use Illuminate\Support\Facades\Event;
use Psr\Log\LoggerInterface;

class PackageEventsTest extends TestCase
{
    public function test_delivery_is_logged_once(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $logger->expects($this->once())
            ->method('info')
            ->with('Shipment delivered', ['shipment_id' => 'shipment-123']);

        $this->app->instance(LoggerInterface::class, $logger);

        Event::dispatch(new ShipmentDelivered('shipment-123'));
    }
}
```

`Event::fake()`는 이벤트 발생 확인에는 편리하지만, 일반적으로 실제 리스너의 실행을 멈춥니다. 이 테스트에서는 등록과 처리를 검증하기 위해 사용하지 않습니다. 자동 감지와의 조합이나 캐시가 있는 부팅도 별도의 검증용 앱·새로운 애플리케이션 인스턴스에서 확인합니다.

<Warning>
  부팅 후에 `event:cache`를 실행해도, 그 프로세스 내에 이미 등록된 Dispatcher가 캐시로부터 재구축되는 것은 아닙니다. 캐시가 있는 상태의 동작 확인은 새로운 부팅에서 수행하세요. 업데이트한 리스너를 장시간 동작하는 워커에 반영하려면 일반적인 배포 절차에 따른 재시작도 필요합니다.
</Warning>

## 장기 유지보수 체크리스트

* 이벤트 클래스 이름, 공개 프로퍼티, 리스너 등록 방침을 사용자용 계약으로 문서화한다.
* 사용자가 구독하는 이벤트의 이름이나 페이로드를 변경할 때는 [버전 호환성 관리](/ko/advanced/package-versioning)에 따라 변경을 안내한다.
* 자동 감지나 `$listen`에 변경이 있는 업데이트에서는 `event:cache` 재생성을 배포 절차에 포함한다.
* 패키지의 기본 리스너와 사용자의 독자적인 리스너를 구별하고, 같은 기본 리스너를 이중으로 등록하지 않도록 한다.
* PHP·Laravel의 지원 버전별로 리스너가 한 번만 실행되는 테스트를 지속한다.

## 참조한 1차 정보

* [Laravel 공식 문서: 이벤트 등록·자동 감지·캐시·구독자](https://github.com/laravel/docs/blob/73e61270287e4aaa1cecd77cd66aa6ef4f499828/events.md)
* [Dispatcher: listen·subscribe·createClassCallable](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Events/Dispatcher.php)
* [Foundation EventServiceProvider: register·getEvents·shouldDiscoverEvents](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php)
* [EventCacheCommand: 캐시 대상 수집](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Console/EventCacheCommand.php)


## Related topics

- [고급 주제](/ko/advanced/index.md)
- [패키지의 라우트 등록과 캐시](/ko/advanced/package-routes.md)
- [이벤트와 리스너](/ko/events.md)
- [Laravel Telescope 실전 테크닉](/ko/blog/telescope-introduction.md)
- [이벤트](/ko/packages/laravel-copilot-sdk/events.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.