본문 바로가기

Eloquent: 시작하기

업데이트됨

번역일: 2026년 9월 10일

이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.

원문 수정
2026년 9월 2일
번역 갱신
2026년 9월 10일

Eloquent: 시작하기

소개

Laravel은 데이터베이스와 즐겁게 상호작용할 수 있도록 Eloquent라는 객체 관계 매퍼(ORM)를 기본으로 제공합니다. Eloquent를 사용하면 각 데이터베이스 테이블마다 그에 대응하는 "모델(Model)"이 존재하며, 이 모델을 통해 테이블과 상호작용합니다. 테이블에서 레코드를 조회하는 것은 물론, 모델 인스턴스를 통해 레코드를 추가·수정·삭제하는 작업까지 모두 처리할 수 있습니다.

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

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

# 모델과 FlightFactory 클래스를 함께 생성... 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 <h1 id="inspecting-models">pivot 모델 생성...</h1> php artisan make:model Member --pivot php artisan make:model Member -p

모델 살펴보기

가끔은 코드만 보고 모델의 전체 속성과 연관관계를 파악하기 어려울 때가 있습니다. 이럴 때는 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 { // ... }

테이블 이름

위 예제 코드를 보면 Flight 모델에 어떤 데이터베이스 테이블이 대응하는지 명시적으로 지정하지 않았습니다. 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라는 기본 키(primary key) 컬럼이 있다고 가정합니다. 필요하다면 모델에 보호된(protected) $primaryKey 속성을 정의해 다른 컬럼을 기본 키로 지정할 수 있습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 이 테이블과 관련된 기본 키. * * @var string */ protected $primaryKey = 'flight_id'; }

또한 Eloquent는 기본 키가 자동 증가하는 정수 값이라고 가정하기 때문에, 자동으로 기본 키를 정수로 캐스팅합니다. 만약 자동 증가하지 않거나 숫자가 아닌 기본 키를 사용하고 싶다면, public $incrementing = false;로 설정해야 합니다.

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

모델의 기본 키가 정수가 아니라면, 모델에 보호된 $keyType 속성을 정의해 string 타입으로 지정해야 합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Flight extends Model { /** * 기본 키 ID의 데이터 타입. * * @var string */ protected $keyType = 'string'; }

"복합" 기본 키

Eloquent는 각 모델마다 고유 식별자로 사용할 수 있는 단일 기본 키가 반드시 있어야 합니다. "복합" 기본 키는 Eloquent 모델에서 지원하지 않습니다. 다만 복합 기본 키 외에도, 데이터베이스 테이블에 고유하게 식별 가능한 여러 컬럼으로 구성된 추가적인 유니크 인덱스를 자유롭게 추가하는 것은 가능합니다.

UUID와 ULID 키

Eloquent 모델의 기본 키로 자동 증가하는 정수 대신 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' => 'Traveling to Europe']); $article->id; // "8f8e8478-9035-4d23-b9a7-62f4d2612ce5"

기본적으로 HasUuids 트레이트는 정렬 가능한(ordered) UUID를 모델에 생성해줍니다. 이러한 UUID는 사전순으로 정렬 가능한 타임스탬프 기반 값이므로, 인덱싱된 데이터베이스 저장 공간에 저장할 때 UUID 기본 키가 흔히 야기하는 성능 저하 문제를 줄여줍니다.

모델에서 UUID 생성 방식을 재정의하고 싶다면 newUniqueId 메서드를 정의하면 됩니다. 또한 UUID를 부여할 컬럼을 지정하고 싶다면, 모델에 uniqueIds 메서드를 정의하면 됩니다.

use Ramsey\Uuid\Uuid; /** * 모델에 대해 새로운 UUID를 생성합니다. */ public function newUniqueId(): string { return (string) Uuid::uuid4(); } /** * 고유 식별자를 받아야 하는 컬럼들을 반환합니다. * * @return array<int, string> */ public function uniqueIds(): array { return ['id', 'discount_code']; }

원한다면 UUID 대신 "ULID"를 사용할 수도 있습니다. ULID는 UUID와 유사하지만 26자로 더 짧습니다. 정렬 가능한 UUID와 마찬가지로, ULID도 사전순 정렬이 가능한 타임스탬프 기반 값을 갖고 있습니다. 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' => 'Traveling to Asia']); $article->id; // "01gd4d3tgrrfqeda94gdbtdk5c"

타임스탬프

기본적으로 Eloquent는 모델과 대응되는 데이터베이스 테이블에 created_at, updated_at 컬럼이 존재할 것이라고 예상합니다. 모델이 생성되거나 수정될 때, Eloquent가 이 컬럼의 값을 자동으로 설정해줍니다. 모델에서 이러한 컬럼이 Eloquent에 의해 자동으로 관리되지 않도록 하려면, 모델에 $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_ATUPDATED_AT 상수를 정의하면 됩니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; 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 = 'sqlite'; }

기본 속성 값

기본적으로 새로 인스턴스화된 모델 인스턴스에는 속성 값이 전혀 포함되어 있지 않습니다. 모델의 일부 속성에 대해 기본값을 정의하고 싶다면, 모델에 $attributes 속성을 정의하면 됩니다. $attributes 배열에 지정하는 속성 값은 데이터베이스에서 읽어온 것처럼 이미 저장 가능한 형식이어야 합니다.

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

Eloquent 엄격성(Strictness) 설정하기

Laravel은 다양한 상황에서 Eloquent의 동작과 "엄격함(strictness)"을 커스터마이징할 수 있는 여러 메서드를 제공합니다.

먼저, preventLazyLoading 메서드는 지연 로딩(lazy loading)을 방지할지 여부를 결정하는 선택적인 boolean 인수를 받습니다. 예를 들어, 프로덕션 환경이 아닐 때만 지연 로딩을 비활성화하여 프로덕션 환경에서는 실수로 지연 로딩된 관계가 정상적으로 동작하도록 유지하고 싶을 수 있습니다. 보통 이 메서드는 애플리케이션의 AppServiceProvider에 있는 boot 메서드에서 호출합니다.

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

또한, preventFilling 메서드를 호출하면 채울 수 없는(non-fillable) 속성에 대한 대량 할당(mass assignment)을 방지할 수 있습니다.

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

그리고 preventAccessingMissingAttributes 메서드를 사용하면, 접근하려는 속성이 존재하지 않을 때 기본적으로 null을 반환하는 대신 예외를 발생시키도록 Laravel의 동작 방식을 변경할 수 있습니다. 이는 개발 도중 모델 인스턴스에서 오타로 인한 속성명 접근 문제를 방지하는 데 도움이 됩니다.

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

이렇게 설정한 뒤에는, Eloquent 모델을 조회했지만 아직 데이터베이스 테이블에 존재하지 않는 마이그레이션되지 않은 속성에 접근하려고 하면 Illuminate\Database\Eloquent\MissingAttributeException 예외가 발생합니다.

이러한 방식으로 이 메서드들을 사용하면 로컬 개발 환경과 테스트 환경에서 애플리케이션의 동작을 더 엄격하게 만들 수 있으며, 프로덕션 환경에서 예기치 못한 동작으로 인해 최종 사용자에게 영향을 주는 것을 방지하면서 안정성을 유지할 수 있습니다.

Eloquent 엄격 모드 한 번에 활성화하기

편의를 위해, shouldBeStrict 메서드를 사용하면 위에서 설명한 세 가지 메서드를 한 번에 전부 활성화할 수 있습니다.

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

모델 조회하기

모델과 그에 대응하는 데이터베이스 테이블을 생성했다면, 이제 데이터베이스에서 데이터를 조회할 준비가 된 것입니다. 각 Eloquent 모델을 강력한 쿼리 빌더라고 생각하면 됩니다. 이를 통해 모델과 연관된 데이터베이스 테이블을 유연하게 쿼리할 수 있습니다. 모델의 all 메서드는 모델과 연관된 데이터베이스 테이블의 모든 레코드를 조회합니다.

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

