Eloquent: 시작하기

번역일: 2026년 7월 2일

Eloquent: 시작하기

소개

Laravel은 데이터베이스와의 상호작용을 직관적으로 만들어주는 ORM(Object-Relational Mapper)인 Eloquent를 기본 제공합니다. Eloquent를 사용하면 각 데이터베이스 테이블에 대응하는 "모델"을 통해 데이터를 쉽게 조회, 삽입, 수정, 삭제할 수 있습니다. 복잡한 SQL 쿼리를 직접 작성하지 않아도 됩니다.

시작하기 전에 config/database.php에서 데이터베이스 연결을 올바르게 설정했는지 확인하세요. 데이터베이스 설정에 대한 자세한 내용은 데이터베이스 설정 문서를 참고하세요.

NOTE

Eloquent를 시작하기 전에 Laravel의 쿼리 빌더에 익숙해지는 것을 권장합니다. Eloquent는 내부적으로 쿼리 빌더를 활용하므로, 쿼리 빌더의 사용법을 알아두면 Eloquent를 더욱 효과적으로 활용할 수 있습니다.

모델 클래스 생성

Eloquent 모델은 make:model Artisan 명령어로 생성합니다.

php artisan make:model Flight

모델과 함께 데이터베이스 마이그레이션을 동시에 생성하려면 --migration 또는 -m 옵션을 사용하세요.

php artisan make:model Flight --migration

모델 생성 시 팩토리, 시더, 폼 리퀘스트, 정책, 컨트롤러 등 다양한 클래스를 함께 생성할 수 있습니다. 여러 옵션을 조합해서 한 번에 관련 파일을 모두 만드는 것도 가능합니다.

<h1 id="generating-model-classes">모델과 FlightFactory 생성</h1> php artisan make:model Flight --factory php artisan make:model Flight -f # 모델과 FlightSeeder 생성 php artisan make:model Flight --seed php artisan make:model Flight -s # 모델과 FlightController 생성 php artisan make:model Flight --controller php artisan make:model Flight -c # 모델, FlightController (리소스), 폼 리퀘스트 클래스 생성 php artisan make:model Flight --controller --resource --requests php artisan make:model Flight -crR # 모델과 FlightPolicy 생성 php artisan make:model Flight --policy # 모델, 마이그레이션, 팩토리, 시더, 컨트롤러 생성 php artisan make:model Flight -mfsc # 모델에 필요한 모든 관련 클래스 한 번에 생성 (마이그레이션, 팩토리, 시더, 정책, 컨트롤러, 폼 리퀘스트) php artisan make:model Flight --all php artisan make:model Flight -a # 피벗 모델 생성 php artisan make:model Member --pivot php artisan make:model Member -p

모델 클래스 살펴보기

새로 생성한 모델의 기본 구조가 어떻게 생겼는지 궁금하다면 --help 옵션으로 확인할 수 있습니다.

php artisan make:model --help

Eloquent 모델 컨벤션

make:model 명령어로 생성된 모델은 app/Models 디렉터리에 위치합니다. 기본적인 모델 클래스 구조를 살펴보고 Eloquent의 주요 컨벤션을 알아보겠습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { // ... }

테이블 이름

위 예시에서 Flight 모델이 어떤 데이터베이스 테이블을 사용하는지 별도로 지정하지 않았다는 점을 눈치채셨나요? Eloquent는 클래스 이름을 스네이크 케이스 복수형으로 변환해서 테이블 이름으로 사용하는 컨벤션을 따릅니다. 즉, Flight 모델은 flights 테이블을, AirTrafficController 모델은 air_traffic_controllers 테이블을 자동으로 사용합니다.

만약 이 컨벤션에 맞지 않는 테이블 이름을 사용해야 한다면 모델에 $table 속성을 직접 지정하세요.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 이 모델이 사용할 테이블 이름 * * @var string */ protected $table = 'my_flights'; }

기본 키

Eloquent는 각 모델의 테이블에 id라는 이름의 기본 키 컬럼이 있다고 가정합니다. 기본 키 이름을 변경하려면 $primaryKey 속성을 정의하세요.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 테이블의 기본 키 컬럼명 * * @var string */ protected $primaryKey = 'flight_id'; }

또한 Eloquent는 기본 키가 자동 증가하는 정수 값이라고 가정합니다. 자동 증가가 아닌 기본 키나 정수가 아닌 타입의 기본 키를 사용하려면 $incrementing 속성을 false로 설정하세요.

<?php class Flight extends Model { /** * 모델의 ID가 자동 증가하는지 여부 * * @var bool */ public $incrementing = false; }

기본 키가 정수가 아닌 경우(예: UUID, 문자열)에는 $keyType 속성을 'string'으로 지정해야 합니다.

<?php class Flight extends Model { /** * 기본 키의 데이터 타입 * * @var string */ protected $keyType = 'string'; }

복합 기본 키

Eloquent는 각 모델이 단일 기본 키를 가져야 합니다. 복합 기본 키(여러 컬럼의 조합)는 지원하지 않습니다. 다만, 고유 인덱스는 단일 기본 키 외에 얼마든지 추가할 수 있습니다.

UUID와 ULID 키

기본 키로 자동 증가 정수 대신 UUID를 사용하고 싶다면 Illuminate\Database\Eloquent\Concerns\HasUuids 트레이트를 사용하세요. UUID는 RFC 4122 규격의 36자리 고유 식별자입니다.

use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Model; class Article extends Model { use HasUuids; // ... } $article = Article::create(['title' => '한국 여행 가이드']); $article->id; // 예: "7da78229-4445-4b0e-bd7e-84d5d5553a1f"

HasUuids 트레이트를 사용하면 모델이 생성될 때 자동으로 UUID가 생성되어 기본 키에 할당됩니다. UUID 컬럼은 마이그레이션에서 uuid() 타입으로 정의해야 합니다.

$table->uuid('id')->primary();

기본적으로 HasUuids 트레이트는 순서 정렬이 가능한(ordered) UUID를 생성합니다. 데이터베이스 인덱스 효율 측면에서 일반 UUID보다 유리합니다. UUID 생성 방식을 직접 제어하고 싶다면 모델에 newUniqueId 메서드를 오버라이드하고, UUID를 적용할 컬럼을 uniqueIds 메서드에서 반환하세요.

use Ramsey\Uuid\Uuid; /** * 모델에서 사용할 새 UUID 생성 */ public function newUniqueId(): string { return (string) Uuid::uuid4(); } /** * UUID를 적용할 컬럼 목록 반환 * * @return array<int, string> */ public function uniqueIds(): array { return ['id', 'discount_code']; }

ULID를 사용하고 싶다면 HasUlids 트레이트를 사용하세요. ULID는 UUID와 달리 26자리로, 사전식(lexicographic) 정렬이 가능합니다.

use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Model; class Article extends Model { use HasUlids; // ... } $article = Article::create(['title' => '제주도 여행 일정']); $article->id; // 예: "01gd6r360bp37zj17nxb55yv40"

ULID 컬럼은 마이그레이션에서 ulid() 타입으로 정의해야 합니다.

$table->ulid('id')->primary();

타임스탬프

기본적으로 Eloquent는 모델의 테이블에 created_atupdated_at 컬럼이 있다고 가정하고, 모델 생성 및 수정 시 이 값을 자동으로 관리합니다. 이 자동 관리 기능이 필요 없다면 $timestamps 속성을 false로 설정하세요.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 타임스탬프 자동 관리 여부 * * @var bool */ public $timestamps = false; }

타임스탬프의 저장 형식을 커스터마이즈하려면 $dateFormat 속성을 설정하세요. 이 형식은 데이터베이스 저장 시와 배열/JSON으로 직렬화할 때 모두 적용됩니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 날짜 저장 형식 * * @var string */ protected $dateFormat = 'U'; }

타임스탬프 컬럼 이름을 변경하려면 CREATED_AT, UPDATED_AT 상수를 정의하세요.

<?php class Flight extends Model { const CREATED_AT = 'creation_date'; const UPDATED_AT = 'updated_date'; }

모델의 updated_at 타임스탬프를 변경하지 않고 작업하려면 withoutTimestamps 메서드 안에서 작업을 수행하세요.

Model::withoutTimestamps(fn () => $post->increment('reads'));

데이터베이스 연결

모든 Eloquent 모델은 기본적으로 애플리케이션에 설정된 기본 데이터베이스 연결을 사용합니다. 특정 모델에서 다른 연결을 사용하려면 $connection 속성을 지정하세요.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 이 모델이 사용할 데이터베이스 연결 이름 * * @var string */ protected $connection = 'mysql_secondary'; }

기본 속성값

새 모델 인스턴스를 생성할 때 일부 속성에 기본값을 설정하고 싶다면 $attributes 속성에 정의하세요. 여기서 지정하는 값은 데이터베이스에서 읽어온 원시 값(raw value)과 동일한 형식이어야 하며, Eloquent의 캐스팅이나 뮤테이터가 적용되지 않은 상태의 값입니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 모델 속성의 기본값 * * @var array */ protected $attributes = [ 'options' => '[]', 'delayed' => false, ]; }

Eloquent 엄격 모드 설정

Eloquent는 여러 상황에서 예외를 발생시키거나 경고를 표시하는 엄격 모드를 제공합니다. 주로 AppServiceProviderboot 메서드에서 설정합니다.

지연 로딩 방지

Eloquent 관계의 지연 로딩(Lazy Loading)은 N+1 쿼리 문제를 일으킬 수 있습니다. 개발 중에 이를 방지하려면 preventLazyLoading 메서드를 사용하세요. 프로덕션 환경에서는 지연 로딩이 실제로 발생해도 앱이 중단되지 않도록 환경 조건을 분리하는 것이 좋습니다.

