> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Eloquent Observers와 모델 이벤트

> Eloquent 모델이 발화하는 이벤트의 구조와, Observer 클래스를 사용해 이벤트를 일원적으로 관리하는 방법을 해설합니다. `#[ObservedBy]` 어트리뷰트 등 Laravel 13의 최신 기능도 포함합니다.

## 모델 이벤트란

Eloquent 모델은 라이프사이클의 각 타이밍에 자동적으로 이벤트를 발화합니다. 이러한 이벤트에 훅을 걸음으로써, 모델의 저장·삭제 등의 전후에 처리를 삽입할 수 있습니다.

Eloquent가 발화하는 이벤트는 이하와 같습니다.

| 이벤트             | 발생 타이밍                  |
| --------------- | ----------------------- |
| `retrieved`     | DB에서 모델을 취득했을 때         |
| `creating`      | 신규 모델을 저장하기 직전          |
| `created`       | 신규 모델을 저장한 직후           |
| `updating`      | 기존 모델을 갱신하기 직전          |
| `updated`       | 기존 모델을 갱신한 직후           |
| `saving`        | 신규 작성·갱신 어느 쪽으로 저장하기 직전 |
| `saved`         | 신규 작성·갱신 어느 쪽으로 저장한 직후  |
| `deleting`      | 모델을 삭제하기 직전             |
| `deleted`       | 모델을 삭제한 직후              |
| `trashed`       | 소프트 삭제한 직후              |
| `forceDeleting` | 물리 삭제하기 직전              |
| `forceDeleted`  | 물리 삭제한 직후               |
| `restoring`     | 소프트 삭제를 복원하기 직전         |
| `restored`      | 소프트 삭제를 복원한 직후          |
| `replicating`   | `replicate()`를 호출했을 때   |

`-ing`로 끝나는 이벤트는 변경이 DB에 영속화되기 **전**에, `-ed`로 끝나는 이벤트는 **후**에 발화됩니다.

<Warning>
  매스 업데이트나 매스 삭제(`User::where(...)->update(...)` 등)에서는, `saving`, `saved`, `updating`, `updated`, `deleting`, `deleted` 이벤트는 발화되지 않습니다. 모델이 실제로는 취득되지 않기 때문입니다.
</Warning>

## 클로저를 사용한 이벤트 리스너

이벤트를 심플하게 다루고 싶은 경우에는, 모델의 `booted` 메서드 내에서 클로저를 등록할 수 있습니다.

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected static function booted(): void
    {
        static::created(function (User $user) {
            // ユーザー作成後に実行される処理
        });

        static::deleting(function (User $user) {
            // ユーザー削除前に実行される処理
        });
    }
}
```

처리를 큐로 비동기 실행하고 싶은 경우에는 `queueable` 헬퍼를 사용합니다.

```php theme={null}
use function Illuminate\Events\queueable;

static::created(queueable(function (User $user) {
    // キューで非同期実行される
}));
```

## `$dispatchesEvents` 프로퍼티

Laravel의 이벤트 시스템과 연계하고 싶은 경우에는, `$dispatchesEvents` 프로퍼티로 모델 이벤트를 독자의 이벤트 클래스에 매핑합니다.

```php theme={null}
<?php

namespace App\Models;

use App\Events\UserDeleted;
use App\Events\UserSaved;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    /**
     * モデルイベントとイベントクラスのマッピング
     *
     * @var array<string, string>
     */
    protected $dispatchesEvents = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}
```

매핑한 이벤트 클래스에는, 컨스트럭터에서 모델의 인스턴스를 받습니다.

```php theme={null}
<?php

namespace App\Events;

use App\Models\User;

class UserSaved
{
    public function __construct(
        public readonly User $user,
    ) {}
}
```

## Observer 클래스의 작성

하나의 모델에 대해 여러 이벤트를 처리하는 경우, 클로저를 나열하기보다 Observer 클래스에 정리하는 편이 깔끔합니다.

<Steps>
  <Step title="Artisan 커맨드로 클래스를 생성한다">
    `make:observer` 커맨드로 뼈대를 생성합니다. `--model` 옵션으로 모델을 지정하면, 대응하는 메서드가 자동으로 추가됩니다.

    ```bash theme={null}
    php artisan make:observer UserObserver --model=User
    ```

    `app/Observers/UserObserver.php`가 생성됩니다.
  </Step>

  <Step title="각 이벤트의 메서드를 구현한다">
    메서드명이 이벤트명에 대응합니다. 인수에는 모델의 인스턴스가 전달됩니다.

    ```php theme={null}
    <?php

    namespace App\Observers;

    use App\Models\User;

    class UserObserver
    {
        public function created(User $user): void
        {
            // ユーザー作成後の処理
        }

        public function updated(User $user): void
        {
            // ユーザー更新後の処理
        }

        public function deleted(User $user): void
        {
            // ユーザー削除後の処理
        }

        public function restored(User $user): void
        {
            // ソフトデリート復元後の処理
        }

        public function forceDeleted(User $user): void
        {
            // 物理削除後の処理
        }
    }
    ```
  </Step>

  <Step title="Observer를 Model에 등록한다">
    등록 방법은 2가지 있습니다. Laravel 13에서는 `#[ObservedBy]` 어트리뷰트를 사용하는 방법이 권장됩니다.

    **방법 1: `#[ObservedBy]` 어트리뷰트(권장)**

    모델 클래스에 어트리뷰트를 부여하는 것만으로 등록이 완료됩니다. `AppServiceProvider`의 변경이 불필요합니다.

    ```php theme={null}
    <?php

    namespace App\Models;

    use App\Observers\UserObserver;
    use Illuminate\Database\Eloquent\Attributes\ObservedBy;
    use Illuminate\Foundation\Auth\User as Authenticatable;

    #[ObservedBy(UserObserver::class)]
    class User extends Authenticatable
    {
        //
    }
    ```

    여러 Observer를 등록하는 경우에는 어트리뷰트를 반복하거나, 배열로 전달합니다.

    ```php theme={null}
    #[ObservedBy(UserObserver::class)]
    #[ObservedBy(AuditObserver::class)]
    class User extends Authenticatable
    {
        //
    }
    ```

    **방법 2: `AppServiceProvider`에서 등록한다**

    `AppServiceProvider`의 `boot` 메서드에서 `observe`를 호출합니다.

    ```php theme={null}
    <?php

    namespace App\Providers;

    use App\Models\User;
    use App\Observers\UserObserver;
    use Illuminate\Support\ServiceProvider;

    class AppServiceProvider extends ServiceProvider
    {
        public function boot(): void
        {
            User::observe(UserObserver::class);
        }
    }
    ```
  </Step>