쿼리 작성하기

Eloquent의 all 메서드는 모델 테이블의 모든 결과를 반환합니다. 하지만 각 Eloquent 모델은 쿼리 빌더 역할도 하므로, 쿼리에 추가 제약 조건을 걸었다가 get 메서드를 호출해 결과를 조회할 수 있습니다.

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

NOTE

Eloquent 모델은 쿼리 빌더이기 때문에, Laravel의 쿼리 빌더가 제공하는 모든 메서드를 살펴보는 것이 좋습니다. Eloquent로 쿼리를 작성할 때 이 메서드들을 자유롭게 사용할 수 있습니다.

모델 새로고침하기

이미 데이터베이스에서 조회한 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처럼 여러 결과를 조회하는 Eloquent 메서드는 일반 PHP 배열이 아니라 Illuminate\Database\Eloquent\Collection 인스턴스를 반환합니다.

Eloquent의 Collection 클래스는 Laravel의 기본 Illuminate\Support\Collection 클래스를 상속하며, 데이터 컬렉션을 다루기 위한 다양한 유용한 메서드를 추가로 제공합니다. 예를 들어, reject 메서드를 사용하면 전달한 클로저의 실행 결과에 따라 컬렉션에서 모델을 제거할 수 있습니다.

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

Laravel의 기본 컬렉션 클래스가 제공하는 메서드 외에도, Eloquent 컬렉션 클래스는 Eloquent 모델 컬렉션을 다루는 데 특화된 몇 가지 추가 메서드를 제공합니다.

Laravel의 모든 컬렉션은 PHP의 반복 가능한(iterable) 인터페이스를 구현하고 있으므로, 배열을 순회하는 것처럼 컬렉션도 반복 처리할 수 있습니다.

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

결과 청크(Chunk) 처리하기

수만 개의 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 메서드에 전달하는 첫 번째 인수는 청크당 조회할 레코드 수입니다. 두 번째 인수로 전달하는 클로저는 데이터베이스에서 조회한 각 청크마다 실행됩니다. 각 청크를 데이터베이스에서 조회하기 위해 쿼리가 실행됩니다.

청크 처리 중에 필터링하는 컬럼을 기준으로 값을 업데이트하고 있다면, chunk 메서드의 결과가 예기치 못하고 일관성 없게 나올 수 있습니다. 청크 처리 중에 조회된 레코드를 업데이트할 계획이라면, 항상 chunkById 메서드를 대신 사용하는 것이 좋습니다. 이 메서드는 모델의 기본 키를 기준으로 자동으로 결과를 페이지 분할합니다.

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 Collection)을 이용한 청크 처리

lazy 메서드는 내부적으로 청크 단위로 쿼리를 실행한다는 점에서 chunk 메서드와 비슷하게 동작합니다. 하지만 각 청크를 콜백에 직접 전달하는 대신, lazy() 메서드는 평탄화(flatten)된 Eloquent 모델의 LazyCollection을 반환합니다. 이를 통해 전체 결과를 하나의 스트림처럼 다룰 수 있습니다.

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

청크 처리 중에 필터링에 사용하는 컬럼을 업데이트할 계획이라면, lazyById 메서드를 사용해야 합니다. 이 메서드는 모델의 기본 키를 기준으로 자동으로 결과를 페이지 분할합니다.

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

lazyByIdDesc 메서드를 사용하면 id의 내림차순을 기준으로 결과를 필터링할 수 있습니다.

커서(Cursor)

lazy 메서드와 마찬가지로, cursor 메서드도 수만 건의 Eloquent 모델 레코드를 순회할 때 애플리케이션의 메모리 사용량을 크게 줄이는 데 사용할 수 있습니다.

cursor 메서드는 단 하나의 데이터베이스 쿼리만 실행합니다. 하지만 실제 Eloquent 모델은 순회 과정에서 실제로 필요해질 때까지 각각 하이드레이트(hydrate)되지 않습니다. 따라서 커서를 순회하는 동안에는 한 번에 하나의 Eloquent 모델만 메모리에 유지됩니다.

WARNING

cursor 메서드는 한 번에 하나의 Eloquent 모델만 메모리에 유지하기 때문에, 연관관계를 즉시 로딩(eager load)할 수 없습니다. 연관관계를 즉시 로딩해야 한다면 lazy 메서드를 사용하는 것이 좋습니다.

내부적으로 cursor 메서드는 PHP의 제너레이터(generator)를 사용해 이 기능을 구현합니다.

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 메서드는 일반 쿼리보다 훨씬 적은 메모리를 사용하지만(한 번에 하나의 Eloquent 모델만 메모리에 유지하므로), 결국에는 메모리가 부족해질 수 있습니다. 이는 PHP의 PDO 드라이버가 내부적으로 원시 쿼리 결과 전체를 버퍼에 캐시하기 때문입니다. 아주 많은 양의 레코드를 다뤄야 한다면 lazy 메서드 사용을 고려해보세요.

고급 서브쿼리

서브쿼리 Select

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

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

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();

Eloquent ORM

소개

Laravel은 데이터베이스 작업을 즐겁게 만들어주는 객체-관계 매퍼(ORM)인 Eloquent를 기본으로 제공합니다. Eloquent를 사용할 때는 각 데이터베이스 테이블마다 그에 대응하는 "모델"이 존재하며, 이 모델을 통해 해당 테이블과 상호작용합니다. Eloquent 모델은 테이블에서 레코드를 조회하는 것뿐만 아니라, 레코드를 추가·수정·삭제하는 작업도 지원합니다.

NOTE

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

모델 클래스 생성하기

먼저 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="inserts">모델과 FlightFactory 클래스를 함께 생성...</h1> php artisan make:model Flight --factory php artisan make:model Flight -f <h1 id="updates">모델과 FlightSeeder 클래스를 함께 생성...</h1> php artisan make:model Flight --seed php artisan make:model Flight -s <h1 id="mass-updates">모델과 FlightController 클래스를 함께 생성...</h1> php artisan make:model Flight --controller php artisan make:model Flight -c <h1 id="examining-attribute-changes">모델, FlightController 리소스 클래스, 폼 리퀘스트 클래스를 함께 생성...</h1> php artisan make:model Flight --controller --resource --requests php artisan make:model Flight -crR <h1 id="mass-assignment">모델과 FlightPolicy 클래스를 함께 생성...</h1> php artisan make:model Flight --policy <h1 id="mass-assignment-json-columns">모델, 마이그레이션, 팩토리, 시더, 컨트롤러를 함께 생성...</h1> php artisan make:model Flight -mfsc <h1 id="allowing-mass-assignment">모델, 마이그레이션, 팩토리, 시더, 정책, 컨트롤러, 폼 리퀘스트까지 한 번에 생성하는 단축 옵션...</h1> php artisan make:model Flight --all php artisan make:model Flight -a <h1 id="mass-assignment-exceptions">피벗 모델 생성...</h1> php artisan make:model Member --pivot php artisan make:model Member -p

NOTE

실무에서는 모델을 만들 때마다 매번 옵션을 나열하기보다, 처음부터 필요한 조합을 하나의 명령어로 실행해 두는 편이 반복 작업을 줄여줍니다. 예를 들어 CRUD 리소스를 새로 추가할 때는 -mfsc 조합을 습관처럼 사용하는 개발자가 많습니다.

모델 살펴보기

