Laravel 12 이벤트와 리스너 완벽 가이드: 이벤트 기반 아키텍처 구현과 면접 대비

Laravel 12의 이벤트-리스너 시스템을 상세히 설명합니다. 자동 검색 기능, 큐 처리, 트랜잭션 대응, 유니크 리스너 구현 방법부터 기술 면접에서 자주 묻는 이벤트 기반 아키텍처 질문까지 종합적으로 다룹니다.

Laravel 이벤트와 리스너 이벤트 기반 아키텍처 다이어그램

Laravel의 이벤트와 리스너는 옵저버 패턴을 구현하여 애플리케이션의 각 컴포넌트를 느슨하게 결합합니다. 사용자가 주문을 완료하면 OrderShipped 이벤트가 발생하고, 알림 전송, 분석 데이터 수집, 재고 업데이트 등의 처리가 각각 독립된 리스너에서 실행됩니다.

Laravel 12 이벤트 자동 검색

Laravel 12에서는 app/Listeners 디렉토리 내의 리스너가 자동으로 검색됩니다. 이벤트 클래스를 타입 힌트한 handle 또는 __invoke 메서드를 가진 클래스는 별도의 설정 파일 없이 자동으로 등록됩니다.

Laravel 12의 이벤트 자동 검색 동작 방식

Laravel 11부터 EventServiceProvider가 제거되었습니다. Laravel은 Listeners 디렉토리를 스캔하고 리플렉션을 사용하여 handle 메서드의 타입 힌트를 통해 각 리스너가 응답하는 이벤트를 결정합니다.

app/Listeners/SendOrderConfirmation.phpphp
<?php

namespace App\Listeners;

use App\Events\OrderShipped;

class SendOrderConfirmation
{
    public function handle(OrderShipped $event): void
    {
        // $event->order->user 에게 확인 이메일 전송
    }
}

위의 리스너는 OrderShipped 이벤트에 자동으로 등록됩니다. 서비스 프로바이더에서 수동 바인딩이 필요 없습니다. 이를 통해 보일러플레이트 코드가 줄어들고 이벤트-리스너 관계가 구현 코드와 가깝게 유지됩니다.

등록된 리스너를 확인하려면 php artisan event:list를 실행합니다. 프로덕션 환경에서는 php artisan event:cache로 이벤트 매니페스트를 캐시하여 요청마다 디렉토리 스캔을 생략할 수 있습니다.

Artisan으로 이벤트와 리스너 생성하기

Laravel은 이벤트와 리스너 스캐폴딩을 위한 Artisan 명령어를 제공합니다.

bash
# 이벤트 클래스 생성
php artisan make:event OrderShipped

# 이벤트에 연결된 리스너 생성
php artisan make:listener SendShipmentNotification --event=OrderShipped

생성되는 이벤트 클래스는 세 가지 트레이트를 사용합니다. Dispatchable은 정적 dispatch() 메서드를 제공하고, InteractsWithSockets는 브로드캐스팅용이며, SerializesModels는 리스너가 큐에 들어갈 때 Eloquent 모델을 직렬화합니다.

app/Events/OrderShipped.phpphp
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Queue\SerializesModels;

class OrderShipped
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public Order $order,
    ) {}
}

이벤트 클래스는 데이터 컨테이너입니다. Order 모델만 보유하며, 비즈니스 로직은 리스너에 작성합니다.

컨트롤러와 서비스에서 이벤트 디스패치하기

이벤트 클래스의 정적 dispatch() 메서드를 사용하여 이벤트를 발생시킵니다.

app/Http/Controllers/OrderController.phpphp
<?php

namespace App\Http\Controllers;

use App\Events\OrderShipped;
use App\Models\Order;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    public function ship(Request $request, Order $order)
    {
        // 배송 로직 처리...
        
        OrderShipped::dispatch($order);
        
        return redirect()->route('orders.index');
    }
}

조건부로 이벤트를 발생시키려면 dispatchIf() 또는 dispatchUnless()를 사용합니다.

php
// 추적 번호가 있는 경우에만 디스패치
OrderShipped::dispatchIf($order->tracking_number !== null, $order);

Laravel 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

