이벤트란
Laravel의 이벤트 시스템은 단순한 옵서버 패턴의 구현입니다.
애플리케이션 내에서 일어난 사건(이벤트)을 발화하고, 그에 반응하는 리스너를 정의함으로써 컴포넌트 간의 의존을 최소한으로 억제할 수 있습니다.
예를 들어 “주문이 확정되었다”라는 이벤트를 발화하면, “확인 메일을 보낸다”, “재고를 줄인다”, “Slack에 알린다”라는 여러 리스너가 각각 독립적으로 움직입니다.
주문 처리의 코드는 메일 전송이나 Slack 알림의 구현을 일체 알 필요가 없습니다.
이벤트 클래스는 app/Events 디렉터리에, 리스너 클래스는 app/Listeners 디렉터리에 놓습니다.
어느 쪽도 존재하지 않는 경우 Artisan 명령어가 자동으로 작성합니다.
이벤트와 리스너의 생성
make:event와 make:listener Artisan 명령어로 클래스의 골격을 생성합니다.
php artisan make:event UserRegistered
php artisan make:listener SendWelcomeEmail --event=UserRegistered
인수 없이 실행하면 인터랙티브하게 입력을 요구받습니다.
php artisan make:event
php artisan make:listener
이벤트 등록
이벤트 디스커버리(자동 검출)
기본적으로 Laravel은 app/Listeners 디렉터리를 스캔해 리스너를 자동 등록합니다.
handle 또는 __invoke라는 이름의 메서드의 인수 타입으로부터 이벤트와의 매핑을 추론합니다.
use App\Events\UserRegistered;
class SendWelcomeEmail
{
public function handle(UserRegistered $event): void
{
// 웰컴 메일을 전송
}
}
PHP의 유니온 타입을 사용하면 여러 이벤트를 하나의 메서드로 받을 수 있습니다.
public function handle(UserRegistered|UserUpdated $event): void
{
// ...
}
리스너를 다른 디렉터리에 놓는 경우 bootstrap/app.php에서 추가 스캔 대상을 지정합니다.
->withEvents(discover: [
__DIR__.'/../app/Domain/Orders/Listeners',
])
와일드카드를 사용해 여러 디렉터리를 함께 지정할 수 있습니다.
->withEvents(discover: [
__DIR__.'/../app/Domain/*/Listeners',
])
등록되어 있는 리스너의 목록은 다음 명령어로 확인할 수 있습니다.
운영 환경에서는 리스너의 매니페스트를 캐시해 두면 성능이 향상됩니다.
배포 시에 php artisan optimize 또는 php artisan event:cache를 실행해 주십시오.
캐시를 삭제하려면 php artisan event:clear를 사용합니다.
수동 등록
AppServiceProvider의 boot 메서드에서 Event 파사드를 사용해 수동 등록할 수도 있습니다.
use App\Events\UserRegistered;
use App\Listeners\SendWelcomeEmail;
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::listen(
UserRegistered::class,
SendWelcomeEmail::class,
);
}
클로저로 등록할 수도 있습니다.
use App\Events\UserRegistered;
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::listen(function (UserRegistered $event) {
// ...
});
}
이벤트의 정의
이벤트 클래스는 데이터의 그릇입니다. 로직은 갖지 않고, 이벤트에 관련된 정보를 프로퍼티로 유지합니다.
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public User $user,
) {}
}
SerializesModels 트레이트에 의해 큐잉된 리스너가 이벤트를 직렬화할 때 Eloquent 모델이 올바르게 취급됩니다.
이벤트 발화
dispatch 스태틱 메서드 또는 event() 헬퍼로 이벤트를 발화합니다.
use App\Events\UserRegistered;
// 스태틱 메서드
UserRegistered::dispatch($user);
// 헬퍼 함수
event(new UserRegistered($user));
조건부로 발화하는 메서드도 준비되어 있습니다.
UserRegistered::dispatchIf($condition, $user);
UserRegistered::dispatchUnless($condition, $user);
데이터베이스 트랜잭션 후에 발화
이벤트를 트랜잭션의 커밋 후에만 발화하고 싶은 경우, ShouldDispatchAfterCommit 인터페이스를 이벤트 클래스에 구현합니다.
트랜잭션이 실패하면 이벤트는 파기됩니다.
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered implements ShouldDispatchAfterCommit
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public User $user,
) {}
}
리스너의 구현
리스너는 handle 메서드로 이벤트를 받습니다.
생성자에서는 서비스 컨테이너가 의존을 자동 주입합니다.
<?php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail
{
public function __construct() {}
public function handle(UserRegistered $event): void
{
Mail::to($event->user->email)
->send(new WelcomeMail($event->user));
}
}
리스너의 handle 메서드에서 false를 반환하면 후속 리스너에의 이벤트 전파를 멈출 수 있습니다.
큐잉된 리스너
메일 전송이나 HTTP 요청 등 시간이 걸리는 처리는 큐잉된 리스너로서 비동기로 실행할 수 있습니다.
ShouldQueue 인터페이스를 구현하기만 하면 이벤트 발화 시에 리스너가 자동으로 큐에 적재됩니다.
<?php
namespace App\Listeners;
use App\Events\UserRegistered;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendWelcomeEmail implements ShouldQueue
{
public function handle(UserRegistered $event): void
{
// 이 처리는 큐 워커가 비동기로 실행
}
}
큐잉된 리스너를 사용하기 전에 큐의 설정과 워커의 기동이 필요합니다.
자세한 내용은 큐와 잡 페이지를 참조해 주십시오.
큐의 접속·이름·지연 시간을 커스터마이즈
PHP 어트리뷰트를 사용해 접속처·큐 이름·지연 시간을 설정할 수 있습니다.
<?php
namespace App\Listeners;
use App\Events\UserRegistered;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Delay;
use Illuminate\Queue\Attributes\Queue;
#[Connection('redis')]
#[Queue('emails')]
#[Delay(10)]
class SendWelcomeEmail implements ShouldQueue
{
public function handle(UserRegistered $event): void
{
// ...
}
}
메서드로 동적으로 값을 정할 수도 있습니다.
public function viaConnection(): string
{
return 'redis';
}
public function viaQueue(): string
{
return 'emails';
}
public function withDelay(UserRegistered $event): int
{
return 10;
}
최대 시도 횟수와 타임아웃
#[Tries]와 #[Timeout] 어트리뷰트로 실패 시의 거동을 제어할 수 있습니다.
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\Timeout;
#[Tries(3)]
#[Timeout(30)]
class SendWelcomeEmail implements ShouldQueue
{
// ...
}
실패 시의 처리
failed 메서드를 정의하면, 리스너가 최대 시도 횟수를 넘어 실패했을 때의 사후 처리를 기술할 수 있습니다.
use Throwable;
public function failed(UserRegistered $event, Throwable $exception): void
{
// 관리자에의 통지 등
}
이벤트 서브스크라이버
이벤트 서브스크라이버를 사용하면 관련된 여러 이벤트 핸들러를 하나의 클래스에 정리할 수 있습니다.
서브스크라이버의 작성
subscribe 메서드에서 이벤트와 핸들러의 매핑을 배열로 반환합니다.
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;
class UserActivitySubscriber
{
public function handleLogin(Login $event): void
{
// 로그인 처리
}
public function handleLogout(Logout $event): void
{
// 로그아웃 처리
}
/**
* @return array<string, string>
*/
public function subscribe(Dispatcher $events): array
{
return [
Login::class => 'handleLogin',
Logout::class => 'handleLogout',
];
}
}
서브스크라이버의 등록
이벤트 디스커버리가 유효한 경우, subscribe 메서드에서 배열을 반환하는 서브스크라이버는 자동 등록됩니다.
수동으로 등록하는 경우, AppServiceProvider의 boot 메서드에서 Event::subscribe를 호출합니다.
use App\Listeners\UserActivitySubscriber;
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::subscribe(UserActivitySubscriber::class);
}
실전 예시: 사용자 등록 시에 웰컴 메일을 전송
이벤트 클래스 작성
php artisan make:event UserRegistered
app/Events/UserRegistered.php를 편집해 등록 사용자를 유지하는 프로퍼티를 추가합니다.<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public User $user,
) {}
}
리스너 클래스 작성
php artisan make:listener SendWelcomeEmail --event=UserRegistered
메일 전송을 큐로 비동기로 하기 위해 ShouldQueue를 구현합니다.<?php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Mail\WelcomeMail;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail implements ShouldQueue
{
public function handle(UserRegistered $event): void
{
Mail::to($event->user->email)
->send(new WelcomeMail($event->user));
}
}
컨트롤러에서 이벤트를 발화
사용자 등록 처리 후에 UserRegistered::dispatch()를 호출합니다.<?php
namespace App\Http\Controllers\Auth;
use App\Events\UserRegistered;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class RegisterController extends Controller
{
public function store(Request $request): RedirectResponse
{
$user = User::create($request->validated());
UserRegistered::dispatch($user);
return redirect('/dashboard');
}
}
RegisterController는 UserRegistered 이벤트를 발화할 뿐이며, 메일 전송의 구현을 알지 못합니다.
향후 “등록 시에 Slack 알림도 보낸다”가 되어도 컨트롤러에는 아무 변경이 불필요합니다. 워커 기동
큐잉된 리스너를 처리하기 위해 워커를 기동합니다.
이벤트 디스커버리가 유효한 경우 AppServiceProvider에의 수동 등록은 불필요합니다.
app/Listeners 디렉터리에 놓인 리스너는 자동으로 검출됩니다.
php artisan event:list로 등록된 이벤트와 리스너의 목록을 확인할 수 있습니다.
상정 외의 리스너가 등록되어 있지 않은지 정기적으로 체크해 주십시오.