코드만 훑어봐서는 모델이 어떤 속성(attribute)과 관계(relation)를 가지고 있는지 한눈에 파악하기 어려울 때가 있습니다. 이럴 때는 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 { // ... }

테이블명

위 예제를 보면 Flight 모델이 어떤 데이터베이스 테이블과 연결되는지 따로 지정한 부분이 없다는 걸 눈치채셨을 겁니다. Eloquent는 별도로 지정하지 않는 한, 클래스 이름을 "스네이크 케이스"로 변환한 복수형을 테이블명으로 사용합니다. 즉 Flight 모델은 flights 테이블을, AirTrafficController 모델은 air_traffic_controllers 테이블을 사용한다고 가정합니다.

만약 모델과 연결되는 실제 테이블명이 이 컨벤션과 맞지 않는다면, Table 어트리뷰트를 사용해 테이블명을 직접 지정할 수 있습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Table; use Illuminate\Database\Eloquent\Model; #[Table('my_flights')] class Flight extends Model { // ... }

기본 키

Eloquent는 각 모델의 테이블에 id라는 이름의 기본 키 컬럼이 있다고 가정합니다. 만약 다른 컬럼을 기본 키로 사용해야 한다면, Table 어트리뷰트의 key 인자로 지정할 수 있습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Table; use Illuminate\Database\Eloquent\Model; #[Table(key: 'flight_id')] class Flight extends Model { // ... }

또한 Eloquent는 기본 키가 자동 증가하는 정수 값이라고 가정하고, 이에 맞춰 기본 키 값을 자동으로 정수로 캐스팅합니다. 자동 증가하지 않거나 숫자가 아닌 기본 키를 사용하고 싶다면, Table 어트리뷰트의 keyTypeincrementing 인자를 지정해야 합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Table; use Illuminate\Database\Eloquent\Model; #[Table(key: 'uuid', keyType: 'string', incrementing: false)] class Flight extends Model { // ... }

단순히 자동 증가 ID만 비활성화하고 싶다면, WithoutIncrementing 어트리뷰트를 사용하면 됩니다.

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

"복합" 기본 키

Eloquent 모델은 고유하게 식별할 수 있는 "ID" 하나를 기본 키로 요구하며, 여러 컬럼을 조합한 "복합" 기본 키는 지원하지 않습니다. 다만 테이블 자체에는 기본 키와 별개로 여러 컬럼을 묶은 고유 인덱스를 자유롭게 추가할 수 있습니다.

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' => 'Traveling to Europe']); $article->id; // "018f2b5c-6a7f-7b12-9d6f-2f8a4e0c9c11"

기본적으로 HasUuids 트레이트는 모델을 위해 UUIDv7 식별자를 생성합니다. UUIDv7은 사전순으로 정렬 가능하기 때문에, 인덱싱된 데이터베이스 저장 방식에서 더 효율적입니다.

모델에 newUniqueId 메서드를 정의하면 UUID 생성 방식을 직접 재정의할 수 있습니다. 또한 uniqueIds 메서드를 정의하면 UUID를 부여할 컬럼을 지정할 수도 있습니다.

use Ramsey\Uuid\Uuid; /** * 모델을 위한 새 UUID를 생성합니다. */ public function newUniqueId(): string { return (string) Uuid::uuid4(); } /** * 고유 식별자를 부여받아야 하는 컬럼 목록을 반환합니다. * * @return array<int, string> */ public function uniqueIds(): array { return ['id', 'discount_code']; }

UUID 대신 "ULID"를 사용할 수도 있습니다. ULID는 UUID와 비슷하지만 길이가 26자로 더 짧습니다. 정렬 가능한 UUID와 마찬가지로 ULID 역시 사전순 정렬이 가능해 데이터베이스 인덱싱에 효율적입니다. 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' => 'Traveling to Asia']); $article->id; // "01gd4d3tgrrfqeda94gdbtdk5c"

타임스탬프

기본적으로 Eloquent는 모델의 테이블에 created_at, updated_at 컬럼이 존재한다고 가정하며, 모델을 생성하거나 수정할 때 이 컬럼들의 값을 자동으로 설정합니다. 이런 자동 관리 동작이 필요 없다면, 모델의 Table 어트리뷰트에서 timestampsfalse로 지정하면 됩니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Table; use Illuminate\Database\Eloquent\Model; #[Table(timestamps: false)] class Flight extends Model { // ... }

단순히 타임스탬프만 비활성화하고 싶다면, WithoutTimestamps 어트리뷰트를 사용할 수 있습니다.

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

타임스탬프 포맷을 커스터마이징해야 한다면, Table 어트리뷰트의 dateFormat 인자를 사용하면 됩니다. 이 값은 날짜 속성이 데이터베이스에 저장되는 형식뿐 아니라, 모델을 배열이나 JSON으로 직렬화할 때의 형식도 결정합니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Table; use Illuminate\Database\Eloquent\Model; #[Table(dateFormat: 'U')] class Flight extends Model { // ... }

날짜 포맷만 따로 지정하고 싶다면, DateFormat 어트리뷰트를 사용할 수 있습니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\DateFormat; use Illuminate\Database\Eloquent\Model; #[DateFormat('U')] class Flight extends Model { // ... }

타임스탬프를 저장하는 컬럼명 자체를 변경해야 한다면, 모델에 CREATED_AT, UPDATED_AT 상수를 정의하면 됩니다.

<?php class Flight extends Model { /** * "생성일" 컬럼명입니다. * * @var string|null */ public const CREATED_AT = 'creation_date'; /** * "수정일" 컬럼명입니다. * * @var string|null */ public const UPDATED_AT = 'updated_date'; }

모델의 updated_at 값을 변경하지 않으면서 특정 작업을 수행하고 싶다면, withoutTimestamps 메서드에 클로저를 전달해 그 안에서 작업을 실행하면 됩니다.

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

데이터베이스 커넥션

기본적으로 모든 Eloquent 모델은 애플리케이션에 설정된 기본 데이터베이스 커넥션을 사용합니다. 특정 모델에 대해 다른 커넥션을 사용하고 싶다면, Connection 어트리뷰트를 사용하면 됩니다.

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Connection; use Illuminate\Database\Eloquent\Model; #[Connection('mysql')] class Flight extends Model { // ... }

속성 기본값

새로 인스턴스화한 모델은 기본적으로 어떤 속성 값도 가지고 있지 않습니다. 모델의 일부 속성에 대해 기본값을 지정하고 싶다면, 모델에 $attributes 프로퍼티를 정의하면 됩니다. $attributes 배열에 넣는 값은 데이터베이스에서 방금 읽어온 것과 같은, 가공되지 않은 "저장용" 형식이어야 합니다.

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

Eloquent의 엄격성 설정

Laravel은 다양한 상황에서 Eloquent의 동작과 "엄격성(strictness)"을 설정할 수 있는 여러 메서드를 제공합니다.

먼저 preventLazyLoading 메서드는 지연 로딩(lazy loading)을 막을지 여부를 나타내는 boolean 인자를 선택적으로 받습니다. 예를 들어 운영 환경이 아닌 곳에서만 지연 로딩을 비활성화하도록 설정해두면, 실수로 지연 로딩되는 관계가 운영 환경 코드에 남아 있더라도 서비스가 정상적으로 동작하도록 할 수 있습니다. 일반적으로 이 메서드는 애플리케이션의 AppServiceProviderboot 메서드에서 호출합니다.

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

또한 preventSilentlyDiscardingAttributes 메서드를 호출하면, fillable에 등록되지 않은 속성을 채우려고 시도할 때 예외가 발생하도록 만들 수 있습니다. 이는 로컬 개발 중에 fillable 배열에 추가하지 않은 속성을 실수로 설정했을 때, 예기치 못한 오류를 미연에 방지하는 데 도움이 됩니다.

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

NOTE

preventLazyLoading, preventSilentlyDiscardingAttributes와 같은 엄격성 설정은 개발 단계에서 실수를 조기에 발견하도록 도와주는 안전장치입니다. 운영 환경에서는 성능이나 예상치 못한 예외 발생을 고려해 상황에 맞게 활성화 여부를 결정하세요.

모델 조회하기

모델과 연결된 데이터베이스 테이블을 만들었다면, 이제 데이터베이스에서 데이터를 조회할 준비가 된 것입니다. Eloquent 모델은 단순한 데이터 클래스가 아니라, 해당 모델과 연결된 테이블을 유연하게 조회할 수 있는 강력한 쿼리 빌더라고 생각하면 이해하기 쉽습니다. all 메서드를 사용하면 모델과 연결된 테이블의 모든 레코드를 가져올 수 있습니다.

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

쿼리 작성하기

all 메서드는 테이블의 모든 결과를 반환합니다. 하지만 Eloquent 모델은 그 자체로 쿼리 빌더 역할을 하기 때문에, 원하는 조건을 추가로 지정한 뒤 get 메서드를 호출해서 결과를 가져올 수도 있습니다.

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

NOTE

Eloquent 모델은 곧 쿼리 빌더이므로, Laravel 쿼리 빌더 문서에서 제공하는 메서드들을 모두 살펴보시기 바랍니다. 이 메서드들은 Eloquent 쿼리를 작성할 때 그대로 사용할 수 있습니다.

모델 새로고침하기

데이터베이스에서 조회한 Eloquent 모델 인스턴스를 이미 가지고 있다면, freshrefresh 메서드로 모델을 "새로고침"할 수 있습니다. fresh 메서드는 데이터베이스에서 모델을 다시 조회해서 새로운 인스턴스로 반환합니다. 기존 모델 인스턴스는 그대로 유지됩니다.

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

반면 refresh 메서드는 데이터베이스의 최신 데이터로 기존 모델 인스턴스 자체를 다시 채웁니다. 이때 이미 로드되어 있던 연관관계(relationship)들도 함께 새로고침됩니다.

$flight = Flight::where('number', 'FR 900')->first(); $flight->number = 'FR 456'; $flight->refresh(); $flight->number; // "FR 900"

트랜잭션 안에서 모델을 새로고침하면서 비관적 잠금(pessimistic lock)까지 함께 걸어야 한다면 refreshForUpdate 메서드를 사용하세요. 이 메서드는 FOR UPDATE 잠금을 사용해 모델을 다시 로드합니다.

DB::transaction(function () use ($flight) { $flight->refreshForUpdate(); // 잠금이 걸린 모델을 업데이트... });

컬렉션

앞서 살펴본 것처럼 all이나 get 같은 Eloquent 메서드는 여러 개의 레코드를 조회합니다. 하지만 이 메서드들이 반환하는 결과는 일반 PHP 배열이 아니라 Illuminate\Database\Eloquent\Collection 인스턴스입니다.

Eloquent의 Collection 클래스는 Laravel의 기본 Illuminate\Support\Collection 클래스를 상속하며, 데이터 컬렉션을 다루는 데 유용한 다양한 메서드를 그대로 사용할 수 있습니다. 예를 들어, reject 메서드를 사용하면 클로저의 실행 결과에 따라 컬렉션에서 특정 모델을 제거할 수 있습니다.

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

Laravel 기본 컬렉션 클래스가 제공하는 메서드 외에도, Eloquent 컬렉션 클래스는 Eloquent 모델 컬렉션을 다루는 데 특화된 추가 메서드들을 제공합니다.

Laravel의 모든 컬렉션은 PHP의 반복(iterable) 인터페이스를 구현하고 있으므로, 배열처럼 foreach로 순회할 수 있습니다.

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

결과를 청크(chunk) 단위로 나누어 처리하기

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

chunk 메서드는 Eloquent 모델을 일정 개수씩 나눠서 조회한 뒤, 그 묶음(청크)을 클로저에 전달합니다. 한 번에 현재 청크만 메모리에 올리기 때문에, 대량의 모델을 다룰 때 메모리 사용량을 크게 줄일 수 있습니다.

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

chunk 메서드의 첫 번째 인수는 한 번에 가져올 "청크"당 레코드 개수입니다. 두 번째 인수로 전달한 클로저는 데이터베이스에서 청크를 하나씩 가져올 때마다 호출됩니다. 즉, 청크마다 한 번씩 별도의 쿼리가 실행됩니다.

만약 chunk 메서드로 조회한 결과를 반복하면서 동시에 필터링 기준이 되는 컬럼 값을 업데이트해야 한다면, chunkById 메서드를 사용해야 합니다. 이런 상황에서 chunk 메서드를 그대로 사용하면 예상치 못한 결과나 데이터 누락이 발생할 수 있습니다. chunkById 메서드는 내부적으로 항상 이전 청크의 마지막 모델보다 id 컬럼 값이 큰 모델들을 조회합니다.

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

NOTE

예를 들어 "출발 예정 항공편"을 조회한 뒤 반복문 안에서 departed 컬럼 값을 바꿔버리면, 다음 청크를 조회할 때 방금 값이 바뀐 레코드가 조건에서 빠지거나 중복 처리되는 문제가 생길 수 있습니다. chunkByIdid 기준으로 커서를 옮겨가며 조회하기 때문에 이런 문제를 피할 수 있습니다.

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 컬렉션으로 청크 단위 처리하기

lazy 메서드는 내부적으로 쿼리를 청크 단위로 실행한다는 점에서 chunk 메서드와 비슷하게 동작합니다. 다만 각 청크를 콜백에 그대로 전달하는 대신, lazy 메서드는 하나로 이어진(flattened) Eloquent 모델의 LazyCollection을 반환합니다. 덕분에 여러 청크에 걸친 결과를 마치 하나의 연속된 스트림처럼 다룰 수 있습니다.

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

lazy 메서드로 조회한 결과를 반복하면서 필터링 기준이 되는 컬럼을 함께 업데이트해야 한다면 lazyById 메서드를 사용하세요. lazyById 메서드는 내부적으로 항상 이전 청크의 마지막 모델보다 id 컬럼 값이 큰 모델들을 조회합니다.

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

lazyByIdDesc 메서드를 사용하면 id의 내림차순을 기준으로 결과를 필터링할 수 있습니다.

커서(Cursor)

lazy 메서드와 마찬가지로, cursor 메서드도 수만 건의 Eloquent 모델을 순회할 때 애플리케이션의 메모리 사용량을 크게 줄여줍니다.

cursor 메서드는 데이터베이스 쿼리를 단 한 번만 실행합니다. 하지만 개별 Eloquent 모델 객체는 실제로 반복문에서 사용되는 시점에만 하이드레이션(생성)됩니다. 따라서 커서를 순회하는 동안에는 어느 시점이든 메모리에 단 하나의 Eloquent 모델만 존재하게 됩니다.

WARNING

cursor 메서드는 한 번에 단 하나의 모델만 메모리에 유지하기 때문에, 연관관계를 즉시 로딩(eager load)할 수 없습니다. 연관관계를 즉시 로딩해야 한다면 lazy 메서드를 대신 사용하세요.

cursor 메서드는 내부적으로 PHP의 제너레이터(generator)를 사용해 이 기능을 구현합니다.

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

cursor 메서드는 Illuminate\Support\LazyCollection 인스턴스를 반환합니다. Lazy 컬렉션을 사용하면 한 번에 하나의 모델만 메모리에 로드하면서도, 일반적인 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의 PDO 드라이버가 원본 쿼리 결과 전체를 내부 버퍼에 캐싱하기 때문입니다. 매우 많은 수의 Eloquent 레코드를 다뤄야 한다면 lazy 메서드 사용을 고려해보세요.

고급 서브쿼리 활용

서브쿼리를 이용한 Select

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

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

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();

Eloquent ORM

단일 모델 / 집계값 조회하기

조건에 맞는 전체 레코드를 컬렉션으로 조회하는 것 외에도, 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 () { // ... });

결과가 없을 때 예외 발생시키기

모델을 찾지 못했을 때 예외를 던지고 싶은 경우도 있습니다. 특히 라우트나 컨트롤러에서 유용하게 쓰입니다. findOrFailfirstOrFail 메서드는 쿼리의 첫 번째 결과를 조회하되, 결과가 없으면 Illuminate\Database\Eloquent\ModelNotFoundException 예외를 던집니다:

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

ModelNotFoundException을 별도로 잡아서 처리하지 않으면, 클라이언트에게 자동으로 404 HTTP 응답이 반환됩니다:

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

NOTE

이처럼 예외를 잡지 않아도 자동으로 404 응답이 반환되는 이유는, Laravel이 ModelNotFoundException을 감지해 처리하는 예외 핸들러를 기본으로 등록해두었기 때문입니다. API 라우트에서 findOrFail을 즐겨 쓰는 것도 이 덕분입니다 — 별도의 try/catch 없이도 "존재하지 않는 리소스" 응답을 자연스럽게 처리할 수 있습니다.

모델 조회 또는 생성하기

firstOrCreate 메서드는 주어진 컬럼 / 값 쌍으로 데이터베이스에서 레코드를 찾습니다. 만약 데이터베이스에서 해당 모델을 찾을 수 없다면, 첫 번째 배열 인자와 (선택적으로 전달하는) 두 번째 배열 인자를 병합한 속성으로 새 레코드를 삽입합니다.

firstOrNew 메서드도 firstOrCreate와 마찬가지로 주어진 속성과 일치하는 레코드를 데이터베이스에서 찾으려 시도합니다. 다만 모델을 찾지 못한 경우, 새로운 모델 인스턴스를 반환할 뿐 데이터베이스에 저장하지는 않습니다. 따라서 firstOrNew가 반환한 모델은 아직 데이터베이스에 저장되지 않은 상태이며, 저장하려면 직접 save 메서드를 호출해야 합니다:

use App\Models\Flight; // 이름으로 항공편을 조회하고, 없으면 새로 생성... $flight = Flight::firstOrCreate([ 'name' => 'London to Paris' ]); // 이름으로 조회하고, 없으면 name, delayed, arrival_time 속성으로 생성... $flight = Flight::firstOrCreate( ['name' => 'London to Paris'], ['delayed' => 1, 'arrival_time' => '11:30'] ); // 이름으로 조회하고, 없으면 새로운 Flight 인스턴스를 생성(미저장)... $flight = Flight::firstOrNew([ 'name' => 'London to Paris' ]); // 이름으로 조회하고, 없으면 name, delayed, arrival_time 속성을 채운 인스턴스를 생성(미저장)... $flight = Flight::firstOrNew( ['name' => 'Tokyo to Sydney'], ['delayed' => 1, 'arrival_time' => '11:30'] );

집계값 조회하기

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

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

모델 삽입과 수정

삽입

Eloquent는 데이터베이스에서 모델을 조회하는 것뿐만 아니라, 새로운 레코드를 삽입하는 작업도 간편하게 처리할 수 있게 해줍니다. 데이터베이스에 새 레코드를 삽입하려면 새 모델 인스턴스를 생성하고 속성 값을 설정한 다음, 해당 인스턴스에서 save 메서드를 호출하면 됩니다:

<?php namespace App\Http\Controllers; 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_at, updated_at 타임스탬프는 save 호출 시 자동으로 설정되므로 직접 지정할 필요가 없습니다.

데이터베이스 트랜잭션 안에서 모델을 저장하고 싶다면 saveOrFail 메서드를 사용할 수 있습니다. 저장 도중 예외가 발생하면 트랜잭션이 자동으로 롤백됩니다:

$flight->saveOrFail();

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

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

다만 create 메서드를 사용하려면 먼저 모델 클래스에 Fillable 또는 Guarded 속성(attribute)을 지정해야 합니다. Eloquent 모델은 기본적으로 대량 할당(mass assignment) 취약점으로부터 보호되기 때문에, 이러한 속성 지정이 반드시 필요합니다. 대량 할당에 대해 더 자세히 알고 싶다면 대량 할당 문서를 참고하세요.

수정

save 메서드는 이미 데이터베이스에 존재하는 모델을 수정할 때도 사용할 수 있습니다. 모델을 조회한 뒤 원하는 속성 값을 변경하고 save 메서드를 호출하면 됩니다. 이때도 updated_at 타임스탬프가 자동으로 갱신되므로 직접 값을 지정할 필요는 없습니다:

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

트랜잭션 안에서 모델을 수정하고 싶다면 updateOrFail 메서드를 사용할 수 있습니다. 수정 중 예외가 발생하면 트랜잭션이 자동으로 롤백됩니다:

$flight->updateOrFail(['name' => 'Paris to London']);

때로는 기존 모델을 수정하거나, 일치하는 모델이 없으면 새로 생성해야 하는 경우가 있습니다. firstOrCreate 메서드와 마찬가지로 updateOrCreate 메서드도 결과를 즉시 저장하므로 별도로 save 메서드를 호출할 필요가 없습니다.

아래 예제에서는 departureOakland이고 destinationSan Diego인 항공편이 이미 존재하면 해당 레코드의 pricediscounted 컬럼을 수정합니다. 만약 그런 항공편이 없다면, 첫 번째 인자 배열과 두 번째 인자 배열을 합친 속성으로 새 항공편을 생성합니다:

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

firstOrCreateupdateOrCreate 같은 메서드를 사용할 때는 새 모델이 생성된 것인지, 기존 모델이 수정된 것인지 바로 알기 어려울 수 있습니다. 이럴 때는 wasRecentlyCreated 속성을 확인하면 현재 생명주기 동안 모델이 새로 생성되었는지 알 수 있습니다:

$flight = Flight::updateOrCreate( // ... ); if ($flight->wasRecentlyCreated) { // 새 항공편 레코드가 삽입되었습니다... }

대량 수정

특정 조건에 일치하는 모델들을 한 번에 수정할 수도 있습니다. 아래 예제에서는 active 상태이고 목적지(destination)가 San Diego인 모든 항공편을 지연(delayed) 상태로 표시합니다:

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

update 메서드에는 수정할 컬럼과 값의 쌍으로 이루어진 배열을 전달합니다. 이 메서드는 영향을 받은 행(row)의 개수를 반환합니다.

WARNING

Eloquent를 통해 대량 수정을 실행하면 saving, saved, updating, updated 모델 이벤트가 발생하지 않습니다. 대량 수정 시에는 대상 모델이 실제로 메모리에 조회되지 않기 때문입니다.

속성 변경 여부 확인

Eloquent는 isDirty, isClean, wasChanged 메서드를 제공하여 모델을 처음 조회했을 때와 비교해 속성 값이 어떻게 변경되었는지 확인할 수 있게 해줍니다.

isDirty 메서드는 모델을 조회한 이후 속성 값이 하나라도 변경되었는지 확인합니다. 특정 속성 이름이나 속성 배열을 인자로 전달하면 해당 속성들이 "dirty"(변경됨) 상태인지 확인할 수 있습니다. 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(); // 원본 속성 배열...

getChanges 메서드는 모델을 마지막으로 저장할 때 변경된 속성들을 배열로 반환하며, getPrevious 메서드는 마지막 저장 이전의 원본 속성 값을 배열로 반환합니다:

$user = User::find(1); $user->name; // John $user->email; // john@example.com $user->update([ 'name' => 'Jack', 'email' => 'jack@example.com', ]); $user->getChanges(); /* [ 'name' => 'Jack', 'email' => 'jack@example.com', ] */ $user->getPrevious(); /* [ 'name' => 'John', 'email' => 'john@example.com', ] */

대량 할당(Mass Assignment)

앞서 살펴본 것처럼 create 메서드를 사용하면 PHP 구문 한 줄로 새 모델을 "저장"할 수 있으며, 생성된 모델 인스턴스가 반환됩니다:

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

다만 create 메서드를 사용하려면 먼저 모델 클래스에 Fillable 또는 Guarded 속성을 지정해야 합니다. Eloquent 모델은 기본적으로 대량 할당 취약점으로부터 보호되기 때문입니다.

대량 할당 취약점이란, 사용자가 예상치 못한 HTTP 요청 필드를 보내고 이 값이 개발자가 의도하지 않은 데이터베이스 컬럼을 변경해버리는 문제를 말합니다. 예를 들어, 악의적인 사용자가 HTTP 요청에 is_admin 파라미터를 슬쩍 끼워 보내고, 이 값이 그대로 모델의 create 메서드에 전달된다면 자신을 관리자로 승격시킬 수도 있습니다.

이러한 문제를 막기 위해, 대량 할당을 허용할 모델 속성을 명시적으로 지정해야 합니다. 모델에 Fillable 속성(attribute)을 사용하면 됩니다. 예를 들어 Flight 모델의 name 속성을 대량 할당 가능하도록 지정해봅시다:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Model; #[Fillable(['name'])] class Flight extends Model { // ... }

대량 할당 가능한 속성을 지정했다면, create 메서드로 새 레코드를 데이터베이스에 삽입할 수 있습니다. create 메서드는 새로 생성된 모델 인스턴스를 반환합니다:

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

이미 존재하는 모델 인스턴스가 있다면 fill 메서드를 사용해 속성 배열로 값을 채울 수 있습니다:

$flight->fill(['name' => 'Amsterdam to Frankfurt']);

대량 할당과 JSON 컬럼

JSON 컬럼에 값을 대량 할당하려면 각 컬럼에서 대량 할당을 허용할 키를 모델의 Fillable 속성에 명시해야 합니다. 보안상의 이유로, Laravel은 Guarded 속성을 사용하는 경우 중첩된 JSON 속성의 업데이트는 지원하지 않습니다:

use Illuminate\Database\Eloquent\Attributes\Fillable; #[Fillable(['options->enabled'])] class Flight extends Model { // ... }

모든 속성을 대량 할당 허용하기

모델의 모든 속성을 대량 할당 가능하게 만들고 싶다면 모델에 Unguarded 속성을 사용하면 됩니다. 다만 이렇게 모델의 보호 기능을 해제(unguard)하는 경우에는, fill, create, update 메서드에 전달하는 배열을 항상 직접 세심하게 구성해야 합니다:

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

대량 할당 예외

기본적으로 Fillable 속성에 포함되지 않은 속성은 대량 할당 작업 시 아무런 경고 없이 조용히 무시됩니다. 운영 환경에서는 이것이 자연스러운 동작이지만, 로컬 개발 중에는 모델 변경 사항이 왜 반영되지 않는지 몰라 혼란스러울 수 있습니다.

원한다면 preventSilentlyDiscardingAttributes 메서드를 호출해서, 할당할 수 없는 속성을 채우려고 시도할 때 예외가 발생하도록 설정할 수 있습니다. 일반적으로 이 메서드는 애플리케이션의 AppServiceProvider 클래스의 boot 메서드 안에서 호출합니다:

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

Upsert

Eloquent의 upsert 메서드를 사용하면 레코드를 수정하거나 생성하는 작업을 단일 원자적(atomic) 연산으로 처리할 수 있습니다. 첫 번째 인자에는 삽입하거나 수정할 값들을 전달하고, 두 번째 인자에는 테이블 내에서 레코드를 고유하게 식별할 수 있는 컬럼(들)을 지정합니다. 세 번째이자 마지막 인자에는 이미 일치하는 레코드가 있을 경우 수정해야 할 컬럼들의 배열을 전달합니다. 모델에서 타임스탬프 기능이 활성화되어 있다면 upsert 메서드는 created_at, updated_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 드라이버는 upsert 메서드의 두 번째 인자를 무시하고, 항상 테이블에 설정된 "기본 키"와 "고유" 인덱스를 기준으로 기존 레코드를 판별합니다.

Eloquent ORM

모델 삭제하기

모델을 삭제하려면 모델 인스턴스에서 delete 메서드를 호출하면 됩니다:

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

데이터베이스 트랜잭션 내에서 모델을 삭제하고 싶다면 deleteOrFail 메서드를 사용할 수 있습니다. 삭제 도중 예외가 발생하면 트랜잭션이 자동으로 롤백됩니다:

$flight->deleteOrFail();

기본 키로 기존 모델 삭제하기

위 예시에서는 delete 메서드를 호출하기 전에 먼저 데이터베이스에서 모델을 조회했습니다. 하지만 모델의 기본 키 값을 이미 알고 있다면, 굳이 모델을 조회하지 않고도 destroy 메서드를 호출해 바로 삭제할 수 있습니다. destroy 메서드는 단일 기본 키뿐만 아니라 여러 개의 기본 키, 기본 키 배열, 컬렉션 형태의 기본 키 목록도 인자로 받을 수 있습니다:

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

소프트 삭제(soft delete)를 사용하는 모델이라면, forceDestroy 메서드로 해당 모델을 완전히(영구적으로) 삭제할 수 있습니다:

Flight::forceDestroy(1);

WARNING

destroy 메서드는 각 모델을 개별적으로 로드한 뒤 delete 메서드를 호출하는 방식으로 동작합니다. 이는 각 모델에 대해 deleting, deleted 이벤트가 정상적으로 발생하도록 하기 위함입니다.

쿼리를 사용해 모델 삭제하기

물론 조건에 맞는 모델들을 한 번에 삭제하는 Eloquent 쿼리를 작성할 수도 있습니다. 아래 예시에서는 active 값이 0으로 표시된, 즉 비활성 상태인 항공편을 모두 삭제합니다. 대량 업데이트(mass update)와 마찬가지로, 대량 삭제(mass delete) 역시 삭제되는 모델에 대한 이벤트를 발생시키지 않는다는 점에 유의하세요:

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

테이블의 모든 모델을 삭제하려면, 조건절 없이 쿼리를 실행하면 됩니다:

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

WARNING

Eloquent를 통해 대량 삭제(mass delete) 구문을 실행하는 경우, 삭제되는 모델에 대해 deleting, deleted 이벤트가 발생하지 않습니다. 삭제 쿼리를 실행할 때 모델을 실제로 조회하는 과정을 거치지 않기 때문입니다.

소프트 삭제 (Soft Deleting)

Eloquent는 데이터베이스에서 레코드를 실제로 제거하는 것 외에도, 모델을 "소프트 삭제"할 수 있는 기능을 제공합니다. 모델이 소프트 삭제되면 실제로 데이터베이스에서 레코드가 지워지는 것이 아니라, 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 컬럼에 현재 날짜와 시간이 저장됩니다. 하지만 해당 레코드는 여전히 테이블에 남아 있습니다. 소프트 삭제를 사용하는 모델을 조회할 때는, 소프트 삭제된 모델이 조회 결과에서 자동으로 제외됩니다.

특정 모델 인스턴스가 소프트 삭제되었는지 확인하려면 trashed 메서드를 사용하세요:

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

NOTE

소프트 삭제는 "휴지통"과 비슷한 개념이라고 생각하면 이해하기 쉽습니다. 파일을 휴지통으로 옮기면 화면에서는 보이지 않지만 완전히 사라진 것은 아니며, 필요하면 다시 복원할 수 있는 것과 같은 원리입니다.

소프트 삭제된 모델 복원하기

경우에 따라 소프트 삭제된 모델을 다시 "복원"하고 싶을 수 있습니다. 모델 인스턴스에서 restore 메서드를 호출하면 소프트 삭제된 모델을 복원할 수 있습니다. restore 메서드는 모델의 deleted_at 컬럼 값을 null로 설정합니다:

$flight->restore();

여러 모델을 한 번에 복원하고 싶다면 쿼리에서 restore 메서드를 사용할 수도 있습니다. 다른 "대량" 작업들과 마찬가지로, 이 경우에도 복원되는 모델에 대한 이벤트는 발생하지 않습니다:

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

restore 메서드는 연관관계 쿼리를 작성할 때도 사용할 수 있습니다:

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

모델 영구 삭제하기

때로는 모델을 데이터베이스에서 완전히 제거해야 할 때가 있습니다. 소프트 삭제된 모델을 테이블에서 영구적으로 삭제하려면 forceDelete 메서드를 사용하세요:

$flight->forceDelete();

Eloquent 연관관계 쿼리를 작성할 때도 forceDelete 메서드를 사용할 수 있습니다:

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

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

소프트 삭제된 모델 포함해서 조회하기

앞서 설명했듯이 소프트 삭제된 모델은 조회 결과에서 자동으로 제외됩니다. 하지만 쿼리에 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();

모델 정리(Pruning)

더 이상 필요하지 않은 모델을 주기적으로 삭제하고 싶을 때가 있습니다. 이럴 때는 정리하고자 하는 모델에 Illuminate\Database\Eloquent\Prunable 또는 Illuminate\Database\Eloquent\MassPrunable 트레이트를 추가하면 됩니다. 둘 중 하나를 추가한 뒤에는, 더 이상 필요 없는 모델들을 조회하는 Eloquent 쿼리 빌더를 반환하는 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; /** * Get the prunable model query. */ public function prunable(): Builder { return static::where('created_at', '<=', now()->minus(months: 1)); } }

Prunable 트레이트를 사용하는 모델에는 pruning 메서드도 정의할 수 있습니다. 이 메서드는 모델이 삭제되기 직전에 호출되며, 모델이 데이터베이스에서 영구히 제거되기 전에 관련된 저장 파일 등 추가 리소스를 함께 정리하고 싶을 때 유용합니다.

/** * Prepare the model for pruning. */ 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 옵션을 붙여 model:prune 명령어를 실행해 보세요. 이 옵션을 사용하면 실제로 삭제하지 않고, 조건에 맞는 레코드가 몇 건인지만 알려줍니다.

php artisan model:prune --pretend

WARNING

소프트 삭제(soft delete)가 적용된 모델이라도 prunable 쿼리 조건에 해당하면 forceDelete를 통해 완전히 삭제됩니다.

대량 정리(Mass Pruning)

Illuminate\Database\Eloquent\MassPrunable 트레이트를 사용하는 모델은 대량 삭제(mass-deletion) 쿼리를 통해 삭제됩니다. 이 방식에서는 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; /** * Get the prunable model query. */ public function prunable(): Builder { return static::where('created_at', '<=', now()->minus(months: 1)); } }

NOTE

Prunable은 개별 모델 이벤트(예: 첨부 파일 삭제 같은 후처리)가 필요할 때, MassPrunable은 단순 삭제만으로 충분하고 성능이 중요할 때 선택하면 됩니다.

Eloquent ORM

모델 복제하기

replicate 메서드를 사용하면 기존 모델 인스턴스를 복사해서 아직 저장되지 않은 새로운 모델 인스턴스를 만들 수 있습니다. 속성 대부분이 동일한 모델을 여러 개 만들어야 할 때 특히 유용합니다.

예를 들어 배송지 주소를 기반으로 청구지 주소를 만드는 상황을 생각해 보겠습니다:

use App\Models\Address; $shipping = Address::create([ 'type' => 'shipping', 'line_1' => '123 Example Street', 'city' => 'Victorville', 'state' => 'CA', 'postcode' => '90001', ]); $billing = $shipping->replicate()->fill([ 'type' => 'billing' ]); $billing->save();

NOTE

replicate로 생성된 모델은 원본과 동일한 속성값을 가지지만 저장되기 전까지는 데이터베이스에 반영되지 않습니다. 위 예시처럼 fill 메서드로 일부 속성만 바꿔준 뒤 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' ]);

위 예시에서는 항공편 정보를 복제하면서 last_flown(마지막 운항 시각)과 last_pilot_id(마지막 조종사 ID)는 새 인스턴스에 복사되지 않도록 제외했습니다. 새로 만든 항공편 레코드는 아직 운항한 적이 없으므로, 이전 운항 기록을 그대로 물려받지 않게 하려는 것이지요.

쿼리 스코프

글로벌 스코프

글로벌 스코프는 특정 모델에 대한 모든 쿼리에 공통 제약 조건을 추가할 수 있는 기능입니다. Laravel의 소프트 삭제 기능 자체도 글로벌 스코프를 사용해서 "삭제되지 않은" 모델만 조회하도록 만들어져 있습니다. 이처럼 글로벌 스코프를 직접 작성하면, 특정 모델을 대상으로 하는 모든 쿼리에 항상 동일한 조건이 적용되도록 간편하게 보장할 수 있습니다.

스코프 생성하기

새 글로벌 스코프를 생성하려면 make:scope Artisan 명령어를 실행하면 됩니다. 생성된 스코프 클래스는 애플리케이션의 app/Models/Scopes 디렉터리에 위치합니다:

php artisan make:scope AncientScope

글로벌 스코프 작성하기

글로벌 스코프를 작성하는 방법은 간단합니다. 먼저 make:scope 명령어로 Illuminate\Database\Eloquent\Scope 인터페이스를 구현하는 클래스를 생성합니다. 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()->minus(years: 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 메서드를 호출하는 방식으로 직접 등록할 수도 있습니다. 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); } }

위 예제처럼 App\Models\User 모델에 스코프를 추가하면, User::all()을 호출할 때 다음과 같은 SQL 쿼리가 실행됩니다:

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

익명 글로벌 스코프

Eloquent에서는 클로저를 사용해서도 글로벌 스코프를 정의할 수 있습니다. 별도의 클래스를 만들 정도는 아닌 간단한 스코프에 특히 유용합니다. 클로저로 글로벌 스코프를 정의할 때는, 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()->minus(years: 2000)); }); } }