무거운 작업을 위한 큐 리스너

이메일 전송, 외부 API 호출, 무거운 계산 작업을 수행하는 리스너는 비동기로 실행해야 합니다. ShouldQueue 인터페이스를 구현하면 리스너가 큐로 푸시됩니다.

app/Listeners/SendShipmentNotification.phpphp
<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    public $queue = 'notifications';
    public $delay = 60; // 처리 전 60초 대기

    public function handle(OrderShipped $event): void
    {
        // $event->order->user 에게 알림 전송
    }
}

큐 연결과 이름은 $connection$queue 프로퍼티로 커스터마이즈하거나 런타임 메서드를 사용합니다.

php
public function viaConnection(): string
{
    return 'redis';
}

public function viaQueue(): string
{
    return $this->event->order->priority === 'high' 
        ? 'high-priority' 
        : 'default';
}

shouldQueue를 통한 조건부 큐잉

이벤트 데이터에 따라 큐잉 여부를 결정해야 하는 경우가 있습니다.

php
public function shouldQueue(OrderShipped $event): bool
{
    // $100 이상의 주문만 큐에 넣기
    return $event->order->total >= 10000;
}

데이터베이스 트랜잭션과 이벤트 타이밍

트랜잭션 내에서 디스패치된 큐 리스너는 트랜잭션이 커밋되기 전에 처리될 수 있어 데이터 누락 오류가 발생할 수 있습니다. 두 가지 해결책이 있습니다.

1. 전역 설정 config/queue.php에서 설정하는 방법:

php
'connections' => [
    'redis' => [
        'after_commit' => true,
    ],
],

2. 리스너 단위 ShouldQueueAfterCommit 사용:

php
use Illuminate\Contracts\Queue\ShouldQueueAfterCommit;

class UpdateInventory implements ShouldQueueAfterCommit
{
    public function handle(OrderShipped $event): void
    {
        // 주문 조회 안전, 트랜잭션 커밋 완료
    }
}

이벤트 자체에 대해서는 이벤트 클래스에서 ShouldDispatchAfterCommit을 구현합니다.

php
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;

class OrderShipped implements ShouldDispatchAfterCommit
{
    // 트랜잭션 커밋 후에만 이벤트가 디스패치됨
}

중복을 방지하는 유니크 리스너

ShouldBeUnique 인터페이스는 동일한 리스너가 처리 중일 때 중복으로 큐에 들어가는 것을 방지합니다.

php
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;

class ProcessLicenseKey implements ShouldQueue, ShouldBeUnique
{
    public $uniqueFor = 3600; // 1시간 후 락 만료

    public function uniqueId(LicensePurchased $event): string
    {
        return 'license:' . $event->license->id;
    }

    public function handle(LicensePurchased $event): void
    {
        // 라이선스 키 생성 및 할당
    }
}

동일한 라이선스 ID로 여러 이벤트가 빠르게 발생하면 첫 번째 리스너만 처리됩니다. 이후 시도는 락이 유지되고 있음을 감지하고 큐잉을 건너뜁니다.

관련 이벤트를 위한 이벤트 구독자

여러 관련 이벤트가 공통 처리 로직을 공유할 때, 이벤트 구독자는 여러 핸들러를 하나의 클래스에 그룹화합니다.

app/Listeners/UserActivitySubscriber.phpphp
<?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
    {
        // 로그인 성공 기록, last_login 타임스탬프 업데이트
    }

    public function handleLogout(Logout $event): void
    {
        // 로그아웃 기록, 세션 데이터 정리
    }

    public function subscribe(Dispatcher $events): array
    {
        return [
            Login::class => 'handleLogin',
            Logout::class => 'handleLogout',
        ];
    }
}

구독자도 자동 검색 규칙을 따릅니다. 핸들러 메서드가 적절하게 타입 힌트된 파라미터를 가지고 있으면 Laravel이 자동으로 등록합니다.

Union Types로 여러 이벤트 처리하기

PHP 8의 Union Types를 사용하면 하나의 리스너로 여러 이벤트 타입에 응답할 수 있습니다.

