본문 바로가기

이벤트

업데이트됨

번역일: 2026년 9월 18일

이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.

원문 수정
2026년 9월 18일
번역 갱신
2026년 9월 18일

이벤트

소개

Laravel의 이벤트 시스템은 옵저버 패턴(Observer Pattern)을 간단하게 구현한 것으로, 애플리케이션에서 발생하는 다양한 일들을 구독하고 감지할 수 있게 해줍니다. 이벤트 클래스는 보통 app/Events 디렉터리에, 그리고 이를 처리하는 리스너는 app/Listeners 디렉터리에 저장됩니다. 만약 프로젝트에 이 디렉터리가 보이지 않더라도 걱정할 필요는 없습니다 — Artisan 콘솔 명령어로 이벤트나 리스너를 생성하면 자동으로 만들어집니다.

이벤트 시스템은 애플리케이션의 여러 관심사(concern)를 서로 느슨하게 결합(decouple)시키는 훌륭한 방법입니다. 하나의 이벤트에 대해 서로 의존하지 않는 여러 개의 리스너를 등록할 수 있기 때문입니다. 예를 들어 주문이 배송될 때마다 사용자에게 Slack 알림을 보내고 싶다고 가정해봅시다. 주문 처리 코드와 Slack 알림 코드를 직접 결합시키는 대신, App\Events\OrderShipped 이벤트를 발생(dispatch)시키고 리스너가 이를 받아서 Slack 알림을 전송하도록 만들 수 있습니다.

이벤트와 리스너 생성하기

이벤트와 리스너를 빠르게 생성하려면 make:event, make:listener Artisan 명령어를 사용하세요.

php artisan make:event PodcastProcessedphp artisan make:listener SendPodcastNotification --event=PodcastProcessed

간편하게, make:event와 make:listener 명령어를 인수 없이 실행할 수도 있습니다. 이 경우 Laravel이 클래스 이름과, 리스너 생성 시에는 리스너가 감지할 이벤트를 대화형으로 물어봅니다.

php artisan make:eventphp artisan make:listener

이벤트와 리스너 등록하기

이벤트 자동 탐색(Event Discovery)

기본적으로 Laravel은 애플리케이션의 Listeners 디렉터리를 스캔하여 이벤트 리스너를 자동으로 찾아 등록합니다. Laravel은 handle 또는 __invoke로 시작하는 리스너 클래스의 메서드를 발견하면, 해당 메서드의 시그니처에 타입힌트된 이벤트에 대한 이벤트 리스너로 등록합니다.