글로벌 스코프 제거하기

특정 쿼리에서 글로벌 스코프를 제거하고 싶다면 withoutGlobalScope 메서드를 사용하면 됩니다. 이 메서드는 제거할 글로벌 스코프의 클래스명을 유일한 인자로 받습니다:

User::withoutGlobalScope(AncientScope::class)->get();

클로저로 정의한 글로벌 스코프라면, 해당 스코프에 지정했던 문자열 이름을 전달하면 됩니다:

User::withoutGlobalScope('ancient')->get();

여러 개 또는 전체 글로벌 스코프를 한 번에 제거하고 싶다면 withoutGlobalScopeswithoutGlobalScopesExcept 메서드를 사용할 수 있습니다:

// 모든 글로벌 스코프 제거... User::withoutGlobalScopes()->get(); // 일부 글로벌 스코프만 제거... User::withoutGlobalScopes([ FirstScope::class, SecondScope::class ])->get(); // 지정한 스코프를 제외한 모든 글로벌 스코프 제거... User::withoutGlobalScopesExcept([ SecondScope::class, ])->get();

로컬 스코프

로컬 스코프를 사용하면 애플리케이션 전체에서 재사용할 수 있는 공통 쿼리 제약 조건을 정의할 수 있습니다. 예를 들어, "인기 있는" 사용자만 자주 조회해야 하는 상황을 가정해봅시다. 이런 경우 Eloquent 메서드에 Scope 어트리뷰트를 붙여서 스코프를 정의할 수 있습니다.

