이벤트
번역일: 2026년 7월 2일
이벤트
소개
Laravel의 이벤트 시스템은 옵저버 패턴을 간단하게 구현한 것입니다. 애플리케이션 내에서 발생하는 다양한 상황에 이벤트를 정의하고, 리스너를 통해 그에 반응하는 코드를 분리해서 작성할 수 있습니다. 이벤트 클래스는 보통 app/Events 디렉터리에, 리스너 클래스는 app/Listeners 디렉터리에 저장합니다. 프로젝트를 처음 만들었을 때 이 디렉터리들이 없더라도, Artisan 명령어로 이벤트나 리스너를 생성하면 자동으로 만들어집니다.
이벤트는 애플리케이션의 여러 관심사를 느슨하게 연결(decouple)하는 데 매우 유용합니다. 예를 들어, 주문이 배송될 때 Slack으로 알림을 보내고 싶다고 가정해 봅시다. 주문 처리 코드 안에 Slack 알림 코드를 직접 넣는 대신, App\Events\OrderShipped 이벤트를 발생시키고 리스너가 이를 받아 알림을 전송하도록 분리할 수 있습니다. 하나의 이벤트에 여러 리스너를 붙일 수 있고, 각 리스너는 서로 독립적으로 동작합니다.
이벤트와 리스너 등록
App\Providers\EventServiceProvider는 애플리케이션의 모든 이벤트 리스너를 한곳에서 관리하는 편리한 장소입니다. listen 프로퍼티에 이벤트 클래스(키)와 리스너 클래스(값) 배열을 정의합니다. 예를 들어 OrderShipped 이벤트를 등록하면 다음과 같습니다.
use App\Events\OrderShipped;
use App\Listeners\SendShipmentNotification;
/**
* 애플리케이션의 이벤트-리스너 매핑
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
OrderShipped::class => [
SendShipmentNotification::class,
],
];NOTE
event:list 명령어를 사용하면 현재 애플리케이션에 등록된 모든 이벤트와 리스너 목록을 확인할 수 있습니다.
이벤트와 리스너 생성
매번 파일을 직접 만드는 것은 번거롭습니다. EventServiceProvider의 $listen 배열에 이벤트와 리스너를 추가한 뒤, event:generate Artisan 명령어를 실행하면 아직 존재하지 않는 이벤트·리스너 파일을 자동으로 생성해 줍니다.
php artisan event:generate개별로 생성하고 싶다면 make:event, make:listener 명령어를 사용하세요.
php artisan make:event PodcastProcessedphp artisan make:listener SendPodcastNotification --event=PodcastProcessed이벤트 수동 등록
일반적으로는 EventServiceProvider의 $listen 배열을 통해 등록하지만, boot 메서드 안에서 클래스 기반 또는 클로저 기반으로 직접 등록할 수도 있습니다.
use App\Events\PodcastProcessed;
use App\Listeners\SendPodcastNotification;
use Illuminate\Support\Facades\Event;
/**
* 그 외 이벤트 등록
*/
public function boot(): void
{
Event::listen(
PodcastProcessed::class,
SendPodcastNotification::class,
);
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)));큐 리스너가 실패했을 때 처리하고 싶다면 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) {
// ...
});이벤트 자동 감지
$listen 배열에 직접 등록하는 대신, 이벤트 자동 감지(Event Discovery)를 활성화할 수 있습니다. 이 기능을 켜면 Laravel이 app/Listeners 디렉터리를 스캔하여 리스너를 자동으로 등록합니다. EventServiceProvider에 명시적으로 정의한 이벤트들도 함께 등록됩니다.
Laravel은 PHP의 리플렉션을 사용해 리스너 클래스의 메서드를 분석합니다. handle 또는 __invoke로 시작하는 메서드가 있고 파라미터에 이벤트 클래스가 타입힌트 되어 있으면 자동으로 리스너로 등록합니다.
use App\Events\PodcastProcessed;
class SendPodcastNotification
{
/**
* 이벤트를 처리합니다.
*/
public function handle(PodcastProcessed $event): void
{
// ...
}
}이벤트 자동 감지는 기본적으로 비활성화되어 있습니다. EventServiceProvider에서 shouldDiscoverEvents 메서드를 오버라이드하여 활성화하세요.
/**
* 이벤트와 리스너를 자동으로 감지할지 결정합니다.
*/
public function shouldDiscoverEvents(): bool
{
return true;
}기본적으로 app/Listeners 디렉터리만 스캔합니다. 추가 디렉터리를 스캔하려면 discoverEventsWithin 메서드를 오버라이드하세요.
/**
* 이벤트 감지에 사용할 리스너 디렉터리 목록
*
* @return array<int, string>
*/
protected function discoverEventsWithin(): array
{
return [
$this->app->path('Listeners'),
];
}프로덕션에서의 이벤트 자동 감지
프로덕션 환경에서는 매 요청마다 리스너 디렉터리를 스캔하는 것이 비효율적입니다. 배포 과정에서 event:cache 명령어를 실행하여 이벤트와 리스너 매핑을 캐시해 두세요. 캐시를 삭제하려면 event:clear 명령어를 사용합니다.
php artisan event:cachephp artisan event:clear이벤트 정의
이벤트 클래스는 기본적으로 이벤트와 관련된 데이터를 담는 컨테이너입니다. 예를 들어 App\Events\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,
) {}
}이벤트 클래스는 비즈니스 로직을 담지 않습니다. 단순히 App\Models\Order 인스턴스를 담는 그릇 역할을 합니다. SerializesModels 트레이트는 큐 리스너 등에서 이벤트 객체가 직렬화될 때 Eloquent 모델을 올바르게 처리해 줍니다.
리스너 정의
리스너 클래스는 handle 메서드에서 이벤트 인스턴스를 받아 필요한 작업을 수행합니다. event:generate나 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 서비스 컨테이너를 통해 해석되므로 의존성이 자동으로 주입됩니다.
이벤트 전파 중단
특정 리스너에서 다른 리스너로의 이벤트 전파를 멈추고 싶다면 handle 메서드에서 false를 반환하면 됩니다.
큐를 사용하는 이벤트 리스너
이메일 전송이나 외부 HTTP 요청처럼 시간이 걸리는 작업을 리스너에서 수행해야 할 때는 큐를 활용하는 것이 좋습니다. 큐 리스너를 사용하기 전에 큐 설정을 완료하고 큐 워커를 실행해야 합니다.
리스너를 큐로 실행하려면 ShouldQueue 인터페이스를 구현하기만 하면 됩니다.
<?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;
class SendShipmentNotification implements ShouldQueue
{
/**
* Job을 전송할 큐 커넥션 이름
*
* @var string|null
*/
public $connection = 'sqs';
/**
* Job을 전송할 큐 이름
*
* @var string|null
*/
public $queue = 'listeners';
/**
* Job 처리 전 대기 시간(초)
*
* @var int
*/
public $delay = 60;
}런타임에 동적으로 결정해야 한다면 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 메서드를 정의하세요. 이 메서드가 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 트레이트를 사용하세요. 이 트레이트는 Artisan 명령어로 생성된 리스너에 기본으로 포함되어 있습니다.
<?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);
}
}
}큐 리스너와 데이터베이스 트랜잭션
데이터베이스 트랜잭션 안에서 큐 리스너가 디스패치되면, 트랜잭션이 커밋되기 전에 큐 워커가 Job을 처리할 수 있습니다. 이 경우 트랜잭션 내에서 변경하거나 생성한 데이터가 아직 데이터베이스에 반영되지 않아 예상치 못한 오류가 발생할 수 있습니다.
NOTE
이런 상황이 자주 발생한다면 큐 커넥션의 after_commit 옵션을 true로 설정하는 것이 가장 간단한 해결책입니다.
after_commit 옵션이 false인 상태에서도 특정 리스너만 트랜잭션 커밋 후에 처리되도록 하려면, ShouldHandleEventsAfterCommit 인터페이스를 구현하세요.
<?php
namespace App\Listeners;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue, ShouldHandleEventsAfterCommit
{
use InteractsWithQueue;
}NOTE
이 주제에 대한 자세한 내용은 큐 Job과 데이터베이스 트랜잭션 문서를 참고하세요.
실패한 Job 처리
큐 리스너가 최대 시도 횟수를 초과하면 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
{
// ...
}
}최대 재시도 횟수 지정
무한 재시도를 방지하기 위해 $tries 프로퍼티로 최대 시도 횟수를 지정할 수 있습니다.
<?php
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* 최대 시도 횟수
*
* @var int
*/
public $tries = 5;
}횟수 대신 시간으로 제한하려면 retryUntil 메서드를 정의하세요. 이 메서드가 반환하는 시각 이후로는 더 이상 재시도하지 않습니다.
use DateTime;
/**
* 리스너의 타임아웃 시각을 반환합니다.
*/
public function retryUntil(): DateTime
{
return now()->addMinutes(5);
}이벤트 디스패치
이벤트를 발생시키려면 이벤트 클래스의 정적 dispatch 메서드를 호출하세요. 이 메서드는 Illuminate\Foundation\Events\Dispatchable 트레이트가 제공합니다. dispatch에 전달한 인자는 이벤트 생성자로 그대로 전달됩니다.
<?php
namespace App\Http\Controllers;
use App\Events\OrderShipped;
use App\Http\Controllers\Controller;
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의 내장 테스트 헬퍼가 이를 지원합니다.
데이터베이스 트랜잭션 커밋 후 이벤트 디스패치
트랜잭션이 성공적으로 커밋된 이후에만 이벤트를 디스패치하고 싶다면 이벤트 클래스에 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,
) {}
}이벤트 구독자
이벤트 구독자 작성
이벤트 구독자는 하나의 클래스 안에서 여러 이벤트에 대한 리스너를 한꺼번에 정의할 수 있습니다. 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',
];
}
}이벤트 구독자 등록
구독자 클래스를 작성했다면 EventServiceProvider의 $subscribe 프로퍼티에 등록합니다.
<?php
namespace App\Providers;
use App\Listeners\UserEventSubscriber;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* 이벤트-리스너 매핑
*
* @var array
*/
protected $listen = [
// ...
];
/**
* 등록할 구독자 클래스 목록
*
* @var array
*/
protected $subscribe = [
UserEventSubscriber::class,
];
}테스트
이벤트를 디스패치하는 코드를 테스트할 때, 리스너를 실제로 실행하지 않아도 되는 경우가 많습니다. 리스너 자체는 별도로 직접 테스트할 수 있기 때문입니다. Event 파사드의 fake 메서드를 사용하면 리스너 실행을 막고, 이벤트 디스패치 여부를 assertDispatched, assertNotDispatched, assertNothingDispatched로 검증할 수 있습니다.
<?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::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 메서드에 이벤트 클래스 배열을 전달하세요.
/**
* 주문 처리 테스트
*/
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 메서드를 사용하세요. 클로저 내부에서는 리스너가 실행되지 않고, 클로저 밖에서는 이벤트와 옵저버가 정상적으로 동작합니다.
<?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([...]);
}
}