Skip to main content
패키지에 이벤트 기반 확장 지점을 마련하려면 이벤트 클래스 제공과 리스너 등록을 나누어 설계합니다. 사용자가 vendor 내의 디렉터리를 자동 감지 대상에 추가하지 않아도 동작하는 구성으로 만들면, 설치·업데이트·캐시 생성 시의 전제 조건을 줄일 수 있습니다. 이 페이지에서는 Laravel Framework v13.36.0의 구현을 확인하고, 이벤트의 기본 조작을 패키지의 장기 유지보수에 적용합니다. 프로바이더 로드는 패키지 자동 감지를, 테스트 환경은 패키지 테스트를 참조하세요.

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

패키지가 기본으로 실행하는 처리는 일반적인 Illuminate\Support\ServiceProvider의 boot()에서 Event::listen()으로 등록할 수 있습니다. Laravel의 기본 리스너 자동 감지 대상은 애플리케이션의 app/Listeners이며, 패키지 내의 src/Listeners까지 자동으로 탐색하지는 않습니다. 예로, 배송 완료 이벤트를 로그에 기록하는 패키지를 만듭니다. 이벤트는 알림에 필요한 값만 가지는 클래스입니다.
리스너는 생성자에서 로거를 받고, handle()에서 이벤트를 처리합니다.
프로바이더에는 이벤트와 리스너의 클래스 이름을 등록합니다.
이 구성은 프로바이더 자체가 로드된다는 것을 전제로 합니다. Composer의 extra.laravel.providers 또는 사용자에 의한 명시적 등록을 준비하세요. 이벤트가 HTTP 처리와 큐 워커 양쪽에서 필요하다면 runningInConsole()로 등록을 제한하지 않습니다.
리스너 등록만을 이유로 이 프로바이더에 DeferrableProvider를 구현하지 마세요. 지연 대상 서비스가 해결되지 않으면 리스너도 등록되지 않습니다. 무거운 서비스 바인딩은 필요에 따라 별도의 지연 프로바이더로 분리합니다.

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

Dispatcher::listen()은 일반적인 이벤트 이름에 대한 리스너를 내부 배열에 추가합니다. 위와 같이 클래스 이름을 전달하는 경우, 이 시점에서는 리스너의 인스턴스를 생성하지 않습니다. 동기 리스너에서는 이벤트 발생 후 createClassCallable()이 컨테이너의 make()를 사용해 클래스를 해결합니다. 따라서 생성자의 의존성 주입을 이용할 수 있습니다. 이 경로는 handle()에 대해 컨테이너의 call()을 수행하지 않습니다. 임의의 서비스는 생성자에 주입하고, handle()에서는 이벤트를 받는 설계로 만듭니다. Artisan 명령어의 handle()에 대한 의존성 주입과 같은 구조라고 생각하지 마세요. 여기서 설명하는 것은 동기 리스너입니다. ShouldQueue를 구현하는 경우에는 큐용 별도 경로가 되므로, 트랜잭션, 재시도, 의존성 해결에 대해 이벤트의 큐 처리와 큐도 확인합니다.

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

event:cache는 이벤트 Dispatcher에 등록된 모든 리스너를 통째로 직렬화하는 명령어가 아닙니다. EventCacheCommand::getEvents()는 Illuminate\Foundation\Support\Providers\EventServiceProvider와 그 파생 프로바이더를 열거하고, 각 프로바이더의 자동 감지 결과와 listens()의 매핑을 저장합니다. 일반적인 ServiceProvider::boot()에 작성한 Event::listen()은 이 수집 대상에 포함되지 않습니다. 한편, 캐시가 있는 부팅에서도 일반 프로바이더의 boot()는 실행됩니다. 캐시에 포함되지 않는 것과, 캐시가 있으면 등록되지 않는 것은 별개입니다. Foundation의 EventServiceProvider를 상속한 패키지에서 $listen을 사용하는 방법도 있습니다. 다만 확인한 구현에서 shouldDiscoverEvents()의 기본값은 그 기반 클래스 자신에 대한 판정입니다. 상속하는 것만으로 그 패키지의 리스너 디렉터리가 자동 감지되는 것은 아닙니다. 또한 같은 프로바이더의 $subscribe와 $observers는 getEvents()의 결과를 등록하는 루프와는 별도로 처리됩니다. $listen의 캐시만 보고 이러한 등록까지 생략된다고 판단하지 마세요.
Event::subscribe(SomeSubscriber::class)는 등록 시 컨테이너로 구독자를 생성하여 subscribe()를 호출합니다. 클래스 이름에 의한 Event::listen()과 같은 지연성을 갖지 않습니다. 구독자의 생성자나 subscribe()에서는 외부 통신 등의 부작용을 피합니다.

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

사용자가 withEvents(discover: [...])로 패키지의 리스너 디렉터리를 추가하고, 패키지의 프로바이더도 같은 리스너를 명시적으로 등록하면, 하나의 이벤트에서 처리가 여러 번 실행될 수 있습니다. Dispatcher::listen()은 일반 이벤트에 대해 배열에 추가를 수행하며, Dispatcher 전체에서 같은 클래스 이름을 자동으로 중복 제거하지 않습니다. Foundation의 EventServiceProvider가 자체 매핑 내에서 수행하는 array_unique()도 다른 프로바이더에 의한 명시적 등록까지 합치는 구조는 아닙니다. 등록 책임을 한쪽으로 정합니다.
  • 패키지가 기본 리스너를 등록하는 경우, 사용자에게 해당 디렉터리를 자동 감지 대상에 추가하도록 요구하지 않습니다.
  • 사용자가 필요한 리스너를 선택하는 경우, 패키지는 이벤트와 리스너 클래스를 제공하고 같은 리스너를 기본으로 등록하지 않습니다.
  • 자동 감지에서 명시적 등록으로 전환하는 경우, 사용자 측의 감지 설정과 오래된 이벤트 캐시도 업데이트 대상으로 안내합니다.
업무 처리의 멱등성도 중요하지만, 이중 등록을 남겨 둔 채 멱등성만으로 감추지 말고 등록 경로를 정리하세요.

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

등록 상태를 확인하려면 패키지를 도입한 검증용 애플리케이션에서 event:list를 실행합니다. 캐시가 없을 때와 있을 때 모두 대상 리스너가 존재하는지 확인합니다.
이것은 검증용 앱에서의 절차입니다. event:list의 표시만으로는 리스너가 처리를 완료하는지, 한 번만 실행되는지까지 보장할 수 없습니다. 실제 이벤트 발생을 사용하는 테스트도 추가합니다. 예로, Testbench의 기본 TestCase에서 CourierServiceProvider를 등록한 뒤 로거가 한 번만 호출되는지 검증합니다.
Event::fake()는 이벤트 발생 확인에는 편리하지만, 일반적으로 실제 리스너의 실행을 멈춥니다. 이 테스트에서는 등록과 처리를 검증하기 위해 사용하지 않습니다. 자동 감지와의 조합이나 캐시가 있는 부팅도 별도의 검증용 앱·새로운 애플리케이션 인스턴스에서 확인합니다.
부팅 후에 event:cache를 실행해도, 그 프로세스 내에 이미 등록된 Dispatcher가 캐시로부터 재구축되는 것은 아닙니다. 캐시가 있는 상태의 동작 확인은 새로운 부팅에서 수행하세요. 업데이트한 리스너를 장시간 동작하는 워커에 반영하려면 일반적인 배포 절차에 따른 재시작도 필요합니다.

장기 유지보수 체크리스트

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

참조한 1차 정보

마지막 수정일 2026년 10월 13일