스코프 메서드는 항상 동일한 쿼리 빌더 인스턴스를 반환하거나 void를 반환해야 합니다:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * 인기 있는 사용자만 포함하도록 쿼리 범위를 지정합니다. */ #[Scope] protected function popular(Builder $query): void { $query->where('votes', '>', 100); } /** * 활성 사용자만 포함하도록 쿼리 범위를 지정합니다. */ #[Scope] protected function active(Builder $query): void { $query->where('active', 1); } }

로컬 스코프 사용하기

스코프를 정의했다면, 모델을 조회할 때 스코프 메서드를 그대로 호출하면 됩니다. 여러 스코프를 체이닝해서 함께 사용할 수도 있습니다:

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

여러 Eloquent 스코프를 or 연산자로 조합하려면, 올바른 논리 그룹화를 위해 클로저를 사용해야 할 수 있습니다:

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

다만 이 방식은 번거로울 수 있기 때문에, Laravel은 클로저 없이도 스코프를 유연하게 체이닝할 수 있는 "고차(higher order)" orWhere 메서드를 제공합니다:

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

동적 스코프

파라미터를 받는 스코프를 정의하고 싶을 때도 있습니다. 이럴 때는 스코프 메서드의 시그니처에 필요한 파라미터를 추가하면 됩니다. 스코프 파라미터는 $query 파라미터 뒤에 정의해야 합니다:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class User extends Model { /** * 지정한 타입의 사용자만 포함하도록 쿼리 범위를 지정합니다. */ #[Scope] protected function ofType(Builder $query, string $type): void { $query->where('type', $type); } }

스코프 메서드 시그니처에 필요한 인자를 추가했다면, 스코프를 호출할 때 해당 인자를 전달하면 됩니다:

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

어트리뷰트로 정의한 스코프 메서드는 protected로 선언해야 합니다. 모델 클래스 내부에서 어트리뷰트 스코프를 호출할 때는, 호출이 Eloquent의 스코프 처리 로직을 거치도록 static::query()->ofType('admin')처럼 쿼리 빌더 인스턴스를 통해 호출해야 합니다.

대기 속성(Pending Attributes)

스코프로 제약한 조건과 동일한 속성값을 가진 모델을 생성하고 싶다면, 스코프 쿼리를 작성할 때 withAttributes 메서드를 사용할 수 있습니다:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class Post extends Model { /** * 임시 저장(draft) 상태의 게시물만 포함하도록 쿼리 범위를 지정합니다. */ #[Scope] protected function draft(Builder $query): void { $query->withAttributes([ 'hidden' => true, ]); } }

withAttributes 메서드는 전달한 속성값을 기준으로 쿼리에 where 조건을 추가하며, 동시에 해당 스코프를 통해 생성되는 모델에도 이 속성값을 자동으로 채워줍니다:

$draft = Post::draft()->create(['title' => 'In Progress']); $draft->hidden; // true

withAttributes 메서드가 쿼리에 where 조건을 추가하지 않도록 하려면, asConditions 인자를 false로 지정하면 됩니다:

$query->withAttributes([ 'hidden' => true, ], asConditions: false);

Eloquent ORM

모델 비교하기

두 모델이 서로 "같은" 모델인지 확인해야 할 때가 있습니다. isisNot 메서드를 사용하면 두 모델이 동일한 기본 키(primary key), 테이블, 데이터베이스 커넥션을 가지고 있는지 간단하게 확인할 수 있습니다:

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

isisNot 메서드는 belongsTo, hasOne, morphTo, morphOne 관계를 사용할 때도 활용할 수 있습니다. 이 메서드는 연관된 모델을 조회하기 위해 별도의 쿼리를 실행하지 않고도 비교할 수 있어 특히 유용합니다:

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

NOTE

예를 들어 게시글(Post)과 작성자(User)를 belongsTo 관계로 연결해 둔 경우, $post->author로 실제 모델을 조회하는 대신 $post->author()->is($user)처럼 관계 메서드 자체에 is를 호출하면 데이터베이스 쿼리 없이도 비교할 수 있습니다. 조회된 모델과 비교 대상 모델의 동일성만 확인하면 되는 상황이라면 이 방식이 더 효율적입니다.

Eloquent ORM

이벤트

NOTE

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

Eloquent 모델은 라이프사이클의 여러 시점에 개입할 수 있도록 다양한 이벤트를 발생시킵니다. 사용 가능한 이벤트는 retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, trashed, forceDeleting, forceDeleted, restoring, restored, replicating입니다.

retrieved 이벤트는 데이터베이스에서 기존 모델을 조회할 때 발생합니다. 새 모델을 처음 저장할 때는 creatingcreated 이벤트가 발생합니다. 기존 모델을 수정한 뒤 save 메서드를 호출하면 updating / updated 이벤트가 발생합니다. saving / saved 이벤트는 모델이 생성되거나 갱신될 때, 즉 속성이 실제로 변경되지 않았더라도 발생합니다. 이름이 -ing으로 끝나는 이벤트는 모델 변경 사항이 데이터베이스에 반영되기 전에 발생하고, -ed로 끝나는 이벤트는 변경 사항이 반영된 후에 발생합니다.

모델 이벤트를 리스닝하려면, Eloquent 모델에 $dispatchesEvents 속성을 정의하세요. 이 속성은 Eloquent 모델 라이프사이클의 각 시점을 여러분이 직접 만든 이벤트 클래스에 매핑합니다. 각 모델 이벤트 클래스는 생성자를 통해 영향을 받은 모델 인스턴스를 전달받도록 작성해야 합니다.

<?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, ]; }

