이벤트
업데이트됨번역일: 2026년 7월 2일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 6월 20일
- 번역 갱신
- 2026년 7월 2일
이벤트
소개
Laravel의 이벤트 시스템은 옵저버(Observer) 패턴을 깔끔하게 구현한 기능입니다. 애플리케이션 내에서 발생하는 다양한 행위에 이벤트를 연결하고, 그 이벤트를 여러 리스너가 각자의 방식으로 처리하도록 구성할 수 있습니다.
예를 들어 주문이 배송 완료됐을 때 OrderShipped 이벤트를 발생시키고, 이를 구독하는 리스너들이 알림 전송, 로그 기록, 통계 업데이트 등을 각각 독립적으로 처리하게 만들 수 있습니다.
이벤트 클래스는 기본적으로 app/Events 디렉터리에, 리스너 클래스는 app/Listeners 디렉터리에 위치합니다. 프로젝트에 이 디렉터리가 없어도 Artisan 명령어로 이벤트와 리스너를 생성하면 자동으로 만들어지므로 걱정하지 않아도 됩니다.
NOTE
이벤트 시스템은 애플리케이션의 다양한 관심사를 서로 분리(decouple)하는 데 특히 유용합니다. 하나의 이벤트에 여러 리스너를 독립적으로 연결할 수 있어, 기능을 추가하거나 변경할 때 기존 코드를 수정할 필요가 없어집니다.
이벤트 및 리스너 생성
이벤트와 리스너 클래스를 빠르게 만들려면 make:event와 make:listener Artisan 명령어를 사용하세요.
php artisan make:event PodcastProcessedphp artisan make:listener SendPodcastNotification --event=PodcastProcessed--event 옵션을 지정하면 리스너 클래스가 해당 이벤트를 자동으로 타입힌트합니다. 생성된 클래스 파일은 각각 app/Events와 app/Listeners 디렉터리에 저장됩니다.
이벤트 및 리스너 등록
이벤트 자동 감지
Laravel은 기본적으로 app/Listeners 디렉터리를 스캔하여 이벤트 리스너를 자동으로 감지하고 등록합니다. handle 또는 __invoke로 시작하는 메서드의 타입힌트를 분석해서, 어떤 이벤트를 처리하는지 자동으로 파악합니다.
use App\Events\PodcastProcessed;
class SendPodcastNotification
{
public function handle(PodcastProcessed $event): void
{
// ...
}
}이렇게 타입힌트만 지정해 두면 별도의 등록 없이도 리스너가 자동으로 동작합니다.
리스너 스캔 경로를 직접 지정하고 싶다면 AppServiceProvider의 boot 메서드에서 Event::discoverWithin을 호출하세요.
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::discoverWithin([__DIR__.'/../Listeners']);
}NOTE
자동 감지 기능은 성능을 위해 이벤트-리스너 매핑을 캐싱합니다. 리스너를 추가하거나 변경한 후에는 php artisan event:clear 명령어로 캐시를 지워야 변경사항이 반영됩니다.
이벤트 수동 등록
자동 감지 대신 직접 이벤트와 리스너를 등록하려면 AppServiceProvider의 boot 메서드에서 Event 파사드를 사용하세요.
use App\Events\PodcastProcessed;
use App\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::listen(
PodcastProcessed::class,
SendPodcastNotification::class,
);
}event:list Artisan 명령어로 현재 등록된 모든 이벤트와 리스너를 확인할 수 있습니다.
php artisan event:list클로저 리스너
별도의 리스너 클래스를 만들지 않고 클로저로 간단하게 리스너를 등록할 수도 있습니다.
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()->addSeconds(10)));큐 처리 중 오류가 발생했을 때의 처리도 지정할 수 있습니다.
use Throwable;
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
})->catch(function (PodcastProcessed $event, Throwable $e) {
// 큐 리스너 실패 시 처리...
}));와일드카드 리스너
*를 와일드카드로 사용하면 여러 이벤트를 하나의 리스너에서 처리할 수 있습니다. 와일드카드 리스너의 첫 번째 인수는 이벤트 이름, 두 번째 인수는 이벤트 데이터 배열입니다.
Event::listen('event.*', function (string $eventName, array $data) {
// ...
});이벤트 정의
이벤트 클래스는 해당 이벤트와 관련된 데이터를 담는 간단한 데이터 컨테이너입니다. 예를 들어 OrderShipped 이벤트가 Eloquent 모델 객체를 전달받는다고 가정하면 다음과 같이 작성합니다.
<?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,
) {}
}SerializesModels 트레이트를 사용하면 이벤트가 큐를 통해 처리될 때 Eloquent 모델이 올바르게 직렬화·역직렬화됩니다.
리스너 정의
리스너 클래스는 handle 메서드에서 이벤트 인스턴스를 받아 처리합니다. make:listener 명령어로 생성하면 해당 이벤트 클래스가 자동으로 타입힌트됩니다.
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
class SendShipmentNotification
{
/**
* 이벤트 리스너를 생성합니다.
*/
public function __construct() {}
/**
* 이벤트를 처리합니다.
*/
public function handle(OrderShipped $event): void
{
// $event->order로 주문 정보에 접근
}
}NOTE
리스너의 생성자에 필요한 의존성을 타입힌트하면 Laravel의 서비스 컨테이너가 자동으로 주입해 줍니다.
이벤트 전파 중단
리스너에서 false를 반환하면 해당 이벤트가 다른 리스너로 전달되는 것을 중단할 수 있습니다.
public function handle(OrderShipped $event): false
{
// 이벤트 전파 중단
return false;
}큐를 사용하는 이벤트 리스너
이메일 발송이나 외부 API 호출처럼 시간이 걸리는 작업을 리스너에서 처리할 때는 큐를 활용하면 좋습니다. 리스너를 큐로 처리하려면 ShouldQueue 인터페이스를 구현하기만 하면 됩니다.
make:listener 명령어로 생성된 리스너 클래스에는 이미 이 인터페이스가 임포트되어 있습니다.
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
// ...
}ShouldQueue를 구현한 리스너는 이벤트가 발생할 때 자동으로 큐에 등록됩니다. 리스너가 실행 중 예외가 발생하더라도 큐의 재시도 설정에 따라 자동으로 재처리됩니다.
큐 연결, 큐 이름, 지연 시간 등의 속성을 리스너에 직접 설정할 수도 있습니다.
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendShipmentNotification implements ShouldQueue
{
/**
* 사용할 큐 연결 이름
*
* @var string|null
*/
public $connection = 'redis';
/**
* 사용할 큐 이름
*
* @var string|null
*/
public $queue = 'listeners';
/**
* Job 실행 전 대기 시간(초)
*
* @var int
*/
public $delay = 60;
}실행 시점에 동적으로 큐 연결이나 이름을 결정하고 싶다면 메서드로 정의하세요.
public function viaConnection(): string
{
return 'redis';
}
public function viaQueue(): string
{
return 'listeners';
}조건부 큐 처리
특정 조건에 따라 큐 처리 여부를 결정하고 싶다면 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 인스턴스에 직접 접근해야 한다면 InteractsWithQueue 트레이트를 사용하세요. 이 트레이트는 release(재시도 예약), delete(삭제) 등의 메서드를 제공합니다.
<?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 (true) {
$this->release(30); // 30초 후 재시도
}
}
}큐 리스너와 데이터베이스 트랜잭션
데이터베이스 트랜잭션 안에서 이벤트가 디스패치될 경우, 큐 리스너가 트랜잭션이 커밋되기 전에 실행될 수 있습니다. 이 경우 트랜잭션에서 수정된 데이터가 아직 데이터베이스에 반영되지 않은 상태이므로, 예기치 않은 오류가 발생할 수 있습니다.
큐 연결의 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과 데이터베이스 트랜잭션 문서를 참고하세요.
큐 리스너 미들웨어
큐 리스너에도 미들웨어를 적용할 수 있습니다. 리스너 클래스에 middleware 메서드를 정의하면 됩니다.
use Illuminate\Queue\Middleware\RateLimited;
public function middleware(): array
{
return [new RateLimited('listeners')];
}암호화된 큐 리스너
Laravel은 큐 페이로드를 암호화하는 기능을 제공합니다. 리스너 클래스에 ShouldBeEncrypted 인터페이스를 구현하면 해당 리스너의 큐 페이로드가 자동으로 암호화됩니다.
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
class SendShipmentNotification implements ShouldQueue, ShouldBeEncrypted
{
// ...
}중복 방지 이벤트 리스너
동일한 이벤트에 대해 리스너가 여러 번 큐에 등록되는 것을 방지하려면 ShouldBeUnique 인터페이스를 구현하세요. 이미 동일한 리스너가 큐에 있으면 새로 등록되지 않습니다.
<?php
namespace App\Listeners;
use App\Events\OrderCreated;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class RewardGiftCard implements ShouldQueue, ShouldBeUnique
{
// ...
}기본적으로 유니크 키는 리스너의 클래스 이름이 사용됩니다. 키와 잠금 유지 시간을 커스터마이징하고 싶다면 uniqueId와 uniqueFor 속성(또는 메서드)을 정의하세요.
public $uniqueFor = 3600; // 1시간 동안 유니크 유지
public function uniqueId(): string
{
return $this->event->order->id; // 주문 ID 기준으로 유니크 처리
}NOTE
ShouldBeUnique는 애플리케이션의 기본 캐시 드라이버를 통해 잠금을 관리합니다. 분산 서버 환경에서는 Redis와 같은 중앙 집중식 캐시 드라이버를 사용해야 합니다.
처리 시작 전까지 유니크 유지
기본적으로 ShouldBeUnique는 Job이 처리를 시작하거나 최대 재시도 횟수에 도달하면 잠금이 해제됩니다. 처리가 완료될 때까지 잠금을 유지하고 싶다면 ShouldBeUniqueUntilProcessing 인터페이스를 대신 사용하세요.
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
class RewardGiftCard implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
// ...
}유니크 리스너 잠금
유니크 잠금에 사용할 캐시 드라이버를 직접 지정하려면 uniqueVia 메서드를 정의하세요.
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
public function uniqueVia(): Repository
{
return Cache::driver('redis');
}실패한 Job 처리
큐 리스너가 최대 재시도 횟수를 초과하여 실패했을 때 failed 메서드가 호출됩니다. 이 메서드에서 알림 전송이나 상태 초기화 등의 실패 후 처리를 구현할 수 있습니다.
<?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
{
// ...
}
public function failed(OrderShipped $event, Throwable $exception): void
{
// 리스너 최종 실패 시 처리 (예: 관리자에게 알림 전송)
}
}최대 재시도 횟수 설정
$tries 속성으로 최대 재시도 횟수를, retryUntil 메서드로 재시도 만료 시간을 지정할 수 있습니다.
/**
* 최대 재시도 횟수
*
* @var int
*/
public $tries = 5;use DateTime;
public function retryUntil(): DateTime
{
return now()->addMinutes(5);
}이벤트 디스패치
이벤트를 발생시키려면 이벤트 클래스의 정적 dispatch 메서드를 호출하세요. 이 메서드는 Dispatchable 트레이트에 의해 제공됩니다.
<?php
namespace App\Http\Controllers;
use App\Events\OrderShipped;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class OrderShipmentController extends Controller
{
public function store(Request $request): Response
{
$order = Order::findOrFail($request->order_id);
// 주문 배송 처리 로직...
OrderShipped::dispatch($order);
return response()->noContent();
}
}조건에 따라 이벤트를 발생시키고 싶다면 dispatchIf나 dispatchUnless를 사용하세요.
OrderShipped::dispatchIf($condition, $order);
OrderShipped::dispatchUnless($condition, $order);NOTE
테스트 시에는 실제 리스너를 실행하지 않고도 이벤트 디스패치를 검증할 수 있습니다. Laravel의 테스트 헬퍼를 활용하세요.
데이터베이스 트랜잭션 이후 이벤트 디스패치
데이터베이스 트랜잭션이 완전히 커밋된 이후에만 이벤트를 발생시키고 싶을 때는 이벤트 클래스에 ShouldDispatchAfterCommit 인터페이스를 구현하세요.
<?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;
public function __construct(
public Order $order,
) {}
}이렇게 구현하면 현재 진행 중인 트랜잭션이 커밋되기 전까지 이벤트 디스패치가 보류됩니다. 트랜잭션이 롤백되면 이벤트는 아예 발생하지 않습니다. 활성 트랜잭션이 없다면 이벤트는 즉시 디스패치됩니다.
이벤트 지연 디스패치
이벤트를 즉시 발생시키지 않고, HTTP 응답이 클라이언트에게 전송된 이후에 처리하고 싶다면 dispatchAfterResponse 메서드를 사용하세요.
use App\Events\OrderShipped;
OrderShipped::dispatchAfterResponse($order);또는 클로저 방식으로도 지정할 수 있습니다.
use App\Events\OrderShipped;
dispatch(function () use ($order) {
OrderShipped::dispatch($order);
})->afterResponse();이 방식을 활용하면 긴 처리 시간을 사용자 응답 이후로 미뤄 UX를 개선할 수 있습니다.
이벤트 구독자
이벤트 구독자 작성
이벤트 구독자(Event Subscriber)는 하나의 클래스 안에서 여러 이벤트를 처리할 수 있는 방법입니다. 구독자 클래스는 subscribe 메서드를 정의하며, 이 메서드에서 이벤트와 처리 메서드를 연결합니다.
<?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 메서드에서 배열을 반환하는 방식으로도 등록할 수 있습니다.
public function subscribe(Dispatcher $events): array
{
return [
Login::class => 'handleUserLogin',
Logout::class => 'handleUserLogout',
];
}이벤트 구독자 등록
구독자 클래스를 작성했으면 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);
}
}테스트
이벤트를 디스패치하는 코드를 테스트할 때, 실제 리스너를 실행하지 않도록 막고 싶은 경우 Event::fake()를 사용하세요. 리스너는 실행되지 않지만, 어떤 이벤트가 디스패치됐는지는 검증할 수 있습니다.
<?php
use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Support\Facades\Event;
test('주문을 처리할 수 있다', function () {
Event::fake();
// 주문 처리 로직 수행...
// 특정 이벤트가 디스패치됐는지 확인
Event::assertDispatched(OrderShipped::class);
// 이벤트가 두 번 디스패치됐는지 확인
Event::assertDispatched(OrderShipped::class, 2);
// 이벤트가 디스패치되지 않았는지 확인
Event::assertNotDispatched(OrderFailedToShip::class);
// 어떤 이벤트도 디스패치되지 않았는지 확인
Event::assertNothingDispatched();
});클로저를 전달하면 특정 조건을 만족하는 이벤트가 디스패치됐는지 세부적으로 확인할 수 있습니다.
Event::assertDispatched(function (OrderShipped $event) use ($order) {
return $event->order->id === $order->id;
});특정 이벤트에 리스너가 연결되어 있는지 확인하려면 assertListening을 사용하세요.
Event::assertListening(
OrderShipped::class,
SendShipmentNotification::class
);WARNING
Event::fake()를 호출하면 모든 이벤트 리스너의 실행이 중단됩니다. 모델의 creating, updating 이벤트처럼 팩토리가 의존하는 이벤트가 있다면, 팩토리로 모델을 생성한 이후에 Event::fake()를 호출하세요.
일부 이벤트만 페이크 처리하기
특정 이벤트만 페이크 처리하고 나머지는 실제로 실행하려면 Event::fake()에 이벤트 클래스 배열을 전달하세요.
test('주문을 처리할 수 있다', function () {
Event::fake([
OrderCreated::class,
]);
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
// 나머지 이벤트는 실제로 실행됨
});반대로 특정 이벤트를 제외하고 나머지를 모두 페이크 처리하려면 except를 사용하세요.
Event::fake()->except([
OrderCreated::class,
]);스코프 기반 이벤트 페이크
테스트의 일부 구간에서만 이벤트 페이크를 적용하고 싶다면 fakeFor를 사용하세요. 클로저 안에서만 이벤트가 페이크 처리되고, 클로저 밖에서는 정상적으로 실행됩니다.
<?php
use App\Events\OrderCreated;
use App\Models\Order;
use Illuminate\Support\Facades\Event;
test('주문을 처리할 수 있다', function () {
$order = Event::fakeFor(function () {
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
return $order;
});
// 이 시점 이후에는 이벤트가 정상 실행되고 옵저버도 동작함
$order->update([/* ... */]);
});이벤트
소개
Laravel의 이벤트 시스템은 옵저버 패턴을 간결하게 구현하며, 애플리케이션 내에서 발생하는 다양한 사건을 구독하고 처리할 수 있게 해줍니다. 이벤트 클래스는 보통 app/Events 디렉터리에, 리스너는 app/Listeners 디렉터리에 저장됩니다. 아직 이 디렉터리가 없어도 걱정하지 않아도 됩니다. Artisan 명령어로 이벤트와 리스너를 생성하면 자동으로 만들어집니다.
이벤트는 애플리케이션의 여러 관심사를 분리하는 데 효과적입니다. 하나의 이벤트에 여러 리스너를 연결할 수 있고, 각 리스너는 서로 독립적으로 동작합니다. 예를 들어, 주문이 발송될 때마다 Slack으로 알림을 보내고 싶다고 가정해 봅시다. 이때 주문 처리 로직 안에 Slack 알림 코드를 직접 작성하는 대신, App\Events\OrderShipped 이벤트를 발생시키고, 별도의 리스너가 이를 받아 Slack 알림을 전송하도록 구성하면 코드를 깔끔하게 분리할 수 있습니다.
이벤트
이벤트와 리스너 생성하기
이벤트와 리스너 클래스는 make:event, make:listener Artisan 명령으로 빠르게 생성할 수 있습니다.
php artisan make:event PodcastProcessedphp artisan make:listener SendPodcastNotification --event=PodcastProcessed인수 없이 명령을 실행하면 Laravel이 대화형으로 클래스 이름을 물어봅니다. 리스너를 생성할 때는 어떤 이벤트를 수신할지도 함께 묻습니다.
php artisan make:eventphp artisan make:listener이벤트
이벤트와 리스너 등록하기
이벤트 자동 감지
Laravel은 기본적으로 애플리케이션의 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 명령을 사용하세요.
동적 이벤트 자동 감지
특정 리스너를 상황에 따라 동적으로 등록 여부를 결정하고 싶다면, 리스너 클래스에 ShouldBeDiscovered 인터페이스를 구현하고 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');
}
}이벤트 수동 등록
자동 감지 방식 대신 직접 이벤트와 리스너를 연결하고 싶다면, AppServiceProvider의 boot 메서드에서 Event 파사드를 사용해 수동으로 등록할 수 있습니다.
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 함수로 클로저를 감싸주세요. 그러면 Laravel이 해당 리스너를 큐 워커를 통해 비동기로 실행합니다.
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)));큐에 등록된 익명 리스너가 실패했을 때 처리할 로직이 필요하다면, 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
와일드카드 리스너는 특정 이벤트 네임스페이스 전체를 로깅하거나 모니터링할 때 유용합니다. 단, 타입 힌트 기반의 자동 감지 방식과는 달리 이벤트 이름 문자열을 직접 다루므로, 복잡한 로직보다는 디버깅이나 감사(audit) 용도로 활용하는 것이 적합합니다.
이벤트 정의하기
이벤트 클래스는 해당 이벤트와 관련된 정보를 담는 데이터 컨테이너입니다. 예를 들어, 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
이벤트 클래스 자체에는 로직을 넣지 않는 것이 원칙입니다. 실제 처리는 리스너에 위임하고, 이벤트는 필요한 데이터만 전달하는 역할에 집중하세요.
리스너 정의하기
이벤트 리스너는 handle 메서드에서 이벤트 인스턴스를 받아 처리합니다. make:listener Artisan 명령에 --event 옵션을 함께 사용하면, 해당 이벤트 클래스를 자동으로 임포트하고 handle 메서드에 타입힌트까지 작성해 줍니다. handle 메서드 내부에서 이벤트에 응답하는 데 필요한 모든 작업을 수행하면 됩니다.
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
class SendShipmentNotification
{
/**
* 이벤트 리스너 생성자
*/
public function __construct() {}
/**
* 이벤트 처리
*/
public function handle(OrderShipped $event): void
{
// $event->order 로 주문 정보에 접근...
}
}NOTE
이벤트 리스너의 생성자에도 필요한 의존성을 타입힌트로 선언할 수 있습니다. 모든 이벤트 리스너는 Laravel 서비스 컨테이너를 통해 해석되므로, 의존성은 자동으로 주입됩니다.
이벤트 전파 중단하기
때로는 특정 리스너에서 이벤트 처리를 마친 뒤, 이후 리스너로 이벤트가 전달되지 않도록 막고 싶을 수 있습니다. 이럴 때는 리스너의 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 어트리뷰트를 사용하세요.
<?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 처리 전 대기할 초(seconds)를 반환합니다.
*/
public function withDelay(OrderShipped $event): int
{
return $event->highPriority ? 0 : 60;
}조건부 큐 처리
런타임 데이터에 따라 리스너를 큐에 넣을지 여부를 결정해야 할 때가 있습니다. 이럴 때는 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 직접 조작
리스너에서 큐 Job의 delete나 release 메서드를 직접 호출해야 한다면, Illuminate\Queue\InteractsWithQueue 트레이트를 사용하세요. 이 트레이트는 make:listener로 생성한 리스너에 기본으로 임포트되어 있습니다.
<?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);
}
}
}큐 리스너와 데이터베이스 트랜잭션
데이터베이스 트랜잭션 내에서 큐 리스너가 디스패치되면, 트랜잭션이 커밋되기 전에 큐 워커가 해당 리스너를 처리할 수 있습니다. 이 경우 트랜잭션 내에서 변경한 모델이나 DB 레코드가 아직 데이터베이스에 반영되지 않은 상태일 수 있습니다. 트랜잭션 내에서 생성한 레코드는 아직 DB에 존재하지 않을 수도 있습니다. 리스너가 이런 데이터에 의존한다면 예기치 않은 오류가 발생할 수 있습니다.
큐 커넥션의 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];
}
}암호화된 큐 리스너
큐에 적재되는 리스너 데이터의 개인정보 보호와 무결성이 필요하다면 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
유니크 리스너는 원자적 잠금(atomic locks)을 지원하는 캐시 드라이버가 필요합니다. 현재 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
{
/**
* 유니크 잠금이 해제될 때까지의 초(seconds).
*
* @var int
*/
public $uniqueFor = 3600;
public function __invoke(LicenseSaved $event): void
{
// ...
}
/**
* 리스너의 유니크 ID를 반환합니다.
*/
public function uniqueId(LicenseSaved $event): string
{
return 'listener:'.$event->license->id;
}
}위 예시에서는 라이선스 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 키로 잠금을 획득하려 시도합니다. 잠금을 이미 보유 중인 경우 리스너는 큐에 추가되지 않습니다. 잠금은 처리 완료 또는 모든 재시도 실패 후 해제됩니다. 기본적으로 기본 캐시 드라이버가 사용되며, 다른 드라이버를 사용하려면 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
단순히 리스너의 동시 처리만 제한하려면 WithoutOverlapping Job 미들웨어를 사용하세요.
실패한 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가 모두 정의된 경우 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;
}배열을 반환하면 지수적(exponential) 백오프를 쉽게 구성할 수 있습니다. 아래 예시에서 1회 재시도는 1초, 2회는 5초, 3회 이후는 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번 발생하면 즉시 실패로 처리됩니다.
타임아웃 지정
리스너 처리에 예상 소요 시간이 있다면 타임아웃을 설정해 두는 것이 좋습니다. 지정된 시간(초)을 초과하면 워커는 오류와 함께 종료됩니다. 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
{
// ...
}이벤트 발행(Dispatching)
이벤트를 발행하려면 이벤트 클래스의 정적 메서드 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
{
/**
* 주문을 배송 처리합니다.
*/
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의 내장 테스트 헬퍼를 사용하면 이를 간단하게 처리할 수 있습니다.
데이터베이스 트랜잭션 커밋 후 이벤트 발행
데이터베이스 트랜잭션이 완전히 커밋된 이후에만 이벤트를 발행하고 싶을 때가 있습니다. 예를 들어 주문 레코드가 DB에 실제로 저장된 뒤에야 배송 알림을 보내야 하는 경우가 이에 해당합니다.
이런 경우 이벤트 클래스에 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;
/**
* 새 이벤트 인스턴스를 생성합니다.
*/
public function __construct(
public Order $order,
) {}
}이벤트 지연 발행(Deferring Events)
이벤트 지연(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' => '첫 번째 포스트!']);
});클로저 내에서 발생한 모든 이벤트는 클로저 실행이 완료된 후에 발행됩니다. 덕분에 리스너는 지연 실행 중에 생성된 연관 레코드 전체에 접근할 수 있습니다. 클로저 내부에서 예외가 발생하면 지연된 이벤트는 발행되지 않습니다.
특정 이벤트만 지연하고 싶다면 defer 메서드의 두 번째 인수로 이벤트 이름 배열을 전달하면 됩니다.
use App\Models\User;
use Illuminate\Support\Facades\Event;
Event::defer(function () {
$user = User::create(['name' => 'Victoria Otwell']);
$user->posts()->create(['title' => '첫 번째 포스트!']);
}, ['eloquent.created: '.User::class]);이벤트 구독자 (Event Subscribers)
이벤트 구독자 작성하기
이벤트 구독자(Event Subscriber)는 하나의 클래스 안에서 여러 이벤트를 한꺼번에 구독할 수 있는 클래스입니다. 관련된 이벤트 핸들러를 하나의 파일로 모아 관리하고 싶을 때 유용합니다.
구독자 클래스에는 subscribe 메서드를 정의해야 하며, 이 메서드는 이벤트 디스패처 인스턴스를 인자로 받습니다. 디스패처의 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 Discovery) 규칙을 따르는 핸들러 메서드는 자동으로 등록됩니다. 그렇지 않은 경우에는 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);
}
}테스트
이벤트를 디스패치하는 코드를 테스트할 때, 리스너가 실제로 실행되지 않도록 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 또는 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([
// ...
]);
}
}