use Illuminate\Database\Eloquent\Model; /** * 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { Model::preventLazyLoading(! app()->isProduction()); }

preventLazyLoadingtrue를 전달하면 지연 로딩 시도 시 Illuminate\Database\LazyLoadingViolationException 예외가 발생합니다.

지연 로딩 위반 동작을 커스터마이즈하려면 handleLazyLoadingViolationsUsing 메서드로 직접 핸들러를 등록하세요. 예를 들어 예외 대신 로그만 남기도록 설정할 수 있습니다.

Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) { $class = $model::class; info("지연 로딩 감지: [{$class}::{$relation}]"); });

존재하지 않는 속성 할당 방지

일반적으로 모델의 $fillable 또는 $guarded 배열에 포함되지 않은 속성을 대량 할당(mass assignment)하면 해당 속성은 무시됩니다. 그러나 경우에 따라 이 상황을 명시적으로 알고 싶을 수 있습니다. preventSilentlyDiscardingAttributes 메서드를 사용하면 채울 수 없는 속성을 할당하려 할 때 예외가 발생합니다.

Model::preventSilentlyDiscardingAttributes(! app()->isProduction());

접근 불가능한 속성 접근 방지

모델 인스턴스에 로드되지 않은 속성에 접근하면 기본적으로 null을 반환합니다. preventAccessingMissingAttributes 메서드를 사용하면 존재하지 않는 속성에 접근할 때 예외를 발생시킵니다.

Model::preventAccessingMissingAttributes(! app()->isProduction());

모든 엄격 모드 한 번에 활성화

위의 세 가지 엄격 모드를 모두 활성화하려면 shouldBeStrict 메서드를 사용하세요.

Model::shouldBeStrict(! app()->isProduction());

모델 조회

모델과 해당 테이블을 생성했다면 데이터베이스에서 데이터를 조회할 준비가 된 것입니다. 각 Eloquent 모델은 강력한 쿼리 빌더로 동작하며, 이를 통해 테이블의 데이터를 유연하게 조회할 수 있습니다. 모델의 all 메서드는 테이블의 모든 레코드를 가져옵니다.

use App\Models\Flight; foreach (Flight::all() as $flight) { echo $flight->name; }

쿼리 빌드하기

all 메서드는 테이블의 모든 레코드를 반환합니다. 하지만 Eloquent 모델은 쿼리 빌더이기도 하므로, 쿼리 조건을 추가한 뒤 get 메서드로 결과를 가져올 수 있습니다.

$flights = Flight::where('active', 1) ->orderBy('name') ->take(10) ->get();

NOTE

Eloquent 모델은 쿼리 빌더의 모든 메서드를 사용할 수 있습니다. 쿼리 빌더의 다양한 기능은 쿼리 빌더 문서를 참고하세요.

모델 새로 고침

이미 데이터베이스에서 가져온 모델 인스턴스를 최신 데이터로 갱신하려면 freshrefresh 메서드를 사용하세요.

fresh는 데이터베이스에서 모델을 새로 가져와 새 인스턴스를 반환하며, 기존 인스턴스는 그대로 유지됩니다.

$flight = Flight::where('number', 'FR 900')->first(); $freshFlight = $flight->fresh();

refresh는 기존 인스턴스를 데이터베이스의 최신 데이터로 직접 갱신합니다. 이미 로드된 관계도 함께 갱신됩니다.

$flight = Flight::where('number', 'FR 900')->first(); $flight->number = 'FR 456'; $flight->refresh(); $flight->number; // "FR 900" — 데이터베이스 값으로 원복됨

컬렉션

지금까지 살펴본 all, get 등의 메서드는 여러 레코드를 반환할 때 Illuminate\Database\Eloquent\Collection 인스턴스를 반환합니다. 이 클래스는 Laravel의 기본 Collection 클래스를 상속하며, Eloquent 모델 컬렉션을 다루기 위한 다양한 유용한 메서드를 추가로 제공합니다.

컬렉션은 PHP의 이터러블(iterable)이므로 일반 배열처럼 foreach로 순회할 수 있습니다.

foreach ($flights as $flight) { echo $flight->name; }

청크 단위 조회

수만 건 이상의 레코드를 한 번에 all이나 get으로 가져오면 메모리 부족이 발생할 수 있습니다. 이럴 때는 chunk 메서드를 사용해 지정한 수만큼씩 나눠서 처리하세요.

chunk 메서드는 일정 수의 Eloquent 모델을 가져와 클로저에 전달합니다. 현재 청크를 처리한 뒤 다음 청크를 가져오므로 메모리 사용량을 크게 줄일 수 있습니다.

use App\Models\Flight; use Illuminate\Database\Eloquent\Collection; Flight::chunk(200, function (Collection $flights) { foreach ($flights as $flight) { // ... } });

chunk 메서드의 첫 번째 인자는 청크당 가져올 레코드 수입니다. 두 번째 클로저는 각 청크마다 호출됩니다. 청크를 가져올 때마다 데이터베이스 쿼리가 실행됩니다.

청크 처리 중 결과를 필터링하는 컬럼을 동시에 수정한다면 예상치 못한 결과가 발생할 수 있습니다. 예를 들어, active 컬럼 기준으로 청크를 가져오면서 동시에 active 값을 변경하면 일부 레코드가 누락될 수 있습니다. 이런 경우에는 chunkById 메서드를 사용하세요. 이 메서드는 기본 키를 기준으로 페이지를 나누어 처리하므로 안전합니다.

Flight::where('departed', true) ->chunkById(200, function (Collection $flights) { $flights->each->update(['departed' => false]); }, column: 'id');

지연 컬렉션으로 청크 조회

lazy 메서드는 내부적으로 청크 방식으로 쿼리를 실행하면서도, 단일 스트림처럼 결과를 다룰 수 있게 해줍니다. 반환값은 LazyCollection입니다.

use App\Models\Flight; foreach (Flight::lazy() as $flight) { // ... }

청크 처리 중 조회 기준 컬럼을 수정할 가능성이 있다면 lazyById 메서드를 사용하세요. 기본 키 기준으로 순차적으로 조회하므로 레코드 누락 문제를 방지할 수 있습니다.

Flight::where('departed', true) ->lazyById(200, column: 'id') ->each->update(['departed' => false]);

내림차순 기본 키 기준으로 처리하려면 lazyByIdDesc 메서드를 사용하세요.

커서

lazy 메서드와 유사하게 cursor 메서드도 대량의 데이터를 메모리 효율적으로 처리할 수 있습니다. cursor는 데이터베이스 쿼리를 단 한 번만 실행하고, 각 모델을 실제로 순회할 때마다 하이드레이션(hydration)합니다. 따라서 전체 결과에서 특정 시점에 오직 하나의 Eloquent 모델만 메모리에 유지됩니다.

use App\Models\Flight; foreach (Flight::where('destination', 'Zurich')->cursor() as $flight) { // ... }

cursorIlluminate\Support\LazyCollection 인스턴스를 반환합니다. 지연 컬렉션을 사용하면 일반 Laravel 컬렉션의 다양한 메서드를 메모리 효율적으로 활용할 수 있습니다.

use App\Models\User; $users = User::cursor()->filter(function (User $user) { return $user->id > 500; }); foreach ($users as $user) { echo $user->id; }

다만, cursor는 내부적으로 PHP의 제너레이터를 사용하므로 단 하나의 쿼리만 실행되지만, 전체 결과를 PHP 메모리에 버퍼링합니다. 매우 큰 데이터셋에서는 lazy 메서드가 더 적합할 수 있습니다.

고급 서브쿼리

서브쿼리로 컬럼 선택

Eloquent는 고급 서브쿼리를 지원합니다. 서브쿼리를 사용하면 연관 테이블의 정보를 추가 조인 없이 단일 쿼리로 가져올 수 있습니다. 예를 들어, destinations 테이블과 flights 테이블이 있을 때, 각 목적지로 출발하는 가장 최근 항공편의 이름을 한 번에 조회할 수 있습니다.

use App\Models\Destination; use App\Models\Flight; return Destination::addSelect(['last_flight' => Flight::select('name') ->whereColumn('destination_id', 'destinations.id') ->orderByDesc('arrived_at') ->limit(1) ])->get();

서브쿼리로 정렬

쿼리 빌더의 orderBy 메서드에서도 서브쿼리를 사용할 수 있습니다. 예를 들어, 각 목적지에 마지막으로 도착한 항공편 시간을 기준으로 목적지를 정렬하려면 다음과 같이 작성합니다.

return Destination::orderByDesc( Flight::select('arrived_at') ->whereColumn('destination_id', 'destinations.id') ->orderByDesc('arrived_at') ->limit(1) )->get();

단일 모델 / 집계값 조회

전체 목록이 아닌 특정 레코드 하나만 조회할 때는 find, first, firstWhere 메서드를 사용합니다. 이 메서드들은 컬렉션 대신 단일 모델 인스턴스를 반환합니다.

use App\Models\Flight; // 기본 키로 단일 모델 조회 $flight = Flight::find(1); // 조건에 맞는 첫 번째 모델 조회 $flight = Flight::where('active', 1)->first(); // 위와 동일하게 동작하는 단축 표현 $flight = Flight::firstWhere('active', 1);

조회 결과가 없을 때 다른 동작을 수행하려면 findOrfirstOr 메서드를 사용하세요. 클로저를 전달하면 결과가 없을 때 클로저의 반환값이 사용됩니다.

$flight = Flight::findOr(1, function () { // ... }); $flight = Flight::where('legs', '>', 3)->firstOr(function () { // ... });

조회 결과가 없을 때 예외 발생

조회 실패 시 예외를 발생시키려면 findOrFail, firstOrFail 메서드를 사용하세요. 결과가 없으면 Illuminate\Database\Eloquent\ModelNotFoundException이 발생하며, 별도로 처리하지 않으면 클라이언트에게 자동으로 404 응답이 반환됩니다.

$flight = Flight::findOrFail(1); $flight = Flight::where('legs', '>', 3)->firstOrFail();

모델 조회 또는 생성

firstOrCreate 메서드는 첫 번째 인자로 전달한 조건으로 레코드를 조회하고, 없으면 첫 번째와 두 번째 인자를 합쳐 새 레코드를 생성합니다.

firstOrNew 메서드도 동일하게 동작하지만, 레코드가 없을 때 데이터베이스에 저장하지 않고 새 모델 인스턴스만 반환합니다. save를 직접 호출해야 저장됩니다.

use App\Models\Flight; // 이름으로 조회, 없으면 생성 $flight = Flight::firstOrCreate([ 'name' => 'London to Paris', ]); // 이름으로 조회, 없으면 추가 속성과 함께 생성 $flight = Flight::firstOrCreate( ['name' => 'London to Paris'], ['delayed' => 1, 'arrival_time' => '11:30'] ); // 이름으로 조회, 없으면 새 인스턴스 반환 (저장 안 됨) $flight = Flight::firstOrNew([ 'name' => 'London to Paris', ]); // 이름으로 조회, 없으면 추가 속성 포함 새 인스턴스 반환 $flight = Flight::firstOrNew( ['name' => 'Tokyo to Sydney'], ['delayed' => 1, 'arrival_time' => '11:30'] );

집계값 조회

Eloquent 모델에서도 쿼리 빌더의 count, sum, max 등 집계 메서드를 그대로 사용할 수 있습니다. 이 메서드들은 모델 인스턴스 대신 스칼라 값을 반환합니다.

$count = Flight::where('active', 1)->count(); $max = Flight::where('active', 1)->max('duration');

모델 삽입과 수정

삽입

데이터베이스에 새 레코드를 삽입할 때는 새 모델 인스턴스를 만들고 속성을 설정한 뒤 save를 호출합니다.

<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Models\Flight; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class FlightController extends Controller { /** * 새 항공편을 데이터베이스에 저장합니다. */ public function store(Request $request): RedirectResponse { // 요청 유효성 검사 ... $flight = new Flight; $flight->name = $request->name; $flight->save(); return redirect('/flights'); } }

save를 호출하면 데이터베이스에 INSERT 쿼리가 실행됩니다. created_atupdated_at 타임스탬프도 자동으로 설정되므로 직접 지정할 필요가 없습니다.

수정

이미 존재하는 모델을 수정할 때도 save 메서드를 사용합니다. 수정하려면 먼저 모델을 조회하고, 변경할 속성을 설정한 뒤 save를 호출하세요. updated_at 타임스탬프는 자동으로 갱신됩니다.

use App\Models\Flight; $flight = Flight::find(1); $flight->name = 'Paris to London'; $flight->save();

경우에 따라 기존 모델을 수정하거나 새 모델을 생성해야 할 수 있습니다. firstOrCreate처럼 updateOrCreate 메서드는 첫 번째 인자 조건으로 레코드를 찾고, 있으면 두 번째 인자로 속성을 업데이트하며, 없으면 두 인자를 합쳐 새 레코드를 생성합니다.

$flight = Flight::updateOrCreate( ['departure' => 'Oakland', 'destination' => 'San Diego'], ['price' => 99, 'discounted' => 1] );

대량 수정

where로 조건을 지정하고 update를 호출하면 여러 레코드를 한 번에 수정할 수 있습니다. update 메서드는 변경할 컬럼과 값을 배열로 받습니다.

Flight::where('active', 1) ->where('destination', 'San Diego') ->update(['delayed' => 1]);

WARNING

update로 대량 수정을 실행하면 각 모델에 대해 saving, saved, updating, updated 이벤트가 발생하지 않습니다. 대량 수정은 모델 인스턴스를 생성하지 않고 직접 쿼리를 실행하기 때문입니다.

변경된 속성 확인

Eloquent는 모델의 내부 상태를 추적하며, 마지막으로 데이터베이스에서 로드된 이후 속성이 변경되었는지 확인할 수 있는 isDirty, isClean, wasChanged 메서드를 제공합니다.

isDirty는 아직 저장하지 않은 변경사항이 있는지 확인하며, 특정 속성명을 전달하면 해당 속성만 확인합니다. isClean은 반대로 변경사항이 없을 때 true를 반환합니다.

use App\Models\User; $user = User::create([ 'first_name' => 'Taylor', 'last_name' => 'Otwell', 'title' => 'Developer', ]); $user->title = 'Painter'; $user->isDirty(); // true $user->isDirty('title'); // true $user->isDirty('first_name'); // false $user->isDirty(['first_name', 'title']); // true $user->isClean(); // false $user->isClean('title'); // false $user->isClean('first_name'); // true $user->isClean(['first_name', 'title']); // false $user->save(); $user->isDirty(); // false $user->isClean(); // true

wasChanged는 마지막으로 save를 호출했을 때 어떤 속성이 변경되었는지 확인합니다.

$user = User::create([ 'first_name' => 'Taylor', 'last_name' => 'Otwell', 'title' => 'Developer', ]); $user->title = 'Painter'; $user->save(); $user->wasChanged(); // true $user->wasChanged('title'); // true $user->wasChanged(['title', 'slug']); // true $user->wasChanged('first_name'); // false $user->wasChanged(['first_name', 'title']); // true

getOriginal은 현재 변경 여부와 관계없이, 모델을 처음 로드했을 때의 원래 속성값을 반환합니다. 특정 속성명을 전달하면 해당 속성의 원래 값만 반환합니다.

$user = User::find(1); $user->name; // John $user->email; // john@example.com $user->name = 'Jack'; $user->name; // Jack $user->getOriginal('name'); // John $user->getOriginal(); // 원래 속성 배열 전체

대량 할당

create 메서드를 사용하면 단 한 줄로 새 모델을 저장할 수 있습니다. 이 메서드는 새로 생성된 모델 인스턴스를 반환합니다.

use App\Models\Flight; $flight = Flight::create([ 'name' => 'London to Paris', ]);

단, create 메서드를 사용하기 전에 모델에 $fillable 또는 $guarded 속성을 반드시 설정해야 합니다. 사용자 입력을 그대로 모델에 대량 할당하는 것은 보안상 위험하기 때문에, Eloquent는 기본적으로 대량 할당을 보호합니다.

대량 할당 취약점(mass assignment vulnerability)은 사용자가 HTTP 요청을 통해 예상치 못한 필드 값을 전송하고, 이 값이 데이터베이스의 민감한 컬럼을 덮어쓰는 경우에 발생합니다. 예를 들어, 악의적인 사용자가 is_admin=1을 전송해 관리자 권한을 획득할 수 있습니다.

이를 방지하기 위해 대량 할당을 허용할 컬럼을 $fillable로 명시적으로 지정하거나, 허용하지 않을 컬럼을 $guarded로 지정합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 대량 할당 가능한 컬럼 목록 * * @var array<int, string> */ protected $fillable = ['name']; }

