본문 바로가기

이벤트

번역일: 2026년 6월 21일

이벤트

소개

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

이벤트는 애플리케이션의 여러 관심사를 느슨하게 결합(decoupling) 하는 데 매우 유용합니다. 하나의 이벤트에 여러 리스너를 연결할 수 있고, 각 리스너는 서로 의존하지 않습니다. 예를 들어, 주문이 발송될 때마다 카카오 알림톡이나 슬랙 메시지를 보내고 싶다고 가정해 봅시다. 주문 처리 코드에 알림 코드를 직접 섞는 대신, 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은 기본적으로 app/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 Artisan 명령어로 리스너 목록을 캐싱해 두면 이벤트 등록 속도가 빨라집니다. 보통 배포 프로세스에 포함시켜 실행합니다. 캐시를 초기화하려면 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) { // ... }); }

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

클로저 리스너를 에서 비동기로 실행하려면 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) { // ... });

이벤트 정의

이벤트 클래스는 이벤트와 관련된 정보를 담는 데이터 컨테이너 역할을 합니다. 비즈니스 로직을 포함하지 않고, 필요한 데이터만 보관합니다. 예를 들어 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 옵션과 함께 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 인터페이스를 구현하면 해당 리스너가 큐에서 비동기로 실행됩니다. 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 (true) { $this->release(30); } } }

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

데이터베이스 트랜잭션 내에서 큐 리스너가 디스패치되면, 트랜잭션이 커밋되기 전에 큐 워커가 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 처리

큐 리스너가 최대 재시도 횟수를 초과하면 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 메서드를 정의합니다. DateTime 인스턴스를 반환하면 됩니다.

use DateTime; /** * 리스너 재시도를 중단할 시각을 반환합니다. */ public function retryUntil(): DateTime { return now()->addMinutes(5); }

재시도 대기 시간(Backoff) 지정

예외 발생 후 재시도 전 대기 시간은 $backoff 프로퍼티로 설정합니다.

/** * 큐 리스너 재시도 전 대기 시간(초) * * @var int */ public $backoff = 3;

더 복잡한 로직이 필요하다면 backoff 메서드로 정의할 수 있습니다.

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

배열을 반환하면 지수 백오프(exponential backoff) 를 쉽게 구성할 수 있습니다. 아래 예시에서 첫 번째 재시도는 1초, 두 번째는 5초, 세 번째 이후는 10초씩 대기합니다.

/** * 큐 리스너 재시도 전 대기 시간(초) 배열을 반환합니다. * * @return array<int, int> */ public function backoff(): array { return [1, 5, 10]; }

이벤트 디스패치

이벤트를 발생시키려면 이벤트 클래스의 정적 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'); } }

조건에 따라 이벤트를 디스패치하려면 dispatchIfdispatchUnless 메서드를 사용합니다.

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 {} /** * 사용자 로그아웃 이벤트

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

번역일: 2026년 6월 21일