> ## 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의 스코프

> 로컬 스코프와 글로벌 스코프의 구조를 해설합니다. SoftDeletingScope 등의 프레임워크 내부 구현을 읽어 나가면서, 멀티 테넌트나 공개/비공개 필터 등의 실전적인 유스케이스를 소개합니다.

## 스코프란

Eloquent의 스코프는, 쿼리에 대한 제약을 정리하여 재이용할 수 있는 구조입니다. 스코프에는 2종류가 있습니다.

| 종류          | 적용 타이밍           | 용도                           |
| ----------- | ---------------- | ---------------------------- |
| **글로벌 스코프** | 항상 자동으로 적용       | 소프트 삭제, 멀티 테넌트, 공개 필터        |
| **로컬 스코프**  | 명시적으로 호출했을 때만 적용 | "인기 게시물", "액티브 사용자" 등의 공통 필터 |

## 로컬 스코프

### 정의

로컬 스코프는, 모델의 메서드에 `#[Scope]` 어트리뷰트를 부여하여 정의합니다.

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

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * 公開済みの投稿に絞り込む
     */
    #[Scope]
    protected function published(Builder $query): void
    {
        $query->where('status', 'published');
    }

    /**
     * 人気の投稿に絞り込む
     */
    #[Scope]
    protected function popular(Builder $query): void
    {
        $query->where('views', '>', 1000);
    }
}
```

<Info>
  `#[Scope]` 어트리뷰트는 `Illuminate\Database\Eloquent\Attributes` 네임스페이스에 있습니다. PHP 8.0 이후의 네이티브 구문입니다.
</Info>

### 사용법

정의한 스코프는, 메서드로서 호출할 수 있습니다. 체인도 가능합니다.

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

// 公開済み投稿のみ取得
$posts = Post::published()->get();

// 公開済みかつ人気の投稿を最新順で取得
$posts = Post::published()->popular()->latest()->get();
```

### 파라미터 전달

스코프 메서드의 제2인수 이후에 추가 파라미터를 정의할 수 있습니다.

```php theme={null}
#[Scope]
protected function ofStatus(Builder $query, string $status): void
{
    $query->where('status', $status);
}
```

호출 시에 그대로 인수를 전달합니다.

```php theme={null}
$posts = Post::ofStatus('draft')->get();
$posts = Post::ofStatus('published')->get();
```

### `orWhere`와의 조합

스코프를 `orWhere`로 연결할 때, 논리 그룹이 필요할 때가 있습니다.

```php theme={null}
// クロージャを使う方法（確実だが冗長）
$users = User::popular()->orWhere(function (Builder $query) {
    $query->active();
})->get();

// 高階メソッドを使うとシンプルに書ける
$users = User::popular()->orWhere->active()->get();
```

## 글로벌 스코프

### 구조

글로벌 스코프는 `Illuminate\Database\Eloquent\Scope` 인터페이스를 구현한 클래스입니다. 이 인터페이스는 `apply` 메서드 하나만을 요구합니다.

```php theme={null}
// フレームワーク本体のインターフェース定義
// src/Illuminate/Database/Eloquent/Scope.php

interface Scope
{
    public function apply(Builder $builder, Model $model);
}
```

`apply` 메서드 안에서 쿼리 빌더에 제약을 추가합니다.

### 글로벌 스코프 클래스의 작성

`make:scope` 커맨드로 뼈대를 생성합니다.

```bash theme={null}
php artisan make:scope ActiveScope
```

`app/Models/Scopes/ActiveScope.php`가 생성됩니다.

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

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class ActiveScope implements Scope
{
    /**
     * スコープをクエリビルダーに適用する
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('is_active', true);
    }
}
```

### 모델에의 적용