Eloquent 이벤트를 정의하고 매핑했다면, 이벤트 리스너를 사용해 해당 이벤트를 처리할 수 있습니다.

WARNING

Eloquent를 통해 대량(mass) 업데이트나 삭제 쿼리를 실행하는 경우, 영향을 받은 모델에 대해 saved, updated, deleting, deleted 모델 이벤트가 발생하지 않습니다. 대량 업데이트/삭제 작업에서는 모델 인스턴스가 실제로 조회되지 않기 때문입니다.

클로저 사용하기

커스텀 이벤트 클래스를 만드는 대신, 모델 이벤트가 발생할 때 실행될 클로저를 직접 등록할 수도 있습니다. 이런 클로저는 보통 모델의 booted 메서드 안에서 등록합니다.

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

필요하다면 모델 이벤트를 등록할 때 큐로 처리 가능한 익명 이벤트 리스너를 활용할 수도 있습니다. 이를 사용하면 Laravel은 애플리케이션의 를 이용해 해당 리스너를 백그라운드에서 실행합니다.

use function Illuminate\Events\queueable; static::created(queueable(function (User $user) { // ... }));

옵저버(Observers)

옵저버 정의하기

특정 모델에서 여러 이벤트를 리스닝해야 한다면, 옵저버를 사용해 관련 리스너들을 하나의 클래스로 묶는 것이 좋습니다. 옵저버 클래스는 리스닝하고자 하는 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 { // }

또는 옵저버를 등록하고자 하는 모델에서 observe 메서드를 직접 호출하여 수동으로 등록할 수도 있습니다. 보통 애플리케이션의 AppServiceProvider 클래스에 있는 boot 메서드에서 등록합니다.

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 { // ... } }

이벤트 잠시 끄기(Muting)

경우에 따라 특정 모델이 발생시키는 모든 이벤트를 일시적으로 "음소거"해야 할 때가 있습니다. 이럴 때는 withoutEvents 메서드를 사용하면 됩니다. withoutEvents 메서드는 클로저를 유일한 인자로 받으며, 클로저 내부에서 실행되는 코드는 어떤 모델 이벤트도 발생시키지 않습니다. 클로저가 반환하는 값은 그대로 withoutEvents 메서드의 반환값이 됩니다.

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

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

때로는 특정 모델을 "저장"하면서 어떠한 이벤트도 발생시키고 싶지 않을 수 있습니다. 이때는 saveQuietly 메서드를 사용하세요.

$user = User::findOrFail(1); $user->name = 'Victoria Faith'; $user->saveQuietly();

이와 마찬가지로 "업데이트", "삭제", "소프트 삭제", "복원", "복제" 작업 역시 이벤트를 발생시키지 않고 수행할 수 있습니다.

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

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

번역일: 2026년 9월 10일