$fillable을 설정한 후 create 메서드로 데이터를 삽입할 수 있습니다.

$flight = Flight::create(['name' => 'London to Paris']);

모든 컬럼을 대량 할당 가능하게 하려면 $guarded를 빈 배열로 설정합니다. 단, 이 경우 보안에 각별히 주의해야 합니다.

/** * 대량 할당을 막을 컬럼 목록 (빈 배열 = 전체 허용) * * @var array<string> */ protected $guarded = [];

대량 할당 예외 설정

기본적으로 $fillable에 포함되지 않은 컬럼은 대량 할당 시 조용히 무시됩니다. 프로덕션 환경에서는 이 동작이 적절하지만, 개발 중에는 왜 모델 변경이 반영되지 않는지 혼란스러울 수 있습니다.

preventSilentlyDiscardingAttributes 메서드를 사용하면 채울 수 없는 속성을 할당하려 할 때 예외가 발생합니다.

Model::preventSilentlyDiscardingAttributes(! app()->isProduction());

Upsert

특정 조건으로 레코드가 있으면 수정하고 없으면 생성하는 작업이 필요할 때 upsert 메서드를 사용합니다. 첫 번째 인자는 삽입하거나 수정할 데이터 배열, 두 번째 인자는 레코드를 고유하게 식별할 컬럼 목록, 세 번째 인자는 레코드가 이미 존재할 경우 업데이트할 컬럼 목록입니다.

Flight::upsert([ ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99], ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150] ], uniqueBy: ['departure', 'destination'], update: ['price']);

WARNING

SQL Server를 제외한 모든 데이터베이스에서 upsert의 두 번째 인자에 지정된 컬럼은 "primary" 또는 "unique" 인덱스가 있어야 합니다. MariaDB와 MySQL은 두 번째 인자를 무시하고 테이블의 "primary" 및 "unique" 인덱스를 기준으로 사용합니다.

또한 upsertupdated_at 타임스탬프를 자동으로 갱신하지 않습니다. 필요하다면 update 컬럼 목록에 직접 포함시키세요.

모델 삭제

모델을 삭제하려면 모델 인스턴스에서 delete 메서드를 호출하세요.

use App\Models\Flight; $flight = Flight::find(1); $flight->delete();

truncate 메서드를 사용하면 테이블의 모든 레코드를 삭제하고, 자동 증가 ID도 초기화할 수 있습니다.

Flight::truncate();

기본 키로 직접 삭제

위 예시처럼 먼저 조회한 뒤 삭제하는 대신, 기본 키로 바로 삭제할 수도 있습니다.

Flight::destroy(1); Flight::destroy(1, 2, 3); Flight::destroy([1, 2, 3]); Flight::destroy(collect([1, 2, 3]));

WARNING

destroy 메서드는 각 모델을 개별적으로 로드해서 delete를 호출하므로, 각 모델에 대해 deletingdeleted 이벤트가 정상적으로 발생합니다.

쿼리를 통한 삭제

where 조건과 함께 delete 메서드를 사용하면 조건에 맞는 여러 레코드를 한 번에 삭제할 수 있습니다.

$deleted = Flight::where('active', 0)->delete();

WARNING

쿼리를 통한 대량 삭제는 각 모델 인스턴스를 로드하지 않으므로 deletingdeleted 이벤트가 발생하지 않습니다.

소프트 삭제

실제로 데이터베이스에서 레코드를 삭제하는 대신, 삭제된 시각을 기록하는 방식을 소프트 삭제라고 합니다. 소프트 삭제를 사용하면 모델의 deleted_at 속성에 삭제 시각이 기록되고, 이후 쿼리에서 해당 레코드는 자동으로 제외됩니다.

소프트 삭제를 활성화하려면 Illuminate\Database\Eloquent\SoftDeletes 트레이트를 추가하세요.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Flight extends Model { use SoftDeletes; }

NOTE

SoftDeletes 트레이트는 deleted_at 속성을 자동으로 DateTime / Carbon 인스턴스로 변환합니다.

마이그레이션에서도 deleted_at 컬럼을 추가해야 합니다. Laravel의 스키마 빌더에 이를 위한 헬퍼 메서드가 제공됩니다.

use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; Schema::table('flights', function (Blueprint $table) { $table->softDeletes(); }); Schema::table('flights', function (Blueprint $table) { $table->dropSoftDeletes(); });

이제 모델의 delete 메서드를 호출하면 실제 삭제가 아닌, deleted_at 컬럼에 현재 시각이 기록됩니다. 소프트 삭제 기능을 사용하는 모델을 쿼리하면 deleted_at이 설정된 레코드는 자동으로 제외됩니다.

모델이 소프트 삭제되었는지 확인하려면 trashed 메서드를 사용하세요.

