본문 바로가기

이벤트

번역일: 2026년 6월 21일

이벤트

소개

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

이벤트는 애플리케이션의 여러 관심사를 느슨하게 결합(decouple)하는 데 매우 유용합니다. 하나의 이벤트에 여러 리스너를 연결할 수 있고, 각 리스너는 서로 독립적으로 동작합니다.

예를 들어, 주문이 발송될 때마다 Slack 알림을 보내고 싶다고 가정해 봅시다. 이때 주문 처리 코드에 Slack 알림 코드를 직접 섞는 것보다, App\Events\OrderShipped 이벤트를 발생시키고 리스너가 이를 받아 알림을 전송하도록 분리하는 것이 훨씬 깔끔합니다.

이벤트와 리스너 생성

make:eventmake: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', ])

현재 등록된 모든 리스너 목록은 다음 명령어로 확인할 수 있습니다.

php artisan event:list

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

프로덕션 환경에서는 매 요청마다 디렉터리를 스캔하는 대신, optimize 또는 event:cache 명령어로 리스너 목록을 캐시해 두는 것이 좋습니다. 이 명령어는 배포 프로세스의 일부로 실행하는 것을 권장합니다. 캐시를 삭제하려면 event:clear 명령어를 사용하세요.

수동으로 이벤트 등록

자동 탐색 대신, Event 파사드를 사용해 AppServiceProviderboot 메서드에서 직접 이벤트와 리스너를 등록할 수도 있습니다.

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

클로저 리스너

리스너를 별도 클래스로 만들지 않고, AppServiceProviderboot 메서드에서 클로저로 직접 등록할 수도 있습니다.

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)));

익명 큐 리스너 실패를 처리하려면 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; /** * 새 이벤트 인스턴스를 생성합니다. */ public function __construct( public Order $order, ) {} }

이 클래스에는 별도의 로직이 없습니다. 단순히 App\Models\Order 인스턴스를 담는 역할만 합니다. SerializesModels 트레이트는 큐를 사용하는 리스너처럼 이벤트 객체가 PHP의 serialize 함수로 직렬화될 때, Eloquent 모델을 안전하게 처리해 줍니다.

리스너 정의

다음으로 이벤트를 처리하는 리스너를 살펴보겠습니다. 리스너는 handle 메서드에서 이벤트 인스턴스를 받습니다. --event 옵션과 함께 make:listener 명령어를 실행하면 적절한 이벤트 클래스를 자동으로 임포트하고 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 명령어로 생성된 리스너에는 이미 이 인터페이스가 임포트되어 있습니다.

<?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의 deleterelease 메서드에 직접 접근해야 할 경우, 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); } } }

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

데이터베이스 트랜잭션 내에서 큐 리스너가 디스패치되면, 트랜잭션이 커밋되기 전에 큐 워커가 해당 리스너를 실행할 수 있습니다. 이 경우 트랜잭션 내에서 변경하거나 생성한 데이터가 아직 데이터베이스에 반영되지 않아 예기치 않은 오류가 발생할 수 있습니다.

큐 커넥션의 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 미들웨어를 사용하면 리스너 실행 전후에 공통 로직을 재사용할 수 있어 코드 중복을 줄일 수 있습니다. 리스너의 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 인터페이스를 구현하세요. 이 인터페이스가 추가된 리스너는 큐에 push되기 전에 자동으로 암호화됩니다.

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

유니크 이벤트 리스너

WARNING

유니크 리스너는 원자적 잠금을 지원하는 캐시 드라이버가 필요합니다. 현재 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 리스너는 유니크하게 동작합니다. 같은 리스너가 이미 큐에 있어 처리 중이라면 새로운 리스너는 큐에 추가되지 않습니다.

특정 "키"로 유니크 여부를 구분하거나 유니크 유지 시간을 지정하려면 uniqueIduniqueFor 프로퍼티 또는 메서드를 정의하세요.

<?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; } }

위 예시에서 리스너는 라이선스 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 { // ... /** * 유니크 리스너 잠금에 사용할 캐시 드라이버를 반환합니다.

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

번역일: 2026년 6월 21일