</Steps>

<Info>
  `#[ObservedBy]` 어트리뷰트는 `Illuminate\Database\Eloquent\Attributes` 네임스페이스에 있습니다. PHP 8.0 이후의 네이티브 구문으로, Laravel 13에서 적극적으로 채용되고 있습니다.
</Info>

## 데이터베이스 트랜잭션 내에서의 Observer

모델이 트랜잭션 내에서 작성·갱신되는 경우, 트랜잭션의 커밋 후에 Observer를 실행하고 싶을 때가 있습니다. `ShouldHandleEventsAfterCommit` 인터페이스를 구현하면 그 동작이 됩니다.

```php theme={null}
<?php

namespace App\Observers;

use App\Models\User;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;

class UserObserver implements ShouldHandleEventsAfterCommit
{
    public function created(User $user): void
    {
        // トランザクションがコミットされた後に実行される
    }
}
```

트랜잭션 밖에서 실행된 경우에는, 통상대로 즉시 실행됩니다.

## 이벤트를 일시적으로 무효화한다

### `withoutEvents`로 특정 처리만 이벤트를 멈춘다

`User::withoutEvents()`에 전달한 클로저 내에서는, 일체의 모델 이벤트가 발화되지 않습니다.

```php theme={null}
use App\Models\User;

$user = User::withoutEvents(function () {
    User::findOrFail(1)->delete();

    return User::find(2);
});
```

### `saveQuietly`로 저장 시의 이벤트를 멈춘다

이벤트를 발화시키지 않고 모델을 저장하고 싶을 때는 `saveQuietly`를 사용합니다.

```php theme={null}
$user = User::findOrFail(1);

$user->name = 'Victoria Faith';

$user->saveQuietly();
```

동일한 메서드가 삭제·복원·레플리케이트에도 준비되어 있습니다.

```php theme={null}
$user->deleteQuietly();
$user->restoreQuietly();
```

## 실전적인 유스케이스

### 캐시의 자동 클리어

모델이 갱신·삭제되었을 때에 관련된 캐시를 자동으로 클리어합니다.

```php theme={null}
<?php

namespace App\Observers;

use App\Models\Post;
use Illuminate\Support\Facades\Cache;

class PostObserver
{
    public function saved(Post $post): void
    {
        Cache::forget("post:{$post->id}");
        Cache::forget('posts:latest');
    }

    public function deleted(Post $post): void
    {
        Cache::forget("post:{$post->id}");
        Cache::forget('posts:latest');
    }
}
```

### 감사 로그의 기록

모델의 변경 이력을 자동으로 기록합니다. `getDirty()`로 변경 전후의 값을 취득할 수 있습니다.

```php theme={null}
<?php

namespace App\Observers;

use App\Models\AuditLog;
use App\Models\User;

class UserObserver
{
    public function updating(User $user): void
    {
        AuditLog::create([
            'model_type' => User::class,
            'model_id'   => $user->id,
            'changes'    => $user->getDirty(),
            'user_id'    => auth()->id(),
        ]);
    }

    public function deleted(User $user): void
    {
        AuditLog::create([
            'model_type' => User::class,
            'model_id'   => $user->id,
            'changes'    => ['deleted' => true],
            'user_id'    => auth()->id(),
        ]);
    }
}
```

<Tip>
  `updating` 이벤트는 DB에의 저장 **전**에 발화되므로, `getDirty()`로 변경 예정의 값을 취득할 수 있습니다. `updated` 이벤트 후에 호출하면 `getDirty()`는 비어 있게 됩니다.
</Tip>

### 관련 모델의 자동 갱신

주문이 완료되었을 때에 재고 수를 자동으로 갱신하는 예입니다.

```php theme={null}
<?php

namespace App\Observers;

use App\Models\Order;

class OrderObserver
{
    public function created(Order $order): void
    {
        foreach ($order->items as $item) {
            $item->product->decrement('stock', $item->quantity);
        }
    }

    public function deleted(Order $order): void
    {
        foreach ($order->items as $item) {
            $item->product->increment('stock', $item->quantity);
        }
    }
}
```

## 다음 단계

<Card title="상급: PHP 어트리뷰트" icon="tag" href="/ko/advanced/php-attributes">
  `#[ObservedBy]`를 포함한, Laravel 13의 PHP 어트리뷰트를 한꺼번에 배웁니다.
</Card>


## Related topics

- [Eloquent의 커스텀 캐스트](/ko/advanced/eloquent-casts.md)
- [이벤트와 리스너](/ko/events.md)
- [Eloquent Bootable Traits](/ko/advanced/eloquent-bootable-traits.md)
- [Eloquent 입문](/ko/eloquent.md)
- [send()와 on()](/ko/packages/laravel-copilot-sdk/send-on.md)