if ($flight->trashed()) { // ... }

소프트 삭제된 모델 조회

소프트 삭제된 레코드 포함 조회

기본적으로 쿼리 결과에서 소프트 삭제된 레코드는 제외됩니다. 소프트 삭제된 레코드를 포함시키려면 withTrashed 메서드를 사용하세요.

use App\Models\Flight; $flights = Flight::withTrashed() ->where('account_id', 1) ->get();

관계 쿼리에서도 withTrashed를 사용할 수 있습니다.

$flight->history()->withTrashed()->get();

소프트 삭제된 레코드만 조회

소프트 삭제된 레코드만 가져오려면 onlyTrashed 메서드를 사용하세요.

$flights = Flight::onlyTrashed() ->where('airline_id', 1) ->get();

소프트 삭제 복원

소프트 삭제된 모델을 다시 활성화하려면 restore 메서드를 호출하세요. deleted_at 컬럼이 null로 초기화됩니다.

$flight->restore();

쿼리에서 여러 모델을 한 번에 복원할 수도 있습니다.

Flight::withTrashed() ->where('airline_id', 1) ->restore();

관계 쿼리에서도 restore를 사용할 수 있습니다.

$flight->history()->restore();

영구 삭제

소프트 삭제 없이 데이터베이스에서 완전히 삭제하려면 forceDelete 메서드를 사용하세요.

$flight->forceDelete();

관계에서도 영구 삭제를 수행할 수 있습니다.

$flight->history()->forceDelete();

모델 정리(Pruning)

오래되거나 더 이상 필요 없는 모델을 주기적으로 삭제하고 싶다면 Illuminate\Database\Eloquent\Prunable 또는 Illuminate\Database\Eloquent\MassPrunable 트레이트를 사용하세요.

모델에 Prunable 트레이트를 추가하고, prunable 메서드에서 삭제할 모델을 결정하는 쿼리 빌더를 반환하세요.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Prunable; class Flight extends Model { use Prunable; /** * 정리 대상 모델 쿼리를 반환합니다. */ public function prunable(): Builder { return static::where('created_at', '<=', now()->subMonth()); } }

Prunable 트레이트를 사용할 때 모델에 pruning 메서드를 정의하면 모델이 삭제되기 전에 실행됩니다. 이 메서드에서 저장된 파일 삭제 등의 추가 정리 작업을 수행할 수 있습니다.

/** * 모델 정리 전 처리 작업 */ protected function pruning(): void { // 연관된 리소스(예: 저장된 파일) 삭제 ... }

정리 대상 모델을 개별 로드하지 않고 대량 삭제하려면 MassPrunable 트레이트를 사용하세요. 이 트레이트를 사용하면 pruning 메서드가 호출되지 않으며 deleting, deleted 이벤트도 발생하지 않습니다. 단, 모델을 로드하지 않으므로 훨씬 효율적입니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\MassPrunable; class Flight extends Model { use MassPrunable; /** * 정리 대상 모델 쿼리를 반환합니다. */ public function prunable(): Builder { return static::where('created_at', '<=', now()->subMonth()); } }

다음으로 model:prune Artisan 명령어를 등록하세요. 이 명령어는 routes/console.php에서 스케줄링할 수 있습니다.

use Illuminate\Support\Facades\Schedule; Schedule::command('model:prune')->daily();

model:prune 명령어는 app/Models 디렉터리에서 Prunable 또는 MassPrunable 트레이트를 사용하는 모델을 자동으로 찾습니다. 다른 위치의 모델을 대상으로 지정하려면 --model 옵션을 사용하세요.

php artisan model:prune --model="App\Models\Flight"

특정 모델은 제외하고 나머지를 모두 정리하려면 --except 옵션을 사용하세요.

php artisan model:prune --except="App\Models\Flight"

정리 전에 얼마나 많은 모델이 삭제될지 미리 확인하려면 --pretend 옵션을 사용하세요.

php artisan model:prune --pretend

WARNING

소프트 삭제 모델도 prunable 쿼리에 매칭되면 영구 삭제(forceDelete)됩니다.

모델 복제

기존 모델의 속성을 복사해 새 인스턴스를 만들려면 replicate 메서드를 사용하세요. 속성값이 비슷한 모델을 여러 개 만들어야 할 때 유용합니다.

use App\Models\Address; $shipping = Address::create([ 'type' => 'shipping', 'line_1' => '123 Example Street', 'city' => '서울', 'country' => 'KR', ]); $billing = $shipping->replicate()->fill([ 'type' => 'billing', ]); $billing->save();

복제에서 제외할 속성이 있다면 replicate 메서드에 배열로 전달하세요.

$flight = Flight::create([ 'destination' => 'LAX', 'origin' => 'LHR', 'last_flown' => '2020-03-04 11:00:00', 'last_pilot_id' => 747, ]); $flight = $flight->replicate([ 'last_flown', 'last_pilot_id', ]);

# Eloquent: 시작하기

소개

Laravel에는 데이터베이스와의 상호작용을 편리하게 해주는 ORM(Object-Relational Mapper)인 Eloquent가 포함되어 있습니다. Eloquent를 사용하면 각 데이터베이스 테이블에 대응하는 "모델"이 존재하며, 이 모델을 통해 해당 테이블의 레코드를 조회, 삽입, 수정, 삭제할 수 있습니다.

NOTE

시작하기 전에 애플리케이션의 config/database.php 설정 파일에서 데이터베이스 연결을 구성해야 합니다. 데이터베이스 설정에 대한 자세한 내용은 데이터베이스 설정 문서를 참고하세요.

Laravel Bootcamp

Laravel을 처음 접한다면 Laravel Bootcamp를 먼저 살펴보는 것을 추천합니다. Bootcamp에서는 Eloquent를 활용해 첫 번째 Laravel 애플리케이션을 직접 만들어보는 과정을 안내합니다. Laravel과 Eloquent가 제공하는 다양한 기능을 실습하며 익힐 수 있는 좋은 출발점입니다.

모델 클래스 생성하기

Eloquent 모델은 일반적으로 app/Models 디렉터리에 위치하며, Illuminate\Database\Eloquent\Model 클래스를 상속합니다. make:model Artisan 커맨드로 새 모델을 생성할 수 있습니다:

php artisan make:model Flight

모델 생성 시 데이터베이스 마이그레이션도 함께 만들려면 --migration 또는 -m 옵션을 추가하세요:

php artisan make:model Flight --migration

모델과 함께 팩토리, 시더, 정책, 컨트롤러, 폼 리퀘스트 등 다양한 클래스를 한 번에 생성할 수도 있습니다. 옵션을 조합하면 여러 클래스를 동시에 만들 수 있어 편리합니다:

<h1 id="applying-global-scopes">모델과 FlightFactory 클래스 생성</h1> php artisan make:model Flight --factory php artisan make:model Flight -f <h1 id="anonymous-global-scopes">모델과 FlightSeeder 클래스 생성</h1> php artisan make:model Flight --seed php artisan make:model Flight -s <h1 id="removing-global-scopes">모델과 FlightController 클래스 생성</h1> php artisan make:model Flight --controller php artisan make:model Flight -c <h1 id="local-scopes">모델, FlightController 리소스 클래스, 폼 리퀘스트 클래스 생성</h1> php artisan make:model Flight --controller --resource --requests php artisan make:model Flight -crR <h1 id="utilizing-a-local-scope">모델과 FlightPolicy 클래스 생성</h1> php artisan make:model Flight --policy <h1 id="dynamic-scopes">모델, 마이그레이션, 팩토리, 시더, 컨트롤러 생성</h1> php artisan make:model Flight -mfsc <h1 id="pending-attributes">모델, 마이그레이션, 팩토리, 시더, 정책, 컨트롤러, 폼 리퀘스트 한 번에 생성</h1> php artisan make:model Flight --all php artisan make:model Flight -a <h1 id="comparing-models">피벗 모델 생성</h1> php artisan make:model Member --pivot php artisan make:model Member -p

모델 정보 확인하기

모델 코드만 훑어봐서는 사용 가능한 속성과 관계(Relationship)를 한눈에 파악하기 어려울 수 있습니다. 이럴 때는 model:show Artisan 커맨드를 활용하세요. 모델의 모든 속성과 관계를 깔끔하게 정리해서 보여줍니다:

php artisan model:show Flight

Eloquent 모델 컨벤션

make:model 명령으로 생성된 모델은 app/Models 디렉토리에 위치합니다. 기본적인 모델 클래스를 살펴보며 Eloquent의 주요 컨벤션을 알아보겠습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { // ... }

테이블 이름

위 예시에서 데이터베이스 테이블을 별도로 지정하지 않았음을 눈치채셨을 겁니다. Eloquent는 별도 지정이 없으면 클래스 이름을 스네이크 케이스(snake_case)로 변환한 복수형을 테이블 이름으로 자동 사용합니다. 예를 들어 Flight 모델은 flights 테이블을, AirTrafficController 모델은 air_traffic_controllers 테이블을 사용한다고 간주합니다.

테이블 이름이 이 컨벤션과 다르다면, 모델에 $table 속성을 직접 지정하면 됩니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 모델과 연결된 테이블 이름 * * @var string */ protected $table = 'my_flights'; }

기본 키

Eloquent는 각 모델의 테이블에 id라는 기본 키 컬럼이 존재한다고 가정합니다. 다른 컬럼을 기본 키로 사용하려면 $primaryKey 속성을 지정하세요.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 테이블의 기본 키 컬럼명 * * @var string */ protected $primaryKey = 'flight_id'; }

Eloquent는 기본 키가 자동 증가하는 정수형이라고도 가정합니다. 자동 증가하지 않거나 숫자가 아닌 기본 키를 사용할 경우, $incrementing 속성을 false로 설정해야 합니다.

<?php class Flight extends Model { /** * 모델 ID의 자동 증가 여부 * * @var bool */ public $incrementing = false; }

기본 키가 정수형이 아닌 경우, $keyType 속성도 'string'으로 지정해야 합니다.

<?php class Flight extends Model { /** * 기본 키의 데이터 타입 * * @var string */ protected $keyType = 'string'; }

복합 기본 키

Eloquent 모델은 단일 기본 키만 지원합니다. 복합 기본 키(여러 컬럼의 조합)는 지원되지 않습니다. 필요하다면 단일 기본 키와 별개로, 여러 컬럼에 대한 복합 유니크 인덱스를 테이블에 추가하는 방식을 활용하세요.

UUID 및 ULID 키

자동 증가 정수 대신 UUID를 기본 키로 사용할 수도 있습니다. UUID는 36자리의 영숫자로 구성된 전 세계적으로 고유한 식별자입니다.

UUID 키를 사용하려면 모델에 Illuminate\Database\Eloquent\Concerns\HasUuids 트레이트를 추가하세요. 테이블에도 UUID 타입의 기본 키 컬럼이 있어야 합니다.

use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Model; class Article extends Model { use HasUuids; // ... } $article = Article::create(['title' => '라라벨로 시작하는 웹 개발']); $article->id; // "8f8e8478-9035-4d23-b9a7-62f4d2612ce5"

HasUuids 트레이트는 기본적으로 정렬 가능한 UUID를 생성합니다. 이 UUID는 사전순 정렬이 가능해 인덱스 성능에 유리합니다.

UUID 생성 방식을 커스터마이징하려면 newUniqueId 메서드를 정의하세요. UUID를 적용할 컬럼을 직접 지정하려면 uniqueIds 메서드를 사용합니다.

use Ramsey\Uuid\Uuid; /** * 모델의 새 UUID를 생성합니다. */ public function newUniqueId(): string { return (string) Uuid::uuid4(); } /** * UUID를 부여할 컬럼 목록을 반환합니다. * * @return array<int, string> */ public function uniqueIds(): array { return ['id', 'discount_code']; }

UUID 대신 ULID를 사용할 수도 있습니다. ULID는 UUID와 유사하지만 26자리로 더 짧고, 마찬가지로 사전순 정렬이 가능해 인덱싱에 효율적입니다. ULID를 사용하려면 Illuminate\Database\Eloquent\Concerns\HasUlids 트레이트를 적용하고, ULID 타입의 기본 키 컬럼을 테이블에 추가하세요.

use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Model; class Article extends Model { use HasUlids; // ... } $article = Article::create(['title' => '라라벨로 시작하는 웹 개발']); $article->id; // "01gd4d3tgrrfqeda94gdbtdk5c"

타임스탬프

Eloquent는 기본적으로 모델에 대응하는 테이블에 created_atupdated_at 컬럼이 존재한다고 가정하며, 레코드가 생성되거나 수정될 때 이 값들을 자동으로 관리합니다. 자동 관리가 필요 없다면 $timestamps 속성을 false로 설정하세요.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 타임스탬프 자동 관리 여부 * * @var bool */ public $timestamps = false; }

타임스탬프 형식을 변경하려면 $dateFormat 속성을 설정하세요. 이 속성은 데이터베이스 저장 형식과 배열·JSON 직렬화 시의 형식 모두에 영향을 줍니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 날짜 컬럼의 저장 형식 * * @var string */ protected $dateFormat = 'U'; }

타임스탬프 컬럼의 이름을 변경하려면 모델에 CREATED_AT, UPDATED_AT 상수를 정의하세요.

<?php class Flight extends Model { const CREATED_AT = 'creation_date'; const UPDATED_AT = 'updated_date'; }

updated_at 타임스탬프를 변경하지 않고 모델을 조작하려면 withoutTimestamps 메서드에 클로저를 전달하면 됩니다.

Model::withoutTimestamps(fn () => $post->increment('reads'));

데이터베이스 연결

모든 Eloquent 모델은 기본적으로 애플리케이션에 설정된 기본 데이터베이스 연결을 사용합니다. 특정 모델에서 다른 연결을 사용하려면 $connection 속성을 지정하세요.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 이 모델이 사용할 데이터베이스 연결 * * @var string */ protected $connection = 'mysql'; }

속성 기본값

새로 인스턴스화한 모델은 기본적으로 아무 속성값도 가지지 않습니다. 일부 속성에 기본값을 지정하려면 $attributes 속성을 사용하세요. 여기에 지정하는 값은 데이터베이스에서 읽어온 것과 같은 "저장 가능한(raw)" 형태여야 합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 모델 속성의 기본값 * * @var array */ protected $attributes = [ 'options' => '[]', 'delayed' => false, ]; }

Eloquent 엄격 모드 설정

Laravel은 Eloquent의 동작 방식과 엄격성 수준을 상황에 맞게 조정할 수 있는 몇 가지 메서드를 제공합니다.

지연 로딩 방지 (preventLazyLoading)

preventLazyLoading 메서드는 지연 로딩을 차단할지 여부를 제어합니다. 개발 환경에서는 차단하고 운영 환경에서는 정상 동작하도록 설정하면, 실수로 지연 로딩이 남아 있어도 운영 서비스에 영향을 주지 않습니다. 보통 AppServiceProviderboot 메서드에 작성합니다.

use Illuminate\Database\Eloquent\Model; /** * 애플리케이션 서비스 초기화 */ public function boot(): void { Model::preventLazyLoading(! $this->app->isProduction()); }