use App\Events\PodcastProcessed; class SendPodcastNotification { /** * Handle the given event. */ public function handle(PodcastProcessed $event): void { // ... } }

리스너에서 여러 개의 이벤트를 감지하고 싶다면, PHP의 유니언 타입을 사용해서 여러 이벤트를 지정할 수 있습니다.

/** * Handle the given event. */ public function handle(PodcastProcessed|PodcastPublished $event): void { // ... }

만약 리스너를 다른 디렉터리나 여러 디렉터리에 나눠 저장하고 싶다면, bootstrap/app.php에서 withEvents 메서드를 사용하여 Laravel에게 해당 디렉터리들도 스캔하도록 지시할 수 있습니다.

->withEvents(discover: [ __DIR__.'/../app/Domain/Orders/Listeners', ])

* 문자를 와일드카드로 사용하여 여러 유사한 디렉터리를 한 번에 스캔하는 것도 가능합니다.

->withEvents(discover: [ __DIR__.'/../app/Domain/*/Listeners', ])

애플리케이션에 등록된 모든 리스너 목록을 확인하고 싶다면 event:list 명령어를 사용하세요.

php artisan event:list

프로덕션 환경에서의 이벤트 자동 탐색

애플리케이션 속도를 높이기 위해, optimize 또는 event:cache Artisan 명령어를 사용하여 모든 리스너 목록을 캐시해두어야 합니다. 이 명령어는 애플리케이션의 모든 이벤트-리스너 매니페스트를 캐시로 생성하여 이벤트 등록 과정을 훨씬 빠르게 만들어줍니다. 이 명령어는 보통 배포 프로세스의 일부로 실행하면 됩니다. 이 매니페스트는 event:clear 명령어를 통해 삭제할 수 있습니다.

NOTE

event:cache 대신 optimize 명령어를 사용하면 이벤트뿐만 아니라 설정, 라우트 등 다른 여러 캐시도 함께 생성됩니다.

수동으로 이벤트 등록하기

Event 파사드를 사용하면 애플리케이션의 서비스 프로바이더 중 하나에서 이벤트를 수동으로 등록할 수 있습니다.

use App\Domain\Orders\Events\PodcastProcessed; use App\Domain\Orders\Listeners\SendPodcastNotification; use Illuminate\Support\Facades\Event; /** * Bootstrap any application services. */ public function boot(): void { Event::listen( PodcastProcessed::class, SendPodcastNotification::class, ); }

event:list 명령어로 애플리케이션에 등록된 모든 리스너를 확인할 수 있습니다.

php artisan event:list

클로저 리스너

보통 리스너는 클래스로 정의하지만, 애플리케이션 서비스 프로바이더의 boot 메서드에서 이벤트 리스너를 클로저 형태로 수동 등록할 수도 있습니다.

use App\Events\PodcastProcessed; use Illuminate\Support\Facades\Event; /** * Bootstrap any application services. */ public function boot(): void { Event::listen(function (PodcastProcessed $event) { // ... }); }

큐를 사용하는 익명 이벤트 리스너

클로저 기반 이벤트 리스너를 등록할 때, 리스너 클로저를 Illuminate\Events\queueable 함수로 감싸면 Laravel이 큐를 사용하여 해당 리스너를 실행하도록 지시할 수 있습니다.

use App\Events\PodcastProcessed; use function Illuminate\Events\queueable; use Illuminate\Support\Facades\Event; /** * Bootstrap any application services. */ public function boot(): void { Event::listen(queueable(function (PodcastProcessed $event) { // ... })); }

큐에 등록된 Job과 마찬가지로, onConnection, onQueue, delay 메서드를 사용하여 큐로 실행되는 클로저의 실행 방식을 커스터마이징할 수 있습니다.

Event::listen(queueable(function (PodcastProcessed $event) { // ... })->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10)));

익명 큐 리스너의 실패를 처리하고 싶다면, queueable 리스너를 정의할 때 catch 메서드에 클로저를 전달하면 됩니다. 이 클로저는 이벤트 인스턴스와 리스너 실패를 야기한 Throwable 인스턴스를 인자로 받습니다.

use App\Events\PodcastProcessed; use function Illuminate\Events\queueable; use Illuminate\Support\Facades\Event; use Throwable; Event::listen(queueable(function (PodcastProcessed $event) { // ... })->catch(function (PodcastProcessed $event, Throwable $e) { // 큐에 등록된 리스너가 실패했습니다... }));

와일드카드 이벤트 리스너

* 문자를 와일드카드 파라미터로 사용하여 리스너를 등록할 수도 있습니다. 이렇게 하면 하나의 리스너로 여러 이벤트를 감지할 수 있습니다. 와일드카드 리스너는 첫 번째 인수로 이벤트 이름을, 두 번째 인수로 전체 이벤트 데이터 배열을 전달받습니다.

Event::listen('event.*', function (string $eventName, array $data) { // ... });

이벤트 정의하기

이벤트 클래스는 본질적으로 해당 이벤트와 관련된 정보를 담는 데이터 컨테이너입니다. 예를 들어 App\Events\OrderShipped 이벤트가 Eloquent ORM 객체를 전달받는다고 가정해보겠습니다.

<?php namespace App\Events; use App\Models\Order; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; class OrderShipped { use Dispatchable, InteractsWithSockets, SerializesModels; /** * Create a new event instance. */ public function __construct( public Order $order, ) {} }

보시다시피, 이 이벤트 클래스에는 별도의 로직이 없습니다. 단순히 구입된 App\Models\Order 인스턴스를 담는 컨테이너 역할만 합니다. 이벤트에 사용된 SerializesModels 트레이트는 이벤트 객체가 PHP의 serialize 함수로 직렬화될 때 — 예를 들어 큐 리스너를 사용하는 경우처럼 — Eloquent 모델을 올바르게 직렬화해줍니다.

리스너 정의하기

이제 예제 이벤트에 대한 리스너를 살펴보겠습니다. 이벤트 리스너는 handle 메서드에서 이벤트 인스턴스를 전달받습니다. make:listener 명령어를 --event 옵션과 함께 실행하면 올바른 이벤트 클래스를 자동으로 임포트하고, handle 메서드에 해당 이벤트를 타입힌트로 설정해줍니다. handle 메서드 내부에서는 이벤트에 응답하기 위해 필요한 모든 작업을 수행할 수 있습니다.

<?php namespace App\Listeners; use App\Events\OrderShipped; class SendShipmentNotification { /** * Create the event listener. */ public function __construct() { // ... } /** * Handle the event. */ public function handle(OrderShipped $event): void { // $event->order 를 통해 주문 정보에 접근할 수 있습니다... } }

NOTE

이벤트 리스너의 생성자에도 필요한 모든 의존성을 타입힌트로 지정할 수 있습니다. 모든 이벤트 리스너는 Laravel 서비스 컨테이너를 통해 해석(resolve)되므로 의존성이 자동으로 주입됩니다.

이벤트 전파 중단하기

경우에 따라 하나의 이벤트가 다른 리스너로 전파되는 것을 중단시키고 싶을 수 있습니다. 이를 위해서는 리스너의 handle 메서드에서 false를 반환하면 됩니다.

이벤트

소개

Laravel의 이벤트 시스템은 옵저버 패턴(observer pattern)을 간단하게 구현해두어, 애플리케이션 내에서 발생하는 다양한 이벤트를 구독하고 감지할 수 있게 해줍니다. 이벤트 클래스는 보통 app/Events 디렉터리에, 이를 처리하는 리스너는 app/Listeners 디렉터리에 저장됩니다. 지금 애플리케이션에 이 디렉터리들이 보이지 않아도 걱정할 필요는 없습니다 — Artisan 콘솔 명령어로 이벤트와 리스너를 생성하면 자동으로 만들어집니다.

이벤트는 애플리케이션의 여러 기능을 서로 느슨하게 결합(decouple)할 수 있게 해주는 훌륭한 수단입니다. 하나의 이벤트에 서로 의존하지 않는 여러 개의 리스너를 연결할 수 있기 때문입니다. 예를 들어 주문이 배송될 때마다 사용자에게 슬랙(Slack) 알림을 보내고 싶다고 가정해봅시다. 주문 처리 코드에 슬랙 알림 코드를 직접 넣어 강하게 결합시키는 대신, App\Events\OrderShipped 이벤트를 발생(raise)시키고, 이를 수신하는 리스너가 슬랙 알림을 전송하도록 분리해서 작성할 수 있습니다.

이렇게 하면 주문 처리 로직과 알림 발송 로직이 서로의 존재를 몰라도 되며, 나중에 슬랙 알림 외에 이메일 알림이나 SMS 알림 리스너를 추가로 붙이고 싶을 때도 주문 처리 코드는 전혀 건드릴 필요가 없습니다.

이벤트

이벤트와 리스너 생성하기

make:event와 make:listener Artisan 명령어를 사용하면 이벤트와 리스너를 빠르게 생성할 수 있습니다:

php artisan make:event PodcastProcessedphp artisan make:listener SendPodcastNotification --event=PodcastProcessed

더 간편하게는 make:event와 make:listener 명령어를 별도의 인자 없이 실행할 수도 있습니다. 이 경우 Laravel이 클래스 이름을 물어보며, 리스너를 생성할 때는 어떤 이벤트를 구독할지도 함께 물어봅니다:

php artisan make:eventphp artisan make:listener

NOTE

매번 명령어를 직접 입력하기 번거롭다면, php artisan make:event처럼 인자 없이 실행한 뒤 대화형 프롬프트를 따라가는 방식을 추천합니다. 이벤트 이름과 리스너가 처리할 이벤트를 연결하는 과정에서 오타를 줄일 수 있습니다.

이벤트

이벤트와 리스너 등록하기

이벤트 자동 탐색(Event Discovery)

라라벨은 기본적으로 애플리케이션의 Listeners 디렉터리를 스캔해서 이벤트 리스너를 자동으로 찾아 등록합니다. handle이나 __invoke로 시작하는 리스너 클래스 메서드를 발견하면, 라라벨은 해당 메서드의 시그니처에 타입힌트된 이벤트에 대한 리스너로 그 메서드를 등록합니다:

use App\Events\PodcastProcessed; class SendPodcastNotification { /** * 이벤트를 처리합니다. */ public function handle(PodcastProcessed $event): void { // ... } }

PHP의 유니온 타입을 사용하면 여러 이벤트를 하나의 리스너에서 처리할 수도 있습니다:

/** * 이벤트를 처리합니다. */ public function handle(PodcastProcessed|PodcastPublished $event): void { // ... }

리스너를 다른 디렉터리나 여러 디렉터리에 나눠서 저장하고 싶다면, bootstrap/app.php 파일에서 withEvents 메서드를 사용해 라라벨이 해당 디렉터리들을 스캔하도록 지정할 수 있습니다:

->withEvents(discover: [ __DIR__.'/../app/Domain/Orders/Listeners', ])

* 문자를 와일드카드로 사용하면 유사한 구조를 가진 여러 디렉터리를 한 번에 스캔할 수도 있습니다:

->withEvents(discover: [ __DIR__.'/../app/Domain/*/Listeners', ])

애플리케이션에 등록된 모든 리스너 목록을 확인하려면 event:list 명령어를 사용하면 됩니다:

php artisan event:list

프로덕션 환경에서의 이벤트 탐색

애플리케이션의 속도를 높이려면 optimize 또는 event:cache Artisan 명령어를 사용해 애플리케이션의 모든 리스너 목록을 캐시해두어야 합니다. 일반적으로 이 명령어는 애플리케이션의 배포 프로세스 중에 실행하는 것이 좋습니다. 이렇게 생성된 매니페스트는 프레임워크가 이벤트 등록 과정을 더 빠르게 처리하는 데 사용됩니다. 캐시를 삭제하려면 event:clear 명령어를 사용하세요.

NOTE

매번 배포할 때마다 매니페스트를 새로 캐시해야 코드 변경 사항이 반영됩니다. CI/CD 파이프라인에 php artisan event:cache 실행 단계를 포함시켜 두면 실수로 빠뜨리는 일을 방지할 수 있습니다.

동적 이벤트 탐색

특정 리스너가 탐색 대상에 포함될지 여부를 동적으로 제어하고 싶다면, 리스너 클래스에 ShouldBeDiscovered 인터페이스를 구현하고 boolean 값을 반환하는 shouldBeDiscovered 메서드를 정의하면 됩니다. 이 메서드가 false를 반환하면 해당 리스너는 이벤트 탐색 과정에서 등록되지 않습니다:

use Illuminate\Contracts\Events\ShouldBeDiscovered; class SendPodcastNotification implements ShouldBeDiscovered { /** * 이벤트를 처리합니다. */ public function handle(PodcastProcessed $event): void { // ... } /** * 해당 리스너를 탐색 대상에 포함할지 결정합니다. */ public static function shouldBeDiscovered(): bool { return app()->environment('production'); } }

이벤트 수동 등록하기

Event 파사드를 사용하면 애플리케이션의 AppServiceProvider에 있는 boot 메서드 안에서 이벤트와 그에 대응하는 리스너를 직접 등록할 수 있습니다:

use App\Domain\Orders\Events\PodcastProcessed; use App\Domain\Orders\Listeners\SendPodcastNotification; use Illuminate\Support\Facades\Event; /** * 애플리케이션의 서비스를 부트스트랩합니다. */ public function boot(): void { Event::listen( PodcastProcessed::class, SendPodcastNotification::class, ); }

이번에도 event:list 명령어로 애플리케이션에 등록된 모든 리스너를 확인할 수 있습니다:

php artisan event:list

클로저 기반 리스너

일반적으로 리스너는 클래스 형태로 정의하지만, AppServiceProvider의 boot 메서드 안에서 클로저 기반 이벤트 리스너를 직접 등록할 수도 있습니다:

use App\Events\PodcastProcessed; use Illuminate\Support\Facades\Event; /** * 애플리케이션의 서비스를 부트스트랩합니다. */ public function boot(): void { Event::listen(function (PodcastProcessed $event) { // ... }); }

큐를 사용하는 익명 이벤트 리스너

클로저 기반 이벤트 리스너를 등록할 때, 리스너 클로저를 Illuminate\Events\queueable 함수로 감싸면 라라벨에게 해당 리스너를 큐를 통해 실행하도록 지시할 수 있습니다:

use App\Events\PodcastProcessed; use function Illuminate\Events\queueable; use Illuminate\Support\Facades\Event; /** * 애플리케이션의 서비스를 부트스트랩합니다. */ public function boot(): void { Event::listen(queueable(function (PodcastProcessed $event) { // ... })); }

큐에 등록된 Job과 마찬가지로, onConnection, onQueue, delay 메서드를 사용해 큐 리스너의 실행 방식을 커스터마이징할 수 있습니다:

Event::listen(queueable(function (PodcastProcessed $event) { // ... })->onConnection('redis')->onQueue('podcasts')->delay(now()->plus(seconds: 10)));

익명 큐 리스너가 실패했을 때 이를 처리하고 싶다면, queueable 리스너를 정의할 때 catch 메서드에 클로저를 전달하면 됩니다. 이 클로저는 이벤트 인스턴스와 리스너 실패의 원인이 된 Throwable 인스턴스를 인자로 전달받습니다:

use App\Events\PodcastProcessed; use function Illuminate\Events\queueable; use Illuminate\Support\Facades\Event; use Throwable; Event::listen(queueable(function (PodcastProcessed $event) { // ... })->catch(function (PodcastProcessed $event, Throwable $e) { // 큐 리스너 실행이 실패했습니다... }));

와일드카드 이벤트 리스너

* 문자를 와일드카드 파라미터로 사용해 리스너를 등록할 수도 있습니다. 이렇게 하면 하나의 리스너에서 여러 이벤트를 한 번에 감지할 수 있습니다. 와일드카드 리스너는 첫 번째 인자로 이벤트 이름을, 두 번째 인자로 이벤트 데이터 전체가 담긴 배열을 전달받습니다:

Event::listen('event.*', function (string $eventName, array $data) { // ... });

NOTE

와일드카드 리스너는 이벤트 이름 패턴이 일치하는 모든 이벤트를 감지할 수 있어 로깅이나 디버깅 목적으로 유용합니다. 다만 특정 이벤트에만 반응해야 하는 비즈니스 로직에는 명시적인 이벤트 클래스 기반 리스너를 사용하는 것이 코드 가독성과 유지보수 측면에서 더 좋습니다.

이벤트 정의하기

이벤트 클래스는 본질적으로 이벤트와 관련된 정보를 담는 데이터 컨테이너입니다. 예를 들어, App\Events\OrderShipped 이벤트가 Eloquent ORM 객체를 전달받는다고 가정해보겠습니다.

<?php namespace App\Events; use App\Models\Order; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; class OrderShipped { use Dispatchable, InteractsWithSockets, SerializesModels; /** * 새 이벤트 인스턴스를 생성합니다. */ public function __construct( public Order $order, ) {} }

보시다시피 이 이벤트 클래스는 별다른 로직 없이, 구매된 App\Models\Order 인스턴스를 담는 컨테이너 역할만 합니다. 이벤트에서 사용하는 SerializesModels 트레이트는 큐에 등록되는 리스너를 사용할 때처럼 이벤트 객체가 PHP의 serialize 함수로 직렬화될 때, 포함된 Eloquent 모델을 안전하게 직렬화해줍니다.

NOTE

이벤트 클래스에 별도의 메서드를 추가할 필요는 없습니다. 대부분의 이벤트 클래스는 생성자를 통해 전달받은 데이터를 그대로 보관하는 순수한 데이터 객체(DTO)에 가깝습니다. 실제 처리 로직은 뒤에서 살펴볼 리스너(Listener)에서 담당합니다.

이벤트

리스너 정의하기

이번에는 앞서 만든 이벤트에 대응하는 리스너를 살펴보겠습니다. 이벤트 리스너는 handle 메서드를 통해 이벤트 인스턴스를 전달받습니다. make:listener Artisan 명령어를 --event 옵션과 함께 실행하면, 해당 이벤트 클래스를 자동으로 import하고 handle 메서드에 타입힌트까지 걸어줍니다. handle 메서드 안에서는 이벤트에 대응하여 필요한 모든 작업을 자유롭게 처리할 수 있습니다:

<?php namespace App\Listeners; use App\Events\OrderShipped; class SendShipmentNotification { /** * 이벤트 리스너를 생성합니다. */ public function __construct() {} /** * 이벤트를 처리합니다. */ public function handle(OrderShipped $event): void { // $event->order를 통해 주문 정보에 접근할 수 있습니다... } }

NOTE

이벤트 리스너도 생성자에서 필요한 의존성을 타입힌트로 선언할 수 있습니다. 모든 이벤트 리스너는 라라벨의 서비스 컨테이너를 통해 해석(resolve)되므로, 의존성이 자동으로 주입됩니다.

이벤트 전파 중단하기

경우에 따라 특정 이벤트가 다른 리스너에게 전파되는 것을 막고 싶을 때가 있습니다. 이런 경우에는 리스너의 handle 메서드에서 false를 반환하면 됩니다.

큐 이벤트 리스너

이메일 발송이나 외부 HTTP 요청처럼 처리 시간이 오래 걸리는 작업을 수행하는 리스너라면, 큐에 등록해서 비동기로 실행하는 것이 좋습니다. 큐 리스너를 사용하기 전에 먼저 큐를 설정하고, 서버나 로컬 개발 환경에서 큐 워커를 실행해두어야 합니다.

리스너를 큐에 등록하려면 리스너 클래스에 ShouldQueue 인터페이스를 구현하면 됩니다. make:listener Artisan 명령어로 생성한 리스너에는 이 인터페이스가 이미 임포트되어 있으므로 바로 사용할 수 있습니다.

<?php namespace App\Listeners; use App\Events\OrderShipped; use Illuminate\Contracts\Queue\ShouldQueue; class SendShipmentNotification implements ShouldQueue { // ... }

이게 전부입니다! 이제 이 리스너가 처리하는 이벤트가 디스패치되면, 이벤트 디스패처가 Laravel의 큐 시스템을 이용해 리스너를 자동으로 큐에 등록합니다. 큐에서 리스너가 실행될 때 예외가 발생하지 않으면, 처리가 끝난 후 해당 큐 Job은 자동으로 삭제됩니다.

큐 연결, 이름, 지연 시간 커스터마이징

이벤트 리스너의 큐 연결, 큐 이름, 지연 시간을 지정하고 싶다면 리스너 클래스에 Connection, Queue, Delay 속성(attribute)을 사용할 수 있습니다.

<?php namespace App\Listeners; use App\Events\OrderShipped; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\Attributes\Connection; use Illuminate\Queue\Attributes\Delay; use Illuminate\Queue\Attributes\Queue; #[Connection('sqs')] #[Queue('listeners')] #[Delay(60)] class SendShipmentNotification implements ShouldQueue { // ... }

큐 연결, 큐 이름, 지연 시간을 런타임에 동적으로 결정하고 싶다면, 리스너에 viaConnection, viaQueue, withDelay 메서드를 정의하면 됩니다.

/** * 리스너가 사용할 큐 연결 이름을 반환합니다. */ public function viaConnection(): string { return 'sqs'; } /** * 리스너가 사용할 큐 이름을 반환합니다. */ public function viaQueue(): string { return 'listeners'; } /** * Job을 처리하기까지 대기할 시간(초)을 반환합니다. */ public function withDelay(OrderShipped $event): int { return $event->highPriority ? 0 : 60; }

모든 큐 리스너에서 각 리스너 클래스를 일일이 커스터마이징하지 않고 동일한 큐를 사용하도록 하고 싶다면, ShouldQueue 계약을 특정 큐로 라우팅하는 방법을 사용할 수도 있습니다.

조건부로 리스너를 큐에 등록하기

런타임에만 확인 가능한 데이터를 기준으로 리스너를 큐에 등록할지 판단해야 할 때가 있습니다. 이런 경우 리스너에 shouldQueue 메서드를 추가하면 됩니다. shouldQueue 메서드가 false를 반환하면 해당 리스너는 큐에 등록되지 않습니다.

<?php namespace App\Listeners; use App\Events\OrderCreated; use Illuminate\Contracts\Queue\ShouldQueue; class RewardGiftCard implements ShouldQueue { /** * 고객에게 기프트 카드를 지급합니다. */ public function handle(OrderCreated $event): void { // ... } /** * 리스너를 큐에 등록해야 하는지 여부를 결정합니다. */ public function shouldQueue(OrderCreated $event): bool { return $event->order->subtotal >= 5000; } }

큐와 직접 상호작용하기

리스너가 사용하는 큐 Job의 delete, release 메서드에 직접 접근해야 한다면 Illuminate\Queue\InteractsWithQueue 트레이트를 사용하면 됩니다. 생성된 리스너에는 이 트레이트가 기본으로 임포트되어 있으며, 다음과 같이 사용할 수 있습니다.

<?php namespace App\Listeners; use App\Events\OrderShipped; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; class SendShipmentNotification implements ShouldQueue { use InteractsWithQueue; /** * 이벤트를 처리합니다. */ public function handle(OrderShipped $event): void { if ($condition) { $this->release(30); } } }

큐 이벤트 리스너와 데이터베이스 트랜잭션

데이터베이스 트랜잭션 내부에서 큐 리스너를 디스패치하면, 트랜잭션이 커밋되기 전에 큐 워커가 먼저 해당 Job을 처리해버릴 수 있습니다. 이 경우 트랜잭션 안에서 수정한 모델이나 레코드가 아직 데이터베이스에 반영되지 않은 상태일 수 있고, 트랜잭션 내에서 새로 생성한 레코드는 아직 존재하지 않을 수도 있습니다. 리스너가 이런 모델·레코드에 의존한다면, Job이 처리될 때 예상치 못한 오류가 발생할 수 있습니다.

큐 연결 설정의 after_commit 옵션이 false로 되어 있더라도, 특정 큐 리스너만큼은 모든 열린 트랜잭션이 커밋된 이후에 디스패치되도록 하고 싶다면 리스너 클래스에 ShouldQueueAfterCommit 인터페이스를 구현하면 됩니다.

<?php namespace App\Listeners; use Illuminate\Contracts\Queue\ShouldQueueAfterCommit; use Illuminate\Queue\InteractsWithQueue; class SendShipmentNotification implements ShouldQueueAfterCommit { use InteractsWithQueue; }

NOTE

이런 문제를 우회하는 방법에 대해 더 자세히 알고 싶다면 큐 Job과 데이터베이스 트랜잭션 문서를 참고하세요.

큐 리스너 미들웨어

큐 리스너에도 Job 미들웨어를 사용할 수 있습니다. Job 미들웨어를 사용하면 큐 리스너 실행 전후로 커스텀 로직을 감쌀 수 있어, 리스너 코드 자체에 반복적으로 들어가는 보일러플레이트 코드를 줄일 수 있습니다. Job 미들웨어를 작성한 후, 리스너의 middleware 메서드에서 반환하도록 지정하면 해당 미들웨어가 적용됩니다.

<?php namespace App\Listeners; use App\Events\OrderShipped; use App\Jobs\Middleware\RateLimited; use Illuminate\Contracts\Queue\ShouldQueue; class SendShipmentNotification implements ShouldQueue { /** * 이벤트를 처리합니다. */ public function handle(OrderShipped $event): void { // 이벤트 처리 로직... } /** * 리스너가 통과해야 할 미들웨어 목록을 반환합니다. * * @return array<int, object> */ public function middleware(OrderShipped $event): array { return [new RateLimited]; } }

암호화된 큐 리스너

Laravel은 암호화 기능을 통해 큐 리스너 데이터의 기밀성과 무결성을 보장할 수 있습니다. 리스너 클래스에 ShouldBeEncrypted 인터페이스만 추가하면, Laravel이 리스너를 큐에 넣기 전에 자동으로 암호화해줍니다.

<?php namespace App\Listeners; use App\Events\OrderShipped; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; class SendShipmentNotification implements ShouldQueue, ShouldBeEncrypted { // ... }

유니크 이벤트 리스너

WARNING

유니크 리스너를 사용하려면 락(lock)을 지원하는 캐시 드라이버가 필요합니다. 현재 memcached, redis, dynamodb, database, file, array 캐시 드라이버가 원자적 락을 지원합니다.

특정 리스너의 인스턴스가 동시에 하나만 큐에 존재하도록 보장해야 할 때가 있습니다. 이럴 때는 리스너 클래스에 ShouldBeUnique 인터페이스를 구현하면 됩니다.

<?php namespace App\Listeners; use App\Events\LicenseSaved; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; class AcquireProductKey implements ShouldQueue, ShouldBeUnique { public function __invoke(LicenseSaved $event): void { // ... } }

위 예제에서 AcquireProductKey 리스너는 유니크하게 동작합니다. 즉, 동일한 리스너의 다른 인스턴스가 이미 큐에서 처리 대기 중이라면 새로 큐에 등록되지 않습니다. 덕분에 라이선스가 짧은 시간 안에 여러 번 저장되더라도 각 라이선스마다 제품 키가 딱 한 번만 발급됩니다.

경우에 따라서는 리스너를 유니크하게 구분하는 "키" 값을 직접 지정하거나, 리스너가 더 이상 유니크 상태를 유지하지 않을 타임아웃 시점을 정의하고 싶을 수 있습니다. 이럴 때는 리스너 클래스에 uniqueId와 uniqueFor 프로퍼티(또는 메서드)를 정의하면 됩니다. 이 메서드들은 이벤트 인스턴스를 인자로 받으므로, 이벤트 데이터를 활용해 반환 값을 구성할 수 있습니다.

<?php namespace App\Listeners; use App\Events\LicenseSaved; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; class AcquireProductKey implements ShouldQueue, ShouldBeUnique { /** * 리스너의 유니크 락이 해제되기까지 걸리는 시간(초). * * @var int */ public $uniqueFor = 3600; public function __invoke(LicenseSaved $event): void { // ... } /** * 리스너의 고유 ID를 반환합니다. */ public function uniqueId(LicenseSaved $event): string { return 'listener:'.$event->license->id; } }

위 예제에서 AcquireProductKey 리스너는 라이선스 ID를 기준으로 유니크하게 동작합니다. 따라서 동일한 라이선스에 대해 리스너가 새로 디스패치되더라도, 이미 처리 중인 리스너가 완료될 때까지는 무시됩니다. 이렇게 하면 동일한 라이선스에 중복으로 제품 키가 발급되는 것을 막을 수 있습니다. 또한 기존 리스너가 1시간 이내에 처리되지 않으면 유니크 락이 해제되어, 동일한 키를 가진 새로운 리스너가 다시 큐에 등록될 수 있습니다.

WARNING

애플리케이션이 여러 웹 서버나 컨테이너에서 이벤트를 디스패치한다면, 모든 서버가 동일한 중앙 캐시 서버를 바라보고 있는지 반드시 확인해야 합니다. 그래야 Laravel이 리스너의 유니크 여부를 정확하게 판단할 수 있습니다.

처리가 시작되기 전까지만 유니크 상태 유지하기

기본적으로 유니크 리스너는 처리가 완료되거나 재시도 횟수를 모두 소진해 실패로 처리된 후에야 락이 해제됩니다. 하지만 경우에 따라 리스너 처리가 "시작되는" 시점에 바로 락을 해제하고 싶을 수도 있습니다. 이럴 때는 ShouldBeUnique 대신 ShouldBeUniqueUntilProcessing 계약을 구현하면 됩니다.

<?php namespace App\Listeners; use App\Events\LicenseSaved; use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Contracts\Queue\ShouldQueue; class AcquireProductKey implements ShouldQueue, ShouldBeUniqueUntilProcessing { // ... }

유니크 리스너 락의 동작 방식

ShouldBeUnique 리스너가 디스패치되면, Laravel 내부에서는 uniqueId 값을 키로 하는 락을 획득하려고 시도합니다. 이미 락이 걸려 있다면 해당 리스너는 디스패치되지 않습니다. 이 락은 리스너 처리가 완료되거나 재시도 횟수를 모두 소진해 실패했을 때 해제됩니다. 기본적으로 Laravel은 기본 캐시 드라이버를 사용해 락을 획득하지만, 다른 드라이버를 사용하고 싶다면 사용할 캐시 드라이버를 반환하는 uniqueVia 메서드를 정의하면 됩니다.

<?php namespace App\Listeners; use App\Events\LicenseSaved; use Illuminate\Contracts\Cache\Repository; use Illuminate\Support\Facades\Cache; class AcquireProductKey implements ShouldQueue, ShouldBeUnique { // ... /** * 유니크 리스너 락에 사용할 캐시 드라이버를 반환합니다. */ public function uniqueVia(LicenseSaved $event): Repository { return Cache::driver('redis'); } }

NOTE

단순히 리스너의 동시 처리 개수만 제한하고 싶다면, ShouldBeUnique 대신 WithoutOverlapping Job 미들웨어를 사용하는 편이 더 적합합니다.

디바운스 이벤트 리스너

짧은 시간 안에 같은 이벤트가 반복적으로 디스패치될 때, 가장 마지막 이벤트만 처리하고 싶은 경우가 있습니다. 이럴 때는 큐 리스너에 DebounceFor 속성(attribute)을 추가하면 됩니다.

<?php namespace App\Listeners; use App\Events\ProductUpdated; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\Attributes\DebounceFor; #[DebounceFor(30)] class UpdateProductSearchIndex implements ShouldQueue { /** * 이벤트를 처리합니다. */ public function handle(ProductUpdated $event): void { // 상품 검색 인덱스를 갱신합니다... } /** * 리스너의 디바운스 ID를 반환합니다. */ public function debounceId(ProductUpdated $event): string { return (string) $event->product->getKey(); } }

위 예제에서는 동일한 상품에 대해 ProductUpdated 이벤트가 30초 이내에 반복적으로 디스패치되더라도, 가장 마지막 이벤트만 처리되도록 리스너가 디바운스됩니다. 디바운스 ID가 다르면 서로 독립적으로 처리됩니다.

특정 이벤트가 너무 자주 발생해 리스너 처리가 계속 지연되는 것을 막고 싶다면, DebounceFor 속성에 maxWait 인자를 지정해 최대 대기 시간을 제한할 수 있습니다.

#[DebounceFor(30, maxWait: 120)] class UpdateProductSearchIndex implements ShouldQueue { // ... }

디바운스 상태를 추적하는 데 사용할 캐시 저장소도 리스너에 debounceVia 메서드를 정의해 커스터마이징할 수 있습니다. 이 메서드는 이벤트 인스턴스를 받아 캐시 저장소를 반환해야 합니다.

use Illuminate\Contracts\Cache\Repository; use Illuminate\Support\Facades\Cache; public function debounceVia(ProductUpdated $event): Repository { return Cache::driver('redis'); }

디바운스 리스너와 유니크 리스너는 함께 사용할 수 없습니다. DebounceFor 속성을 사용하는 리스너는 ShouldBeUnique를 구현해서는 안 됩니다.

WARNING

애플리케이션이 여러 웹 서버나 컨테이너에서 이벤트를 디스패치한다면, 모든 서버가 동일한 중앙 캐시 서버를 바라보고 있는지 반드시 확인해야 합니다.

실패한 Job 다루기

큐 이벤트 리스너가 실패하는 경우도 있습니다. 큐 워커에 설정된 최대 시도 횟수를 초과하면, 리스너의 failed 메서드가 호출됩니다. failed 메서드는 이벤트 인스턴스와 실패 원인이 된 Throwable을 인자로 받습니다.

<?php namespace App\Listeners; use App\Events\OrderShipped; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; use Throwable; class SendShipmentNotification implements ShouldQueue { use InteractsWithQueue; /** * 이벤트를 처리합니다. */ public function handle(OrderShipped $event): void { // ... } /** * Job 실패를 처리합니다. */ public function failed(OrderShipped $event, Throwable $exception): void { // ... } }

큐 리스너 최대 시도 횟수 지정하기

큐 리스너에서 오류가 계속 발생한다면, 이를 무한정 재시도하고 싶지는 않을 것입니다. Laravel은 리스너를 몇 번, 혹은 얼마 동안 재시도할지 지정할 수 있는 다양한 방법을 제공합니다.

리스너 클래스에 Tries 속성을 사용하면, 실패로 간주되기 전까지 리스너를 몇 번 재시도할지 지정할 수 있습니다.

<?php namespace App\Listeners; use App\Events\OrderShipped; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\Attributes\Tries; use Illuminate\Queue\InteractsWithQueue; #[Tries(5)] class SendShipmentNotification implements ShouldQueue { use InteractsWithQueue; // ... }

시도 횟수를 지정하는 대신, 리스너를 더 이상 재시도하지 않을 시점을 시간으로 정의할 수도 있습니다. 이렇게 하면 정해진 시간 범위 내에서는 횟수 제한 없이 리스너를 재시도할 수 있습니다. 리스너 클래스에 retryUntil 메서드를 추가하고, 이 메서드가 DateTimeInterface 인스턴스를 반환하도록 하면 됩니다.

use DateTimeInterface; /** * 리스너가 타임아웃되는 시점을 결정합니다. */ public function retryUntil(): DateTimeInterface { return now()->plus(minutes: 5); }

retryUntil과 tries가 모두 정의되어 있다면, Laravel은 retryUntil을 우선적으로 적용합니다.

큐 리스너 백오프(backoff) 지정하기

리스너에서 예외가 발생했을 때 재시도까지 몇 초를 기다릴지 설정하고 싶다면, 리스너 클래스에 Backoff 속성을 사용하면 됩니다.

<?php namespace App\Listeners; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\Attributes\Backoff; #[Backoff(3)] class SendShipmentNotification implements ShouldQueue { // ... }

백오프 시간을 결정하는 데 더 복잡한 로직이 필요하다면, 리스너 클래스에 backoff 메서드를 정의할 수 있습니다.

/** * 큐 리스너를 재시도하기 전까지 대기할 시간(초)을 계산합니다. */ public function backoff(OrderShipped $event): int { return 3; }

backoff 메서드에서 배열을 반환하면 "지수적으로 증가하는" 백오프도 손쉽게 구성할 수 있습니다. 아래 예제에서는 첫 번째 재시도까지 1초, 두 번째는 5초, 세 번째는 10초를 대기하며, 그 이후 남은 재시도부터는 계속 10초씩 대기합니다.

/** * 큐 리스너를 재시도하기 전까지 대기할 시간(초)을 계산합니다. * * @return list<int> */ public function backoff(OrderShipped $event): array { return [1, 5, 10]; }

큐 리스너 최대 예외 횟수 지정하기

리스너를 여러 번 재시도하도록 허용하되, release 메서드로 인한 재시도가 아니라 처리되지 않은 예외로 인한 재시도가 특정 횟수를 넘으면 실패로 처리하고 싶을 수 있습니다. 이럴 때는 리스너 클래스에 Tries와 MaxExceptions 속성을 함께 사용하면 됩니다.

<?php namespace App\Listeners; use App\Events\OrderShipped; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\Attributes\MaxExceptions; use Illuminate\Queue\Attributes\Tries; use Illuminate\Queue\InteractsWithQueue; #[Tries(25)] #[MaxExceptions(3)] class SendShipmentNotification implements ShouldQueue { use InteractsWithQueue; /** * 이벤트를 처리합니다. */ public function handle(OrderShipped $event): void { // 이벤트 처리 로직... } }

이 예제에서는 리스너가 최대 25번까지 재시도됩니다. 하지만 처리되지 않은 예외가 3번 발생하면 그 즉시 리스너는 실패로 처리됩니다.

큐 리스너 타임아웃 지정하기

큐 리스너가 대략 얼마나 걸릴지 예상할 수 있는 경우가 많습니다. Laravel에서는 이런 경우를 위해 "타임아웃" 값을 지정할 수 있습니다. 리스너 처리 시간이 지정한 타임아웃 초를 초과하면, 해당 리스너를 처리하던 워커는 오류와 함께 종료됩니다. 리스너 클래스에 Timeout 속성을 사용해 최대 실행 시간(초)을 지정할 수 있습니다.

<?php namespace App\Listeners; use App\Events\OrderShipped; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\Attributes\Timeout; #[Timeout(120)] class SendShipmentNotification implements ShouldQueue { // ... }

타임아웃이 발생했을 때 리스너를 실패로 표시하고 싶다면, 리스너 클래스에 FailOnTimeout 속성을 사용하면 됩니다.

<?php namespace App\Listeners; use App\Events\OrderShipped; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\Attributes\FailOnTimeout; #[FailOnTimeout] class SendShipmentNotification implements ShouldQueue { // ... }

이벤트 디스패치

이벤트를 디스패치하려면 이벤트 클래스에서 정적 메서드 dispatch를 호출하면 됩니다. 이 메서드는 Illuminate\Foundation\Events\Dispatchable 트레이트를 통해 제공되며, dispatch 메서드에 전달한 인자는 그대로 이벤트의 생성자로 전달됩니다:

<?php namespace App\Http\Controllers; use App\Events\OrderShipped; use App\Models\Order; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class OrderShipmentController extends Controller { /** * Ship the given order. */ public function store(Request $request): RedirectResponse { $order = Order::findOrFail($request->order_id); // 배송 처리 로직... OrderShipped::dispatch($order); return redirect('/orders'); } }

특정 조건에서만 이벤트를 디스패치하고 싶다면 dispatchIf와 dispatchUnless 메서드를 사용할 수 있습니다:

OrderShipped::dispatchIf($condition, $order); OrderShipped::dispatchUnless($condition, $order);

NOTE

테스트를 작성할 때는 리스너를 실제로 실행시키지 않고 특정 이벤트가 디스패치되었는지만 검증하고 싶을 때가 많습니다. Laravel이 기본 제공하는 테스트 헬퍼를 사용하면 이런 검증을 손쉽게 할 수 있습니다.

데이터베이스 트랜잭션 커밋 이후에 이벤트 디스패치하기

경우에 따라 현재 진행 중인 데이터베이스 트랜잭션이 커밋된 이후에만 이벤트를 디스패치하고 싶을 수 있습니다. 이럴 때는 이벤트 클래스에 ShouldDispatchAfterCommit 인터페이스를 구현하면 됩니다.

이 인터페이스를 구현하면 Laravel은 현재 진행 중인 데이터베이스 트랜잭션이 커밋될 때까지 이벤트 디스패치를 미룹니다. 만약 트랜잭션이 실패하면 해당 이벤트는 그대로 폐기됩니다. 반대로 이벤트를 디스패치하는 시점에 진행 중인 트랜잭션이 없다면, 이벤트는 즉시 디스패치됩니다:

<?php namespace App\Events; use App\Models\Order; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Events\ShouldDispatchAfterCommit; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; class OrderShipped implements ShouldDispatchAfterCommit { use Dispatchable, InteractsWithSockets, SerializesModels; /** * Create a new event instance. */ public function __construct( public Order $order, ) {} }

이벤트 지연시키기

이벤트 지연(defer) 기능을 사용하면 특정 코드 블록이 완료될 때까지 모델 이벤트의 디스패치와 리스너 실행을 미룰 수 있습니다. 이 기능은 이벤트 리스너가 실행되기 전에 관련된 여러 레코드가 모두 생성되어 있어야 하는 상황에서 특히 유용합니다.

이벤트를 지연시키려면 Event::defer() 메서드에 클로저를 전달하면 됩니다:

use App\Models\User; use Illuminate\Support\Facades\Event; Event::defer(function () { $user = User::create(['name' => 'Victoria Otwell']); $user->posts()->create(['title' => 'My first post!']); });

클로저 내부에서 발생한 모든 이벤트는 클로저 실행이 끝난 이후에 디스패치됩니다. 덕분에 이벤트 리스너는 지연 실행 과정에서 생성된 모든 관련 레코드에 접근할 수 있게 됩니다. 만약 클로저 실행 도중 예외가 발생하면, 지연되었던 이벤트들은 디스패치되지 않습니다.

특정 이벤트만 지연시키고 싶다면, defer 메서드의 두 번째 인자로 이벤트 배열을 전달하면 됩니다:

use App\Models\User; use Illuminate\Support\Facades\Event; Event::defer(function () { $user = User::create(['name' => 'Victoria Otwell']); $user->posts()->create(['title' => 'My first post!']); }, ['eloquent.created: '.User::class]);

NOTE

이벤트를 지연시키더라도 클로저 내부의 코드는 즉시 실행됩니다. 다만 클로저 안에서 dispatch()로 트리거된 이벤트 자체의 처리(리스너 실행)만 클로저가 끝날 때까지 미뤄진다는 점에 유의하세요. 데이터베이스 트랜잭션 커밋 이후 디스패치와는 별개의 개념이며, 두 기능을 함께 사용할 수도 있습니다.

이벤트 구독자(Event Subscribers)

이벤트 구독자 작성하기

이벤트 구독자(Event Subscriber)는 여러 이벤트를 하나의 클래스 안에서 한꺼번에 처리할 수 있게 해주는 클래스입니다. 관련된 이벤트 핸들러들을 리스너 클래스 여러 개로 분산시키는 대신, 하나의 구독자 클래스에 모아서 정의할 수 있어 편리합니다.

구독자 클래스에는 subscribe 메서드를 정의해야 하며, 이 메서드는 이벤트 디스패처(dispatcher) 인스턴스를 인자로 전달받습니다. 전달받은 디스패처의 listen 메서드를 호출해 원하는 만큼 이벤트 리스너를 등록하면 됩니다:

<?php namespace App\Listeners; use Illuminate\Auth\Events\Login; use Illuminate\Auth\Events\Logout; use Illuminate\Events\Dispatcher; class UserEventSubscriber { /** * 사용자 로그인 이벤트를 처리합니다. */ public function handleUserLogin(Login $event): void {} /** * 사용자 로그아웃 이벤트를 처리합니다. */ public function handleUserLogout(Logout $event): void {} /** * 구독자에 대한 리스너를 등록합니다. */ public function subscribe(Dispatcher $events): void { $events->listen( Login::class, [UserEventSubscriber::class, 'handleUserLogin'] ); $events->listen( Logout::class, [UserEventSubscriber::class, 'handleUserLogout'] ); } }

만약 이벤트 리스너 메서드가 구독자 클래스 자체에 정의되어 있다면, subscribe 메서드에서 이벤트와 메서드 이름을 매핑한 배열을 반환하는 방식이 더 간결합니다. 이 경우 Laravel이 구독자의 클래스 이름을 자동으로 인식해서 이벤트 리스너로 등록해 줍니다:

<?php namespace App\Listeners; use Illuminate\Auth\Events\Login; use Illuminate\Auth\Events\Logout; use Illuminate\Events\Dispatcher; class UserEventSubscriber { /** * 사용자 로그인 이벤트를 처리합니다. */ public function handleUserLogin(Login $event): void {} /** * 사용자 로그아웃 이벤트를 처리합니다. */ public function handleUserLogout(Logout $event): void {} /** * 구독자에 대한 리스너를 등록합니다. * * @return array<string, string> */ public function subscribe(Dispatcher $events): array { return [ Login::class => 'handleUserLogin', Logout::class => 'handleUserLogout', ]; } }

이벤트 구독자 등록하기

구독자를 작성한 뒤, 핸들러 메서드가 Laravel의 이벤트 자동 탐색 규칙을 따른다면 별도의 등록 작업 없이 자동으로 등록됩니다.

자동 탐색 규칙을 따르지 않는 경우에는 Event 파사드의 subscribe 메서드를 사용해 수동으로 등록해야 합니다. 보통 이 작업은 애플리케이션의 AppServiceProvider에 있는 boot 메서드 안에서 수행합니다:

<?php namespace App\Providers; use App\Listeners\UserEventSubscriber; use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { /** * 애플리케이션의 서비스를 부트스트랩합니다. */ public function boot(): void { Event::subscribe(UserEventSubscriber::class); } }

NOTE

구독자 클래스는 관련된 이벤트들이 많아질 때 리스너 파일 개수를 줄이는 데 유용합니다. 다만 하나의 구독자에 너무 많은 이벤트를 몰아넣으면 오히려 가독성이 떨어질 수 있으니, 도메인이나 기능 단위로 적절히 나누어 관리하는 것을 권장합니다.

테스트

이벤트를 디스패치하는 코드를 테스트할 때는, 실제로 리스너가 실행되지 않도록 Laravel에 지시하고 싶을 수 있습니다. 리스너 코드 자체는 이벤트를 디스패치하는 코드와는 별개로 직접 테스트할 수 있기 때문입니다. 물론 리스너 자체를 테스트하려면 리스너 인스턴스를 생성한 뒤 테스트 코드에서 handle 메서드를 직접 호출하면 됩니다.

Event 파사드의 fake 메서드를 사용하면 실제 리스너 실행을 막고, 테스트 대상 코드를 실행한 다음, assertDispatched, assertNotDispatched, assertNothingDispatched 메서드로 애플리케이션이 어떤 이벤트를 디스패치했는지 검증할 수 있습니다:

Pest

<?php use App\Events\OrderFailedToShip; use App\Events\OrderShipped; use Illuminate\Support\Facades\Event; test('orders can be shipped', function () { Event::fake(); // 주문 배송 처리... // 이벤트가 디스패치되었는지 검증... Event::assertDispatched(OrderShipped::class); // 이벤트가 두 번 디스패치되었는지 검증... Event::assertDispatched(OrderShipped::class, 2); // 이벤트가 한 번 디스패치되었는지 검증... Event::assertDispatchedOnce(OrderShipped::class); // 이벤트가 디스패치되지 않았는지 검증... Event::assertNotDispatched(OrderFailedToShip::class); // 어떤 이벤트도 디스패치되지 않았는지 검증... Event::assertNothingDispatched(); });

PHPUnit

<?php namespace Tests\Feature; use App\Events\OrderFailedToShip; use App\Events\OrderShipped; use Illuminate\Support\Facades\Event; use Tests\TestCase; class ExampleTest extends TestCase { /** * 주문 배송 테스트. */ public function test_orders_can_be_shipped(): void { Event::fake(); // 주문 배송 처리... // 이벤트가 디스패치되었는지 검증... Event::assertDispatched(OrderShipped::class); // 이벤트가 두 번 디스패치되었는지 검증... Event::assertDispatched(OrderShipped::class, 2); // 이벤트가 한 번 디스패치되었는지 검증... Event::assertDispatchedOnce(OrderShipped::class); // 이벤트가 디스패치되지 않았는지 검증... Event::assertNotDispatched(OrderFailedToShip::class); // 어떤 이벤트도 디스패치되지 않았는지 검증... Event::assertNothingDispatched(); } }

assertDispatched나 assertNotDispatched 메서드에는 클로저를 전달할 수도 있습니다. 이 경우 주어진 "조건 검사"를 통과하는 이벤트가 디스패치되었는지를 검증합니다. 조건을 만족하는 이벤트가 하나라도 디스패치되었다면 검증은 성공합니다:

Event::assertDispatched(function (OrderShipped $event) use ($order) { return $event->order->id === $order->id; });

특정 이벤트에 특정 리스너가 연결되어 있는지만 확인하고 싶다면 assertListening 메서드를 사용할 수 있습니다:

Event::assertListening( OrderShipped::class, SendShipmentNotification::class );

WARNING

Event::fake()를 호출하고 나면 어떤 이벤트 리스너도 실행되지 않습니다. 따라서 모델의 creating 이벤트에서 UUID를 생성하는 경우처럼, 이벤트에 의존하는 모델 팩토리를 테스트에서 사용한다면 팩토리를 사용한 이후에 Event::fake()를 호출해야 합니다.

일부 이벤트만 가짜(Fake)로 처리하기

특정 이벤트에 대해서만 리스너를 가짜로 처리하고 싶다면, 해당 이벤트들을 fake나 fakeFor 메서드에 전달하면 됩니다:

Pest

test('orders can be processed', function () { Event::fake([ OrderCreated::class, ]); $order = Order::factory()->create(); Event::assertDispatched(OrderCreated::class); // 다른 이벤트들은 평소처럼 정상적으로 디스패치됩니다... $order->update([ // ... ]); });

PHPUnit

/** * 주문 처리 테스트. */ public function test_orders_can_be_processed(): void { Event::fake([ OrderCreated::class, ]); $order = Order::factory()->create(); Event::assertDispatched(OrderCreated::class); // 다른 이벤트들은 평소처럼 정상적으로 디스패치됩니다... $order->update([ // ... ]); }

반대로 특정 이벤트들을 제외한 나머지 모든 이벤트를 가짜로 처리하고 싶다면 except 메서드를 사용하면 됩니다:

Event::fake()->except([ OrderCreated::class, ]);

특정 범위에서만 이벤트 가짜 처리하기

테스트의 일부 구간에서만 이벤트 리스너를 가짜로 처리하고 싶다면 fakeFor 메서드를 사용할 수 있습니다:

Pest

<?php use App\Events\OrderCreated; use App\Models\Order; use Illuminate\Support\Facades\Event; test('orders can be processed', function () { $order = Event::fakeFor(function () { $order = Order::factory()->create(); Event::assertDispatched(OrderCreated::class); return $order; }); // 이후에는 이벤트가 정상적으로 디스패치되고 옵저버도 정상 동작합니다... $order->update([ // ... ]); });

PHPUnit

<?php namespace Tests\Feature; use App\Events\OrderCreated; use App\Models\Order; use Illuminate\Support\Facades\Event; use Tests\TestCase; class ExampleTest extends TestCase { /** * 주문 처리 테스트. */ public function test_orders_can_be_processed(): void { $order = Event::fakeFor(function () { $order = Order::factory()->create(); Event::assertDispatched(OrderCreated::class); return $order; }); // 이후에는 이벤트가 정상적으로 디스패치되고 옵저버도 정상 동작합니다... $order->update([ // ... ]); } }

이 문서는 Laravel 공식 문서(MIT)를 한국 개발자를 위해 번역·재구성한 것입니다.

번역일: 2026년 9월 18일