<Steps>
  <Step title="#[ScopedBy] 어트리뷰트로 등록(권장)">
    Laravel 13에서는 `#[ScopedBy]` 어트리뷰트를 사용하는 것이 가장 심플합니다.

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

    namespace App\Models;

    use App\Models\Scopes\ActiveScope;
    use Illuminate\Database\Eloquent\Attributes\ScopedBy;
    use Illuminate\Database\Eloquent\Model;

    #[ScopedBy([ActiveScope::class])]
    class User extends Model
    {
        //
    }
    ```

    여러 스코프를 배열로 지정할 수 있습니다.

    ```php theme={null}
    #[ScopedBy([ActiveScope::class, TenantScope::class])]
    class User extends Model
    {
        //
    }
    ```
  </Step>

  <Step title="booted() 메서드로 수동 등록">
    `booted` 메서드를 오버라이드하여 `addGlobalScope`를 호출하는 방법도 있습니다.

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

    namespace App\Models;

    use App\Models\Scopes\ActiveScope;
    use Illuminate\Database\Eloquent\Model;

    class User extends Model
    {
        protected static function booted(): void
        {
            static::addGlobalScope(new ActiveScope);
        }
    }
    ```
  </Step>
</Steps>

글로벌 스코프를 추가하면, `User::all()` 등 전 쿼리에 자동으로 `WHERE is_active = 1`이 붙습니다.

### 무명 클로저에 의한 글로벌 스코프

클래스를 별도 파일로 만들 정도는 아닌 단순한 스코프는, 클로저로 정의할 수 있습니다.

```php theme={null}
protected static function booted(): void
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('is_active', true);
    });
}
```

<Warning>
  클로저로 정의한 스코프를 나중에 제외하려면, 클래스명이 아니라 스코프명(문자열)을 사용해야 합니다.
</Warning>

### 글로벌 스코프의 제외

특정 쿼리에서는 스코프를 무효로 하고 싶은 장면이 있습니다.

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

// 特定のスコープを除外
User::withoutGlobalScope(ActiveScope::class)->get();

// クロージャで定義したスコープを除外
User::withoutGlobalScope('active')->get();

// すべてのグローバルスコープを除外
User::withoutGlobalScopes()->get();

// 複数のスコープを除外
User::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get();

// 指定したスコープ以外をすべて除外
User::withoutGlobalScopesExcept([TenantScope::class])->get();
```

## 프레임워크 내부: SoftDeletingScope

Laravel 표준의 `SoftDeletes` 트레이트가 글로벌 스코프를 어떻게 활용하고 있는지를 보면, 구현 패턴을 알 수 있습니다.

`SoftDeletingScope`는 `Scope` 인터페이스를 구현하고 있습니다.

```php theme={null}
// src/Illuminate/Database/Eloquent/SoftDeletingScope.php

class SoftDeletingScope implements Scope
{
    protected $extensions = [
        'Restore', 'RestoreOrCreate', 'CreateOrRestore',
        'WithTrashed', 'WithoutTrashed', 'OnlyTrashed',
    ];