fillable 미등록 속성 할당 예외 처리 (preventSilentlyDiscardingAttributes)

fillable 배열에 등록되지 않은 속성에 값을 할당하려 할 때, 기본적으로는 조용히 무시됩니다. preventSilentlyDiscardingAttributes를 설정하면 이 경우 예외가 발생하므로, 로컬 개발 중 실수를 빠르게 발견할 수 있습니다.

Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());

NOTE

위 두 설정 모두 운영 환경(isProduction())에서는 비활성화하는 패턴을 권장합니다. 개발 중에는 엄격하게 오류를 잡고, 운영에서는 예상치 못한 장애를 방지하는 균형 있는 접근 방식입니다.

모델 조회하기

모델과 연결된 데이터베이스 테이블을 생성했다면, 이제 데이터베이스에서 데이터를 조회할 준비가 된 것입니다. Eloquent 모델은 강력한 쿼리 빌더 역할을 하므로, 연결된 테이블을 유연하게 조회할 수 있습니다. all 메서드를 사용하면 테이블의 모든 레코드를 가져올 수 있습니다:

use App\Models\Flight; foreach (Flight::all() as $flight) { echo $flight->name; }

쿼리 조건 추가하기

all 메서드는 테이블의 모든 결과를 반환합니다. 하지만 Eloquent 모델은 쿼리 빌더이기도 하므로, 조건을 추가한 뒤 get 메서드로 결과를 가져올 수 있습니다:

$flights = Flight::where('active', 1) ->orderBy('name') ->take(10) ->get();

NOTE

Eloquent 모델은 쿼리 빌더이기도 합니다. Laravel 쿼리 빌더가 제공하는 모든 메서드를 Eloquent 쿼리에서도 그대로 사용할 수 있습니다.

모델 새로고침

데이터베이스에서 이미 조회한 Eloquent 모델 인스턴스를 최신 데이터로 갱신하려면 freshrefresh 메서드를 사용합니다.

fresh 메서드는 데이터베이스에서 모델을 다시 조회해 새로운 인스턴스를 반환합니다. 기존 인스턴스는 변경되지 않습니다:

$flight = Flight::where('number', 'KE 001')->first(); $freshFlight = $flight->fresh();

refresh 메서드는 데이터베이스의 최신 데이터로 기존 인스턴스를 갱신합니다. 로드된 관계도 함께 새로고침됩니다:

$flight = Flight::where('number', 'KE 001')->first(); $flight->number = 'OZ 123'; $flight->refresh(); $flight->number; // "KE 001"

컬렉션

all이나 get처럼 여러 레코드를 반환하는 Eloquent 메서드는 일반 PHP 배열이 아닌 Illuminate\Database\Eloquent\Collection 인스턴스를 반환합니다.

Eloquent Collection은 Laravel의 기본 Illuminate\Support\Collection 클래스를 확장하며, 데이터를 다루는 다양한 유용한 메서드를 제공합니다. 예를 들어, reject 메서드로 특정 조건에 맞는 모델을 컬렉션에서 제거할 수 있습니다:

$flights = Flight::where('destination', '인천')->get(); $flights = $flights->reject(function (Flight $flight) { return $flight->cancelled; });

기본 컬렉션 메서드 외에도, Eloquent 컬렉션은 Eloquent 모델 전용 추가 메서드도 제공합니다.

Laravel의 모든 컬렉션은 PHP의 이터러블 인터페이스를 구현하므로, 배열처럼 반복(loop)할 수 있습니다:

foreach ($flights as $flight) { echo $flight->name; }

결과 청크 처리

수만 건의 Eloquent 레코드를 all이나 get으로 한 번에 불러오면 메모리가 부족해질 수 있습니다. 이런 경우 chunk 메서드를 사용하면 대량의 모델을 효율적으로 처리할 수 있습니다.

chunk 메서드는 Eloquent 모델을 지정한 수만큼 나누어 가져오고, 각 묶음(chunk)을 클로저에 전달합니다. 한 번에 하나의 청크만 메모리에 올리므로 메모리 사용량을 크게 줄일 수 있습니다:

use App\Models\Flight; use Illuminate\Database\Eloquent\Collection; Flight::chunk(200, function (Collection $flights) { foreach ($flights as $flight) { // ... } });

첫 번째 인수는 청크당 가져올 레코드 수이고, 두 번째 인수는 각 청크마다 호출될 클로저입니다.

반복하는 도중 청크 기준이 되는 컬럼을 직접 업데이트하는 경우에는 chunk 대신 chunkById를 사용해야 합니다. chunk를 사용하면 결과가 예상과 다르게 나올 수 있습니다. chunkById는 내부적으로 이전 청크의 마지막 id보다 큰 레코드를 순서대로 가져옵니다:

Flight::where('departed', true) ->chunkById(200, function (Collection $flights) { $flights->each->update(['departed' => false]); }, column: 'id');

chunkByIdlazyById는 내부적으로 자체 where 조건을 쿼리에 추가합니다. 따라서 직접 작성한 조건은 클로저로 논리적으로 묶어 주는 것이 좋습니다:

Flight::where(function ($query) { $query->where('delayed', true)->orWhere('cancelled', true); })->chunkById(200, function (Collection $flights) { $flights->each->update([ 'departed' => false, 'cancelled' => true ]); }, column: 'id');

지연 컬렉션으로 청크 처리하기

lazy 메서드는 내부적으로 청크 방식으로 쿼리를 실행하지만, 각 청크를 콜백에 전달하는 대신 하나의 평탄화된 LazyCollection 스트림으로 반환합니다. 덕분에 대량의 데이터를 마치 단일 컬렉션처럼 다룰 수 있습니다:

use App\Models\Flight; foreach (Flight::lazy() as $flight) { // ... }

반복 중에 기준 컬럼을 업데이트하는 경우에는 lazyById를 사용해야 합니다. lazyById는 내부적으로 이전 청크의 마지막 id보다 큰 레코드를 순서대로 가져옵니다:

Flight::where('departed', true) ->lazyById(200, column: 'id') ->each->update(['departed' => false]);

id의 내림차순 기준으로 결과를 필터링하려면 lazyByIdDesc 메서드를 사용하면 됩니다.

커서

cursor 메서드도 수만 건의 레코드를 반복할 때 메모리 소비를 크게 줄여줍니다.

cursor는 데이터베이스 쿼리를 단 한 번만 실행하지만, 각 Eloquent 모델은 실제로 반복될 때에만 생성(hydrate)됩니다. 따라서 반복 중 언제나 메모리에는 하나의 Eloquent 모델만 존재합니다.

WARNING

cursor 메서드는 한 번에 하나의 Eloquent 모델만 메모리에 유지하므로, 관계를 Eager 로딩할 수 없습니다. 관계를 함께 로드해야 한다면 lazy 메서드를 사용하세요.

내부적으로 cursor는 PHP 제너레이터(generator)를 활용합니다:

use App\Models\Flight; foreach (Flight::where('destination', '제주')->cursor() as $flight) { // ... }

cursorIlluminate\Support\LazyCollection 인스턴스를 반환합니다. 지연 컬렉션을 사용하면 일반 컬렉션의 다양한 메서드를 활용하면서도 메모리에는 모델 하나만 유지할 수 있습니다:

use App\Models\User; $users = User::cursor()->filter(function (User $user) { return $user->id > 500; }); foreach ($users as $user) { echo $user->id; }

cursor는 일반 쿼리보다 훨씬 적은 메모리를 사용하지만, 데이터가 매우 많으면 결국 메모리가 부족해질 수 있습니다. 이는 PHP의 PDO 드라이버가 내부적으로 모든 로우 쿼리 결과를 버퍼에 캐시하는 방식 때문입니다. 매우 대량의 레코드를 다뤄야 한다면 lazy 메서드를 고려하세요.

고급 서브쿼리

서브쿼리 SELECT

Eloquent는 연관 테이블의 정보를 단일 쿼리로 가져올 수 있는 고급 서브쿼리 기능을 지원합니다. 예를 들어, destinations(목적지) 테이블과 flights(항공편) 테이블이 있고, flights 테이블에는 목적지 도착 시각을 나타내는 arrived_at 컬럼이 있다고 가정해 보겠습니다.

쿼리 빌더의 selectaddSelect 메서드에서 서브쿼리 기능을 활용하면, 모든 목적지와 각 목적지에 가장 최근 도착한 항공편 이름을 단 한 번의 쿼리로 조회할 수 있습니다:

use App\Models\Destination; use App\Models\Flight; return Destination::addSelect(['last_flight' => Flight::select('name') ->whereColumn('destination_id', 'destinations.id') ->orderByDesc('arrived_at') ->limit(1) ])->get();

서브쿼리 정렬

쿼리 빌더의 orderBy도 서브쿼리를 지원합니다. 위의 예시를 이어서, 각 목적지에 마지막으로 도착한 항공편의 도착 시각을 기준으로 목적지를 정렬할 수 있습니다. 이 역시 단 한 번의 데이터베이스 쿼리로 처리됩니다:

return Destination::orderByDesc( Flight::select('arrived_at') ->whereColumn('destination_id', 'destinations.id') ->orderByDesc('arrived_at') ->limit(1) )->get();

단일 모델 / 집계 조회

쿼리 조건에 맞는 모든 레코드를 가져오는 것 외에, find, first, firstWhere 메서드를 사용해 단일 레코드를 조회할 수 있습니다. 이 메서드들은 컬렉션 대신 모델 인스턴스 하나를 반환합니다.

use App\Models\Flight; // 기본 키로 모델 조회 $flight = Flight::find(1); // 조건에 맞는 첫 번째 모델 조회 $flight = Flight::where('active', 1)->first(); // firstWhere를 사용한 동일한 조회 $flight = Flight::firstWhere('active', 1);

결과가 없을 때 특정 동작을 수행하고 싶다면 findOrfirstOr 메서드를 사용할 수 있습니다. 결과가 존재하면 해당 모델을 반환하고, 없으면 전달한 클로저를 실행해 그 반환값을 결과로 사용합니다.

$flight = Flight::findOr(1, function () { // 모델을 찾지 못했을 때 실행 }); $flight = Flight::where('legs', '>', 3)->firstOr(function () { // 조건에 맞는 모델이 없을 때 실행 });

모델 미조회 시 예외 발생

라우트나 컨트롤러에서 모델을 찾지 못했을 때 자동으로 예외를 발생시키고 싶다면 findOrFailfirstOrFail 메서드를 사용하세요. 조건에 맞는 결과가 없으면 Illuminate\Database\Eloquent\ModelNotFoundException이 발생합니다.

$flight = Flight::findOrFail(1); $flight = Flight::where('legs', '>', 3)->firstOrFail();

ModelNotFoundException을 별도로 처리하지 않으면 Laravel이 자동으로 클라이언트에게 404 HTTP 응답을 반환합니다.

use App\Models\Flight; Route::get('/api/flights/{id}', function (string $id) { return Flight::findOrFail($id); });

NOTE

API 라우트에서 단일 리소스를 조회할 때는 find 대신 findOrFail을 사용하는 것이 좋습니다. 모델이 없을 경우 별도의 분기 없이 자동으로 404를 응답하므로 코드가 간결해집니다.

조회 또는 생성

firstOrCreate 메서드는 주어진 컬럼/값 쌍으로 데이터베이스 레코드를 찾습니다. 찾지 못하면 첫 번째 배열 인수와 두 번째 배열 인수(선택 사항)를 합쳐 새 레코드를 데이터베이스에 삽입합니다.

firstOrNew 메서드도 동일하게 동작하지만, 모델을 찾지 못했을 때 데이터베이스에 저장하지 않고 새 모델 인스턴스만 반환합니다. 실제로 저장하려면 save 메서드를 직접 호출해야 합니다.

use App\Models\Flight; // 이름으로 항공편 조회, 없으면 생성 $flight = Flight::firstOrCreate([ 'name' => '서울 to 도쿄' ]); // 이름으로 조회, 없으면 추가 속성을 포함해 생성 $flight = Flight::firstOrCreate( ['name' => '서울 to 도쿄'], ['delayed' => 1, 'arrival_time' => '11:30'] ); // 이름으로 조회, 없으면 저장되지 않은 새 인스턴스 반환 $flight = Flight::firstOrNew([ 'name' => '서울 to 도쿄' ]); // 이름으로 조회, 없으면 추가 속성을 포함한 새 인스턴스 반환 $flight = Flight::firstOrNew( ['name' => '부산 to 오사카'], ['delayed' => 1, 'arrival_time' => '11:30'] );
메서드없을 때 동작DB 저장 여부
firstOrCreate새 레코드 삽입 후 반환✅ 자동 저장
firstOrNew새 인스턴스만 반환❌ 수동으로 save() 필요

집계 함수 사용

Eloquent 모델에서도 Laravel 쿼리 빌더가 제공하는 count, sum, max 등의 집계 메서드를 그대로 사용할 수 있습니다. 이 메서드들은 모델 인스턴스가 아닌 스칼라 값을 반환합니다.

$count = Flight::where('active', 1)->count(); $max = Flight::where('active', 1)->max('price');

모델 삽입 및 업데이트

삽입

Eloquent는 데이터베이스에서 모델을 조회하는 것만큼이나 새 레코드를 삽입하는 것도 간단하게 처리합니다. 새 레코드를 삽입하려면 모델 인스턴스를 새로 만들고 속성을 설정한 뒤, save 메서드를 호출하면 됩니다.

<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Models\Flight; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class FlightController extends Controller { /** * 새 항공편을 데이터베이스에 저장합니다. */ public function store(Request $request): RedirectResponse { // 요청 유효성 검사... $flight = new Flight; $flight->name = $request->name; $flight->save(); return redirect('/flights'); } }

이 예제에서는 HTTP 요청의 name 필드를 App\Models\Flight 모델 인스턴스의 name 속성에 할당합니다. save 메서드를 호출하면 데이터베이스에 레코드가 삽입됩니다. created_atupdated_at 타임스탬프는 save 호출 시 자동으로 설정되므로 별도로 지정할 필요가 없습니다.

단일 PHP 구문으로 새 모델을 저장하고 싶다면 create 메서드를 사용할 수 있습니다. create 메서드는 새로 삽입된 모델 인스턴스를 반환합니다.

use App\Models\Flight; $flight = Flight::create([ 'name' => '인천 to 도쿄', ]);

다만 create 메서드를 사용하기 전에 모델 클래스에 $fillable 또는 $guarded 속성을 반드시 정의해야 합니다. Eloquent 모델은 기본적으로 대량 할당(mass assignment) 취약점으로부터 보호되기 때문입니다. 자세한 내용은 아래의 대량 할당 문서를 참고하세요.

업데이트

save 메서드는 이미 존재하는 모델을 업데이트할 때도 사용합니다. 업데이트하려면 모델을 조회한 뒤 변경할 속성을 설정하고 save를 호출하면 됩니다. updated_at 타임스탬프는 자동으로 갱신됩니다.

use App\Models\Flight; $flight = Flight::find(1); $flight->name = '도쿄 to 인천'; $flight->save();

updateOrCreate

기존 레코드가 있으면 업데이트하고, 없으면 새로 생성해야 하는 경우에는 updateOrCreate 메서드를 사용합니다. firstOrCreate와 마찬가지로 모델을 자동으로 저장하므로 save를 별도로 호출할 필요가 없습니다.

아래 예제에서는 departure'Oakland'이고 destination'San Diego'인 항공편이 존재하면 pricediscounted 컬럼을 업데이트합니다. 해당 항공편이 없으면 첫 번째 인자와 두 번째 인자를 합친 속성으로 새 항공편을 생성합니다.

$flight = Flight::updateOrCreate( ['departure' => 'Oakland', 'destination' => 'San Diego'], ['price' => 99, 'discounted' => 1] );

대량 업데이트

쿼리 조건에 맞는 여러 모델을 한 번에 업데이트할 수도 있습니다. 아래 예제는 active 상태이며 목적지가 'San Diego'인 모든 항공편을 지연(delayed) 상태로 변경합니다.

Flight::where('active', 1) ->where('destination', 'San Diego') ->update(['delayed' => 1]);

update 메서드는 업데이트할 컬럼과 값의 쌍을 배열로 받으며, 영향을 받은 행의 수를 반환합니다.

WARNING

Eloquent를 통해 대량 업데이트를 실행하면 업데이트된 모델에 대한 saving, saved, updating, updated 모델 이벤트가 발생하지 않습니다. 대량 업데이트 시 모델 인스턴스를 실제로 조회하지 않기 때문입니다.

속성 변경 사항 확인

Eloquent는 모델의 내부 상태를 파악하고, 처음 조회된 이후 속성이 어떻게 변경되었는지 확인할 수 있는 isDirty, isClean, wasChanged 메서드를 제공합니다.

  • isDirty: 조회 이후 속성이 변경되었는지 확인합니다. 특정 속성명이나 속성명 배열을 인자로 전달할 수 있습니다.
  • isClean: 조회 이후 속성이 변경되지 않았는지 확인합니다. 마찬가지로 특정 속성명을 전달할 수 있습니다.
use App\Models\User; $user = User::create([ 'first_name' => 'Taylor', 'last_name' => 'Otwell', 'title' => 'Developer', ]); $user->title = 'Painter'; $user->isDirty(); // true $user->isDirty('title'); // true $user->isDirty('first_name'); // false $user->isDirty(['first_name', 'title']); // true $user->isClean(); // false $user->isClean('title'); // false $user->isClean('first_name'); // true $user->isClean(['first_name', 'title']); // false $user->save(); $user->isDirty(); // false $user->isClean(); // true

wasChanged 메서드는 현재 요청 사이클 내에서 모델이 마지막으로 저장될 때 속성이 변경되었는지 확인합니다. 특정 속성명을 전달해 해당 속성의 변경 여부만 확인할 수도 있습니다.

$user = User::create([ 'first_name' => 'Taylor', 'last_name' => 'Otwell', 'title' => 'Developer', ]); $user->title = 'Painter'; $user->save(); $user->wasChanged(); // true $user->wasChanged('title'); // true $user->wasChanged(['title', 'slug']); // true $user->wasChanged('first_name'); // false $user->wasChanged(['first_name', 'title']); // true

getOriginal 메서드는 모델을 조회한 이후 변경이 있더라도, 원래 속성값을 배열로 반환합니다. 특정 속성명을 전달하면 해당 속성의 원래 값만 가져올 수 있습니다.

$user = User::find(1); $user->name; // John $user->email; // john@example.com $user->name = "Jack"; $user->name; // Jack $user->getOriginal('name'); // John $user->getOriginal(); // 원래 속성 전체 배열...

대량 할당 (Mass Assignment)

create 메서드를 사용하면 단일 구문으로 새 모델을 저장할 수 있으며, 새로 생성된 모델 인스턴스가 반환됩니다.

use App\Models\Flight; $flight = Flight::create([ 'name' => '인천 to 도쿄', ]);

그러나 create 메서드를 사용하기 전에 모델에 $fillable 또는 $guarded 속성을 정의해야 합니다. Eloquent 모델은 기본적으로 대량 할당 취약점으로부터 보호됩니다.

NOTE

대량 할당 취약점이란? 예를 들어 악의적인 사용자가 HTTP 요청에 is_admin=1 파라미터를 몰래 포함시켜 전송하면, 이 값이 create 메서드로 그대로 전달되어 관리자 권한을 획득할 수 있습니다. 이를 방지하기 위해 어떤 필드를 대량 할당 가능하게 할지 명시적으로 선언해야 합니다.

$fillable로 허용 필드 지정

대량 할당을 허용할 속성은 모델의 $fillable 속성에 명시합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 대량 할당 가능한 속성 목록. * * @var array<int, string> */ protected $fillable = ['name']; }

허용할 속성을 지정하면 create 메서드로 새 레코드를 삽입할 수 있습니다.

$flight = Flight::create(['name' => '인천 to 도쿄']);

이미 모델 인스턴스가 있다면 fill 메서드로 속성을 일괄 설정할 수 있습니다.

$flight->fill(['name' => '김포 to 제주']);

대량 할당과 JSON 컬럼

JSON 컬럼을 대량 할당할 때는 각 컬럼의 키를 $fillable 배열에 명시해야 합니다. 보안상의 이유로 Laravel은 $guarded 속성을 사용할 때 중첩된 JSON 속성 업데이트를 지원하지 않습니다.

/** * 대량 할당 가능한 속성 목록. * * @var array<int, string> */ protected $fillable = [ 'options->enabled', ];

모든 속성 대량 할당 허용

모든 속성을 대량 할당 가능하게 하려면 $guarded를 빈 배열로 정의하면 됩니다. 단, 이 경우에는 fill, create, update 등에 전달하는 배열을 항상 직접 신중하게 구성해야 합니다.

/** * 대량 할당을 막을 속성 목록. * * @var array<string>|bool */ protected $guarded = [];

대량 할당 예외 처리

기본적으로 $fillable에 포함되지 않은 속성은 대량 할당 시 조용히 무시됩니다. 운영 환경에서는 의도된 동작이지만, 로컬 개발 중에는 모델 변경이 적용되지 않아 디버깅이 어려울 수 있습니다.

preventSilentlyDiscardingAttributes 메서드를 사용하면 허용되지 않은 속성을 채우려 할 때 예외를 던지도록 설정할 수 있습니다. 이 메서드는 보통 AppServiceProviderboot 메서드에서 호출합니다.

use Illuminate\Database\Eloquent\Model; /** * 애플리케이션 서비스를 부트스트랩합니다. */ public function boot(): void { Model::preventSilentlyDiscardingAttributes($this->app->isLocal()); }

Upserts

upsert 메서드를 사용하면 단일 원자적 연산으로 레코드를 업데이트하거나 삽입할 수 있습니다.

  • 첫 번째 인자: 삽입하거나 업데이트할 값의 배열
  • 두 번째 인자(uniqueBy): 테이블 내에서 레코드를 고유하게 식별하는 컬럼(들)
  • 세 번째 인자(update): 일치하는 레코드가 있을 때 업데이트할 컬럼 목록

모델에 타임스탬프가 활성화되어 있으면 created_atupdated_at이 자동으로 설정됩니다.

Flight::upsert([ ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99], ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150] ], uniqueBy: ['departure', 'destination'], update: ['price']);

WARNING

SQL Server를 제외한 모든 데이터베이스에서는 upsert의 두 번째 인자로 지정된 컬럼에 "primary" 또는 "unique" 인덱스가 반드시 있어야 합니다. 또한 MariaDB와 MySQL 드라이버는 두 번째 인자를 무시하고, 테이블의 "primary" 및 "unique" 인덱스를 기준으로 기존 레코드를 판별합니다.

Eloquent: 시작하기

모델 삭제

모델 인스턴스에서 delete 메서드를 호출하면 해당 레코드를 삭제할 수 있습니다.

use App\Models\Flight; $flight = Flight::find(1); $flight->delete();

기본 키로 모델 삭제하기

위 예제에서는 먼저 모델을 조회한 뒤 delete를 호출했습니다. 하지만 기본 키를 이미 알고 있다면, destroy 메서드를 사용해 조회 없이 바로 삭제할 수 있습니다. destroy는 단일 기본 키뿐만 아니라 여러 기본 키, 배열, 또는 컬렉션도 받을 수 있습니다.

Flight::destroy(1); Flight::destroy(1, 2, 3); Flight::destroy([1, 2, 3]); Flight::destroy(collect([1, 2, 3]));

소프트 삭제 모델을 사용하는 경우, forceDestroy 메서드로 영구 삭제할 수 있습니다.

Flight::forceDestroy(1);

WARNING

destroy 메서드는 각 모델을 개별적으로 조회한 뒤 delete를 호출합니다. 따라서 각 모델에 대해 deletingdeleted 이벤트가 올바르게 발행됩니다.

쿼리로 모델 일괄 삭제하기

Eloquent 쿼리에 조건을 붙여 조건에 맞는 모델을 한꺼번에 삭제할 수도 있습니다. 아래 예제는 비활성 상태(active = 0)인 항공편을 모두 삭제합니다. 일괄 삭제는 일괄 업데이트와 마찬가지로 각 모델에 대한 이벤트를 발행하지 않는다는 점에 주의하세요.

$deleted = Flight::where('active', 0)->delete();

테이블의 모든 모델을 삭제하려면 조건 없이 쿼리를 실행합니다.

$deleted = Flight::query()->delete();

WARNING

Eloquent를 통해 일괄 삭제를 실행하면, 삭제 대상 모델에 대해 deletingdeleted 이벤트가 발행되지 않습니다. 일괄 삭제 시에는 실제로 모델 인스턴스를 조회하지 않기 때문입니다.

소프트 삭제 (Soft Delete)

소프트 삭제는 데이터베이스에서 레코드를 실제로 지우지 않고, 삭제된 것처럼 처리하는 방식입니다. 소프트 삭제가 적용된 모델은 deleted_at 컬럼에 삭제 시각이 기록될 뿐, 테이블에서 사라지지 않습니다. 소프트 삭제를 활성화하려면 모델에 Illuminate\Database\Eloquent\SoftDeletes 트레이트를 추가합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Flight extends Model { use SoftDeletes; }

NOTE

SoftDeletes 트레이트는 deleted_at 속성을 자동으로 DateTime / Carbon 인스턴스로 캐스팅합니다.

데이터베이스 테이블에도 deleted_at 컬럼을 추가해야 합니다. Laravel 스키마 빌더는 이를 위한 헬퍼 메서드를 제공합니다.

use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; // deleted_at 컬럼 추가 Schema::table('flights', function (Blueprint $table) { $table->softDeletes(); }); // deleted_at 컬럼 제거 Schema::table('flights', function (Blueprint $table) { $table->dropSoftDeletes(); });

이제 모델에서 delete를 호출하면 실제 레코드는 유지되면서 deleted_at 컬럼에 현재 시각이 기록됩니다. 소프트 삭제 모델을 쿼리할 때, 소프트 삭제된 레코드는 기본적으로 결과에서 자동으로 제외됩니다.

특정 모델 인스턴스가 소프트 삭제 상태인지 확인하려면 trashed 메서드를 사용합니다.

if ($flight->trashed()) { // 소프트 삭제된 상태 }

소프트 삭제 모델 복원하기

소프트 삭제된 모델을 다시 되살리려면 restore 메서드를 호출합니다. 이 메서드는 deleted_at 컬럼을 null로 되돌립니다.

$flight->restore();

쿼리에서 restore를 사용하면 여러 모델을 한 번에 복원할 수도 있습니다. 다른 일괄 작업과 마찬가지로 이 경우에도 모델 이벤트는 발행되지 않습니다.

Flight::withTrashed() ->where('airline_id', 1) ->restore();

관계 쿼리에서도 restore를 사용할 수 있습니다.

$flight->history()->restore();

영구 삭제하기

소프트 삭제된 모델을 데이터베이스에서 완전히 제거해야 할 때는 forceDelete 메서드를 사용합니다.

$flight->forceDelete();

관계 쿼리에서도 동일하게 사용할 수 있습니다.

$flight->history()->forceDelete();

소프트 삭제 모델 조회하기

소프트 삭제 모델 포함하여 조회하기

앞서 설명했듯이, 소프트 삭제된 모델은 기본적으로 쿼리 결과에서 제외됩니다. 소프트 삭제된 모델까지 포함해서 조회하려면 withTrashed 메서드를 사용합니다.

use App\Models\Flight; $flights = Flight::withTrashed() ->where('account_id', 1) ->get();

관계 쿼리에서도 사용할 수 있습니다.

$flight->history()->withTrashed()->get();

소프트 삭제 모델만 조회하기

onlyTrashed 메서드를 사용하면 소프트 삭제된 모델 조회할 수 있습니다.

$flights = Flight::onlyTrashed() ->where('airline_id', 1) ->get();

모델 정리(Pruning)

더 이상 필요하지 않은 모델을 주기적으로 삭제하고 싶을 때는 Illuminate\Database\Eloquent\Prunable 또는 Illuminate\Database\Eloquent\MassPrunable 트레이트를 사용합니다. 해당 트레이트를 모델에 추가한 뒤, 삭제 대상 레코드를 반환하는 prunable 메서드를 구현하면 됩니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Prunable; class Flight extends Model { use Prunable; /** * 정리 대상 모델 쿼리를 반환합니다. */ public function prunable(): Builder { // 한 달 이전에 생성된 레코드를 정리 대상으로 지정 return static::where('created_at', '<=', now()->subMonth()); } }

Prunable 트레이트를 사용할 때는 모델에 pruning 메서드를 추가로 정의할 수도 있습니다. 이 메서드는 모델이 실제로 삭제되기 직전에 호출되므로, DB에서 레코드가 지워지기 전에 관련 파일이나 외부 리소스를 함께 정리하는 용도로 활용할 수 있습니다.

/** * 모델 정리 전 처리 로직 */ protected function pruning(): void { // 예: 연결된 파일 삭제 등 추가 정리 작업 }

모델 설정이 끝났다면, routes/console.php 파일에서 model:prune Artisan 명령을 스케줄에 등록합니다. 실행 주기는 프로젝트 상황에 맞게 자유롭게 지정할 수 있습니다.

use Illuminate\Support\Facades\Schedule; Schedule::command('model:prune')->daily();

model:prune 명령은 내부적으로 app/Models 디렉터리에서 Prunable 트레이트가 적용된 모델을 자동으로 탐색합니다. 모델 파일이 다른 위치에 있다면 --model 옵션으로 클래스를 명시하면 됩니다.

Schedule::command('model:prune', [ '--model' => [Address::class, Flight::class], ])->daily();

반대로 특정 모델만 정리 대상에서 제외하려면 --except 옵션을 사용합니다.

Schedule::command('model:prune', [ '--except' => [Address::class, Flight::class], ])->daily();

실제로 삭제하기 전에 prunable 쿼리가 몇 건을 대상으로 하는지 확인하고 싶다면 --pretend 옵션을 사용하세요. 쿼리는 실행되지 않고 삭제 예정 건수만 출력됩니다.

php artisan model:prune --pretend

WARNING

소프트 삭제(Soft Delete)가 적용된 모델이라도 prunable 쿼리 조건에 해당하면 forceDelete영구 삭제됩니다.

대량 정리 (Mass Pruning)

MassPrunable 트레이트를 사용하면 대량 삭제 쿼리(DELETE ... WHERE ...)로 레코드를 한 번에 제거합니다. 모델 인스턴스를 하나씩 조회하지 않기 때문에 pruning 메서드가 호출되지 않으며, deleting / deleted 모델 이벤트도 발생하지 않습니다. 그 대신 훨씬 빠르게 처리되므로, 삭제 전 추가 작업이 필요 없는 경우에 적합합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\MassPrunable; class Flight extends Model { use MassPrunable; /** * 정리 대상 모델 쿼리를 반환합니다. */ public function prunable(): Builder { return static::where('created_at', '<=', now()->subMonth()); } }

NOTE

PrunableMassPrunable의 선택 기준: 삭제 전 파일 제거, 외부 API 호출 등 부가 작업이 필요하다면 Prunable을, 단순히 DB 레코드만 빠르게 지우면 된다면 MassPrunable을 사용하세요.

모델 복제

기존 모델 인스턴스의 저장되지 않은 복사본을 만들려면 replicate 메서드를 사용합니다. 이 메서드는 여러 속성이 동일한 모델 인스턴스를 만들 때 특히 유용합니다.

use App\Models\Address; $shipping = Address::create([ 'type' => 'shipping', 'line_1' => '서울특별시 강남구 테헤란로 123', 'city' => '서울', 'state' => '강남구', 'postcode' => '06234', ]); $billing = $shipping->replicate()->fill([ 'type' => 'billing' ]); $billing->save();

복제 시 특정 속성을 제외하려면, 제외할 속성명의 배열을 replicate 메서드에 전달하면 됩니다.

$flight = Flight::create([ 'destination' => 'ICN', 'origin' => 'LHR', 'last_flown' => '2020-03-04 11:00:00', 'last_pilot_id' => 747, ]); $flight = $flight->replicate([ 'last_flown', 'last_pilot_id' ]);

쿼리 스코프

글로벌 스코프

글로벌 스코프를 사용하면 특정 모델에 대한 모든 쿼리에 자동으로 조건을 추가할 수 있습니다. Laravel의 소프트 삭제 기능도 내부적으로 글로벌 스코프를 활용해 삭제되지 않은 레코드만 조회합니다. 직접 글로벌 스코프를 작성하면, 특정 모델의 모든 쿼리에 일관된 제약 조건을 편리하게 적용할 수 있습니다.

스코프 생성

make:scope Artisan 명령어로 새 글로벌 스코프 클래스를 생성할 수 있습니다. 생성된 파일은 app/Models/Scopes 디렉터리에 위치합니다.

php artisan make:scope AncientScope

글로벌 스코프 작성

글로벌 스코프 클래스는 Illuminate\Database\Eloquent\Scope 인터페이스를 구현해야 하며, apply 메서드 하나만 정의하면 됩니다. apply 메서드 안에서 where 조건이나 다른 쿼리 절을 자유롭게 추가할 수 있습니다.

<?php namespace App\Models\Scopes; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; class AncientScope implements Scope { /** * 주어진 Eloquent 쿼리 빌더에 스코프를 적용합니다. */ public function apply(Builder $builder, Model $model): void { $builder->where('created_at', '<', now()->subYears(2000)); } }

NOTE

글로벌 스코프에서 select 절에 컬럼을 추가해야 한다면, select 대신 addSelect 메서드를 사용하세요. select를 사용하면 기존 select 절이 덮어써질 수 있습니다.

글로벌 스코프 적용

모델에 글로벌 스코프를 적용하는 가장 간단한 방법은 ScopedBy 어트리뷰트를 사용하는 것입니다.

<?php namespace App\Models; use App\Models\Scopes\AncientScope; use Illuminate\Database\Eloquent\Attributes\ScopedBy; #[ScopedBy([AncientScope::class])] class User extends Model { // }

또는 모델의 booted 메서드를 오버라이드하여 addGlobalScope를 직접 호출할 수도 있습니다.

<?php namespace App\Models; use App\Models\Scopes\AncientScope; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * 모델의 "booted" 메서드. */ protected static function booted(): void { static::addGlobalScope(new AncientScope); } }

위 예시처럼 스코프를 등록하면, User::all()을 호출할 때 다음과 같은 SQL이 실행됩니다.

select * from `users` where `created_at` < 0021-02-18 00:00:00

익명 글로벌 스코프 (클로저 방식)

별도의 클래스를 만들 필요 없이, 간단한 스코프는 클로저로도 정의할 수 있습니다. 이 경우 addGlobalScope의 첫 번째 인자로 스코프의 이름을 문자열로 지정해야 합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * 모델의 "booted" 메서드. */ protected static function booted(): void { static::addGlobalScope('ancient', function (Builder $builder) { $builder->where('created_at', '<', now()->subYears(2000)); }); } }

글로벌 스코프 제거

특정 쿼리에서 글로벌 스코프를 제외하려면 withoutGlobalScope 메서드를 사용합니다. 클래스 방식으로 정의한 스코프라면 클래스명을, 클로저 방식이라면 지정한 이름 문자열을 전달합니다.

// 클래스 방식으로 정의한 스코프 제거 User::withoutGlobalScope(AncientScope::class)->get(); // 클로저 방식으로 정의한 스코프 제거 User::withoutGlobalScope('ancient')->get();

여러 개 또는 모든 글로벌 스코프를 한 번에 제거하려면 withoutGlobalScopes를 사용하세요.

// 모든 글로벌 스코프 제거 User::withoutGlobalScopes()->get(); // 특정 글로벌 스코프만 제거 User::withoutGlobalScopes([ FirstScope::class, SecondScope::class ])->get();

로컬 스코프

로컬 스코프는 자주 사용하는 쿼리 조건을 모델 메서드로 정의해 두고 재사용할 수 있도록 해줍니다. 예를 들어 "인기 있는 사용자"를 자주 조회해야 한다면, 그 조건을 스코프로 만들어 두면 편리합니다.

로컬 스코프는 모델 메서드 이름 앞에 scope 접두사를 붙여 정의합니다. 메서드는 쿼리 빌더 인스턴스를 반환하거나 void여야 합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * 인기 있는 사용자만 조회하는 스코프. */ public function scopePopular(Builder $query): void { $query->where('votes', '>', 100); } /** * 활성 사용자만 조회하는 스코프. */ public function scopeActive(Builder $query): void { $query->where('active', 1); } }

로컬 스코프 사용

스코프를 호출할 때는 scope 접두사를 제외하고 메서드명만 사용합니다. 여러 스코프를 체이닝해서 조합할 수도 있습니다.

use App\Models\User; $users = User::popular()->active()->orderBy('created_at')->get();

or 조건으로 여러 스코프를 연결할 때는 논리적 그룹핑을 위해 클로저를 사용해야 할 수 있습니다.

$users = User::popular()->orWhere(function (Builder $query) { $query->active(); })->get();

다소 번거롭게 느껴질 수 있는데, Laravel은 클로저 없이도 스코프를 orWhere로 체이닝할 수 있는 "고차(higher-order)" orWhere 방식을 제공합니다.

$users = User::popular()->orWhere->active()->get();

동적 스코프

파라미터를 받아 동적으로 동작하는 스코프도 정의할 수 있습니다. $query 파라미터 뒤에 원하는 파라미터를 추가하면 됩니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * 특정 타입의 사용자만 조회하는 스코프. */ public function scopeOfType(Builder $query, string $type): void { $query->where('type', $type); } }

스코프를 호출할 때 인자를 함께 전달합니다.

$users = User::ofType('admin')->get();

대기 어트리뷰트 (Pending Attributes)

스코프를 통해 모델을 생성할 때, 쿼리 조건에 사용된 어트리뷰트를 생성된 모델에도 자동으로 반영하고 싶다면 withAttributes 메서드를 활용하세요.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class Post extends Model { /** * 임시 저장(draft) 게시글만 조회하는 스코프. */ public function scopeDraft(Builder $query): void { $query->withAttributes([ 'hidden' => true, ]); } }

withAttributes는 쿼리에 where 조건을 추가하는 동시에, 해당 스코프를 통해 생성된 모델에도 지정한 어트리뷰트 값을 설정합니다.

$draft = Post::draft()->create(['title' => '작성 중인 게시글']); $draft->hidden; // true

모델 비교

두 모델 인스턴스가 "같은" 모델인지 확인해야 할 때 isisNot 메서드를 사용할 수 있습니다. 이 메서드들은 두 모델의 기본 키(primary key), 테이블, 데이터베이스 커넥션이 모두 동일한지 비교합니다:

if ($post->is($anotherPost)) { // ... } if ($post->isNot($anotherPost)) { // ... }

isisNot 메서드는 belongsTo, hasOne, morphTo, morphOne 관계에서도 사용할 수 있습니다. 연관 모델을 비교할 때 굳이 추가 쿼리를 실행하지 않아도 된다는 점에서 특히 유용합니다:

if ($post->author()->is($user)) { // ... }

이벤트

NOTE

Eloquent 이벤트를 클라이언트 애플리케이션으로 직접 브로드캐스트하고 싶다면, Laravel의 모델 이벤트 브로드캐스팅을 참고하세요.

Eloquent 모델은 생명주기의 여러 시점에서 자동으로 이벤트를 발생시킵니다. 지원되는 이벤트 목록은 다음과 같습니다:

retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, trashed, forceDeleting, forceDeleted, restoring, restored, replicating

각 이벤트가 발생하는 시점은 다음과 같습니다:

  • retrieved — 데이터베이스에서 기존 모델을 조회했을 때
  • creating / created — 새 모델을 처음 저장할 때
  • updating / updated — 기존 모델을 수정한 뒤 save를 호출할 때
  • saving / saved — 생성 또는 수정 여부와 관계없이 save가 호출될 때 (속성이 변경되지 않았더라도 발생)

이름이 -ing으로 끝나는 이벤트는 변경 사항이 데이터베이스에 반영되기 전에 발생하고, -ed로 끝나는 이벤트는 반영된 후에 발생합니다.

모델 이벤트를 수신하려면 Eloquent 모델에 $dispatchesEvents 프로퍼티를 정의하세요. 이 프로퍼티는 생명주기의 각 시점을 여러분이 만든 이벤트 클래스에 매핑합니다. 각 이벤트 클래스는 생성자에서 해당 모델 인스턴스를 받아야 합니다:

<?php namespace App\Models; use App\Events\UserDeleted; use App\Events\UserSaved; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; class User extends Authenticatable { use Notifiable; /** * 모델 이벤트 매핑 * * @var array<string, string> */ protected $dispatchesEvents = [ 'saved' => UserSaved::class, 'deleted' => UserDeleted::class, ]; }

이벤트를 등록한 뒤에는 이벤트 리스너를 정의해 이벤트를 처리할 수 있습니다.

WARNING

Eloquent를 통해 대량 업데이트(mass update)나 대량 삭제(mass delete) 쿼리를 실행하면, 영향을 받는 모델에 대해 saved, updated, deleting, deleted 이벤트가 발생하지 않습니다. 대량 처리 시에는 모델을 실제로 조회하지 않기 때문입니다.

클로저로 이벤트 처리하기

별도의 이벤트 클래스를 만들지 않고 클로저를 직접 등록할 수도 있습니다. 일반적으로 모델의 booted 메서드 안에서 등록합니다:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * 모델 부팅 시 실행되는 메서드 */ protected static function booted(): void { static::created(function (User $user) { // 사용자가 생성된 직후 실행 }); } }

필요하다면 큐를 사용하는 익명 이벤트 리스너로 등록해 백그라운드에서 처리할 수도 있습니다:

use function Illuminate\Events\queueable; static::created(queueable(function (User $user) { // 큐를 통해 백그라운드에서 실행 }));

옵저버

옵저버 정의하기

특정 모델에서 여러 이벤트를 처리해야 할 때는 옵저버(Observer)를 사용하면 관련 리스너를 하나의 클래스로 깔끔하게 정리할 수 있습니다. 옵저버 클래스의 메서드 이름은 수신하려는 Eloquent 이벤트 이름과 동일하게 정의하며, 각 메서드는 영향을 받은 모델 인스턴스를 인자로 받습니다.

make:observer Artisan 명령어로 옵저버 클래스를 생성할 수 있습니다:

php artisan make:observer UserObserver --model=User

생성된 파일은 app/Observers 디렉터리에 위치합니다. 디렉터리가 없으면 Artisan이 자동으로 생성합니다. 생성된 옵저버의 기본 구조는 다음과 같습니다:

<?php namespace App\Observers; use App\Models\User; class UserObserver { /** * User "created" 이벤트 처리 */ public function created(User $user): void { // ... } /** * User "updated" 이벤트 처리 */ public function updated(User $user): void { // ... } /** * User "deleted" 이벤트 처리 */ public function deleted(User $user): void { // ... } /** * User "restored" 이벤트 처리 */ public function restored(User $user): void { // ... } /** * User "forceDeleted" 이벤트 처리 */ public function forceDeleted(User $user): void { // ... } }

옵저버를 등록하는 방법은 두 가지입니다. 첫 번째는 모델에 ObservedBy 어트리뷰트를 붙이는 방법입니다:

use App\Observers\UserObserver; use Illuminate\Database\Eloquent\Attributes\ObservedBy; #[ObservedBy([UserObserver::class])] class User extends Authenticatable { // }

두 번째는 AppServiceProviderboot 메서드에서 observe 메서드를 직접 호출하는 방법입니다:

use App\Models\User; use App\Observers\UserObserver; /** * 애플리케이션 서비스 초기화 */ public function boot(): void { User::observe(UserObserver::class); }

NOTE

옵저버는 saving, retrieved 등 앞서 설명한 모든 이벤트를 수신할 수 있습니다. 전체 이벤트 목록은 이벤트 섹션을 참고하세요.

옵저버와 데이터베이스 트랜잭션

데이터베이스 트랜잭션 안에서 모델을 생성하는 경우, 트랜잭션이 커밋된 후에만 이벤트 핸들러가 실행되도록 설정하고 싶을 수 있습니다. 이럴 때는 옵저버 클래스에 ShouldHandleEventsAfterCommit 인터페이스를 구현하면 됩니다. 트랜잭션이 진행 중이 아닌 경우에는 이벤트 핸들러가 즉시 실행됩니다:

<?php namespace App\Observers; use App\Models\User; use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit; class UserObserver implements ShouldHandleEventsAfterCommit { /** * User "created" 이벤트 처리 */ public function created(User $user): void { // 트랜잭션 커밋 후에 실행됨 } }

이벤트 일시 중단하기

특정 작업을 수행할 때 모델 이벤트가 발생하지 않도록 일시적으로 억제해야 할 때는 withoutEvents 메서드를 사용합니다. 이 메서드에 클로저를 전달하면, 클로저 내부에서 실행되는 코드는 모델 이벤트를 발생시키지 않으며, 클로저의 반환값이 그대로 반환됩니다:

use App\Models\User; $user = User::withoutEvents(function () { User::findOrFail(1)->delete(); return User::find(2); });

이벤트 없이 단일 모델 저장하기

이벤트를 발생시키지 않고 모델을 저장하려면 saveQuietly 메서드를 사용하세요:

$user = User::findOrFail(1); $user->name = '홍길동'; $user->saveQuietly();

마찬가지로 업데이트, 삭제, 소프트 삭제, 복원, 복제 작업도 이벤트 없이 조용히 실행할 수 있습니다:

$user->deleteQuietly(); $user->forceDeleteQuietly(); $user->restoreQuietly();

이 문서는 Laravel 공식 문서(MIT)를 한국 개발자를 위해 번역·재구성한 것입니다.

번역일: 2026년 7월 2일