php
public function handle(OrderShipped|OrderCancelled $event): void
{
    match (true) {
        $event instanceof OrderShipped => $this->notifyShipped($event),
        $event instanceof OrderCancelled => $this->notifyCancelled($event),
    };
}

이 패턴은 여러 이벤트 타입에서 유사한 로직을 따르는 알림에 적합합니다.

Event::defer로 이벤트 지연 디스패치하기

Laravel 12에서는 코드 블록이 완료될 때까지 이벤트 디스패치를 지연시키는 Event::defer()가 도입되었습니다. 이를 통해 리스너가 모든 관련 레코드에 접근할 수 있습니다.

php
use Illuminate\Support\Facades\Event;

Event::defer(function () {
    $user = User::create(['name' => 'John']);
    $user->posts()->create(['title' => 'Welcome']);
});
// User와 Post 생성 이벤트가 여기서 디스패치됨

클로저 내에서 예외가 발생하면 지연된 이벤트는 디스패치되지 않습니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

Laravel 이벤트와 리스너 면접 질문

기술 면접에서는 이벤트 기반 아키텍처에 대한 이해도를 자주 테스트합니다. 다음은 숙련된 Laravel 개발자를 구별하는 질문들입니다.

Q: Laravel 12는 EventServiceProvider 없이 어떻게 이벤트 리스너를 등록합니까?

Laravel은 리플렉션을 사용하여 app/Listeners를 스캔합니다. handle() 또는 __invoke() 메서드의 타입 힌트를 읽어 각 리스너가 응답하는 이벤트를 결정합니다. 프로덕션에서는 php artisan event:cache로 매니페스트를 캐시합니다.

Q: 리스너가 ShouldQueue를 구현해야 하는 시점은 언제입니까?

작업이 몇 밀리초 이상 걸리는 경우입니다. 이메일 전송, 외부 API 호출, 리포트 생성, 파일 처리 등이 해당됩니다. 동기 리스너는 HTTP 응답을 블로킹합니다.

Q: ShouldQueueAfterCommit이 해결하는 문제는 무엇입니까?

큐 리스너는 데이터베이스 트랜잭션이 커밋되기 전에 처리될 수 있습니다. 리스너가 방금 생성된 레코드를 쿼리하면 데이터가 아직 커밋되지 않아 실패합니다. ShouldQueueAfterCommit은 트랜잭션이 완료될 때까지 큐 디스패치를 지연시킵니다.

Q: 동일 이벤트의 중복 처리를 어떻게 방지합니까?

ShouldBeUnique를 구현합니다. uniqueId()를 정의하여 이벤트 데이터 기반의 캐시 키를 반환합니다. 리스너는 원자적 락을 획득하고, 락이 유지되면 중복 디스패치가 건너뛰어집니다.

Q: 이벤트 리스너와 이벤트 구독자의 차이점은 무엇입니까?

리스너는 하나의 이벤트 타입을 처리합니다. 구독자는 여러 관련 이벤트에 대한 핸들러를 한 곳에 등록하는 클래스로, 인증 이벤트나 감사 로깅 그룹화에 유용합니다.

확장 가능한 애플리케이션에서 Laravel 이벤트의 의미

  • 이벤트는 컴포넌트를 분리하여 알림, 분석 등의 기능이 독립적으로 발전할 수 있게 합니다
  • 큐 리스너는 무거운 작업을 오프로드하여 HTTP 응답을 빠르게 유지합니다
  • ShouldQueueAfterCommit은 큐 워커와 데이터베이스 트랜잭션 간의 경쟁 조건을 방지합니다
  • Laravel 12의 자동 검색은 설정 오버헤드를 제거하고 리스너 로직을 구현과 가깝게 유지합니다
  • 이벤트 구독자는 사용자 활동 추적 같은 일반적인 패턴의 관련 이벤트 처리를 통합하여 파일 분산을 줄입니다
  • SharpSkill/technologies/laravel/interview-questions/events-listeners 모듈에서 이러한 패턴을 인터랙티브한 질문으로 학습할 수 있습니다
오늘의 챌린지

Laravel 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 8월 20일 업데이트

태그

#laravel
#events
#listeners
#event-driven-architecture
#php
#interview-questions

공유

관련 기사