메인 콘텐츠로 건너뛰기

스코프란

Eloquent의 스코프는, 쿼리에 대한 제약을 정리하여 재이용할 수 있는 구조입니다. 스코프에는 2종류가 있습니다.
종류적용 타이밍용도
글로벌 스코프항상 자동으로 적용소프트 삭제, 멀티 테넌트, 공개 필터
로컬 스코프명시적으로 호출했을 때만 적용”인기 게시물”, “액티브 사용자” 등의 공통 필터

로컬 스코프

정의

로컬 스코프는, 모델의 메서드에 #[Scope] 어트리뷰트를 부여하여 정의합니다.
<?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);
    }
}
#[Scope] 어트리뷰트는 Illuminate\Database\Eloquent\Attributes 네임스페이스에 있습니다. PHP 8.0 이후의 네이티브 구문입니다.

사용법

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

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

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

파라미터 전달

스코프 메서드의 제2인수 이후에 추가 파라미터를 정의할 수 있습니다.
#[Scope]
protected function ofStatus(Builder $query, string $status): void
{
    $query->where('status', $status);
}
호출 시에 그대로 인수를 전달합니다.
$posts = Post::ofStatus('draft')->get();
$posts = Post::ofStatus('published')->get();

orWhere와의 조합

스코프를 orWhere로 연결할 때, 논리 그룹이 필요할 때가 있습니다.
// クロージャを使う方法(確実だが冗長)
$users = User::popular()->orWhere(function (Builder $query) {
    $query->active();
})->get();

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

글로벌 스코프

구조

글로벌 스코프는 Illuminate\Database\Eloquent\Scope 인터페이스를 구현한 클래스입니다. 이 인터페이스는 apply 메서드 하나만을 요구합니다.
// フレームワーク本体のインターフェース定義
// src/Illuminate/Database/Eloquent/Scope.php

interface Scope
{
    public function apply(Builder $builder, Model $model);
}
apply 메서드 안에서 쿼리 빌더에 제약을 추가합니다.

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

make:scope 커맨드로 뼈대를 생성합니다.
php artisan make:scope ActiveScope
app/Models/Scopes/ActiveScope.php가 생성됩니다.
<?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);
    }
}

모델에의 적용

1

#[ScopedBy] 어트리뷰트로 등록(권장)

Laravel 13에서는 #[ScopedBy] 어트리뷰트를 사용하는 것이 가장 심플합니다.
<?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
{
    //
}
여러 스코프를 배열로 지정할 수 있습니다.
#[ScopedBy([ActiveScope::class, TenantScope::class])]
class User extends Model
{
    //
}
2

booted() 메서드로 수동 등록

booted 메서드를 오버라이드하여 addGlobalScope를 호출하는 방법도 있습니다.
<?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);
    }
}
글로벌 스코프를 추가하면, User::all() 등 전 쿼리에 자동으로 WHERE is_active = 1이 붙습니다.

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

클래스를 별도 파일로 만들 정도는 아닌 단순한 스코프는, 클로저로 정의할 수 있습니다.
protected static function booted(): void
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('is_active', true);
    });
}
클로저로 정의한 스코프를 나중에 제외하려면, 클래스명이 아니라 스코프명(문자열)을 사용해야 합니다.

글로벌 스코프의 제외

특정 쿼리에서는 스코프를 무효로 하고 싶은 장면이 있습니다.
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 트레이트가 글로벌 스코프를 어떻게 활용하고 있는지를 보면, 구현 패턴을 알 수 있습니다. SoftDeletingScopeScope 인터페이스를 구현하고 있습니다.
// 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(),
            ]);
        });
    }
}
withTrashed()는 실제로는 withoutGlobalScope($this)를 호출하고 있습니다. 즉 SoftDeletingScope 자신을 제외함으로써, 삭제된 레코드도 취득할 수 있게 하고 있습니다.
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')을 추가하고 있습니다.
protected function addOnlyTrashed(Builder $builder)
{
    $builder->macro('onlyTrashed', function (Builder $builder) {
        $model = $builder->getModel();

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

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

실전적인 유스케이스

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

SaaS 애플리케이션에서는, 전 쿼리에 테넌트 ID의 필터를 자동 적용하는 것이 중요합니다.
<?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);
        }
    }
}
use App\Models\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy([TenantScope::class])]
class Post extends Model
{
    //
}
이것으로 Post::all()을 호출하는 것만으로, 인증 중인 사용자의 테넌트 데이터만이 반환됩니다.

공개/비공개 필터

관리 화면에서는 비공개 게시물도 표시하고 싶지만, 프론트엔드에서는 공개된 것만 표시하고 싶은 경우입니다.
<?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로 스코프를 제외합니다.
// フロントエンド: 公開済みのみ(PublishedScope が自動適用)
$posts = Post::latest()->get();

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

select가 아닌 addSelect를 사용한다

글로벌 스코프 내에서 컬럼을 추가할 때는 select가 아니라 addSelect를 사용해 주세요. select를 사용하면, 호출자의 쿼리가 select하고 있는 컬럼을 덮어써 버립니다.
// 悪い例: 呼び出し元の 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');
}

다음 단계

Eloquent 커스텀 캐스트

속성의 변환 로직을 커스텀 캐스트로서 구현하고, Value Object 패턴을 활용하는 방법을 배웁니다.
마지막 수정일 2026년 7월 13일