    /**
     * スコープをクエリビルダーに適用する
     * deleted_at が NULL のレコードだけを取得するよう制約を追加
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->whereNull($model->getQualifiedDeletedAtColumn());
    }

    /**
     * クエリビルダーにマクロを拡張する
     * withTrashed() / onlyTrashed() などが使えるようになる
     */
    public function extend(Builder $builder)
    {
        foreach ($this->extensions as $extension) {
            $this->{"add{$extension}"}($builder);
        }

        // delete() 操作をオーバーライドして deleted_at を更新する処理にする
        $builder->onDelete(function (Builder $builder) {
            $column = $this->getDeletedAtColumn($builder);
            return $builder->update([
                $column => $builder->getModel()->freshTimestampString(),
            ]);
        });
    }
}
```

<Accordion title="withTrashed()의 구현을 본다">
  `withTrashed()`는 실제로는 `withoutGlobalScope($this)`를 호출하고 있습니다. 즉 `SoftDeletingScope` 자신을 제외함으로써, 삭제된 레코드도 취득할 수 있게 하고 있습니다.

  ```php theme={null}
  protected function addWithTrashed(Builder $builder)
  {
      $builder->macro('withTrashed', function (Builder $builder, $withTrashed = true) {
          if (! $withTrashed) {
              return $builder->withoutTrashed();
          }

          return $builder->withoutGlobalScope($this);
      });
  }
  ```

  `onlyTrashed()`도 마찬가지로, 스코프 자신을 제외한 뒤 `whereNotNull('deleted_at')`을 추가하고 있습니다.

  ```php theme={null}
  protected function addOnlyTrashed(Builder $builder)
  {
      $builder->macro('onlyTrashed', function (Builder $builder) {
          $model = $builder->getModel();

          $builder->withoutGlobalScope($this)->whereNotNull(
              $model->getQualifiedDeletedAtColumn()
          );

          return $builder;
      });
  }
  ```
</Accordion>

<Tip>
  `Scope` 인터페이스에 `extend` 메서드는 정의되어 있지 않지만, Eloquent의 빌더는 스코프가 `extend` 메서드를 가지고 있으면 자동적으로 호출합니다. 커스텀 매크로를 추가하고 싶은 경우에 활용할 수 있습니다.
</Tip>

## 실전적인 유스케이스

### 멀티 테넌트: 테넌트 ID에 의한 자동 좁혀 넣기

SaaS 애플리케이션에서는, 전 쿼리에 테넌트 ID의 필터를 자동 적용하는 것이 중요합니다.

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

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if ($tenantId = auth()->user()?->tenant_id) {
            $builder->where('tenant_id', $tenantId);
        }
    }
}
```

```php theme={null}
use App\Models\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy([TenantScope::class])]
class Post extends Model
{
    //
}
```

이것으로 `Post::all()`을 호출하는 것만으로, 인증 중인 사용자의 테넌트 데이터만이 반환됩니다.

### 공개/비공개 필터

관리 화면에서는 비공개 게시물도 표시하고 싶지만, 프론트엔드에서는 공개된 것만 표시하고 싶은 경우입니다.

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

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class PublishedScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('status', 'published')
                ->where('published_at', '<=', now());
    }
}
```

관리 화면에서는 `withoutGlobalScope`로 스코프를 제외합니다.

```php theme={null}
// フロントエンド: 公開済みのみ（PublishedScope が自動適用）
$posts = Post::latest()->get();

// 管理画面: 全投稿を表示
$posts = Post::withoutGlobalScope(PublishedScope::class)->latest()->get();
```

### `select`가 아닌 `addSelect`를 사용한다

<Warning>
  글로벌 스코프 내에서 컬럼을 추가할 때는 `select`가 아니라 `addSelect`를 사용해 주세요. `select`를 사용하면, 호출자의 쿼리가 `select`하고 있는 컬럼을 덮어써 버립니다.

  ```php theme={null}
  // 悪い例: 呼び出し元の select を上書きする
  public function apply(Builder $builder, Model $model): void
  {
      $builder->select('id', 'tenant_id', 'name');
  }

  // 良い例: 既存の select に追記する
  public function apply(Builder $builder, Model $model): void
  {
      $builder->addSelect('tenant_id');
  }
  ```
</Warning>

## 다음 단계

<Card title="Eloquent 커스텀 캐스트" icon="wand-magic-sparkles" href="/ko/advanced/eloquent-casts">
  속성의 변환 로직을 커스텀 캐스트로서 구현하고, Value Object 패턴을 활용하는 방법을 배웁니다.
</Card>


## Related topics

- [PHP 어트리뷰트](/ko/advanced/php-attributes.md)
- [Eloquent Bootable Traits](/ko/advanced/eloquent-bootable-traits.md)
- [Laravel Passport(OAuth2 서버 구현)](/ko/passport.md)
- [지연 서비스 프로바이더](/ko/advanced/deferred-provider.md)
- [Socialite for Discord](/ko/packages/socialite-discord.md)
