Eloquent: 시작하기
번역일: 2026년 7월 2일
Eloquent: 시작하기
- 소개
- 모델 클래스 생성
- Eloquent 모델 규칙
- 모델 조회
- 단일 모델 / 집계 조회
- 모델 삽입 및 업데이트
- 모델 삭제
- 모델 정리(Pruning)
- 모델 복제
- 쿼리 스코프
- 모델 비교
- 이벤트
소개
Laravel에는 데이터베이스와의 상호작용을 간결하고 직관적으로 만들어주는 ORM(Object-Relational Mapper)인 Eloquent가 내장되어 있습니다. Eloquent를 사용하면 각 데이터베이스 테이블에 대응하는 모델을 정의하고, 이 모델을 통해 데이터를 조회하고 조작할 수 있습니다. SQL을 직접 작성하지 않아도 되며, 코드가 훨씬 읽기 쉬워집니다.
시작하기 전에 config/database.php에서 데이터베이스 연결을 올바르게 설정했는지 확인하세요. 데이터베이스 설정에 대한 자세한 내용은 데이터베이스 설정 문서를 참고하세요.
NOTE
Eloquent를 시작하기 전에 Active Record 패턴에 익숙하지 않다면 잠깐 살펴보는 것을 권장합니다. Active Record 패턴에서는 하나의 클래스(모델)가 데이터베이스 테이블 하나에 대응하며, 클래스의 인스턴스가 테이블의 행(row) 하나를 나타냅니다.
모델 클래스 생성
Eloquent 모델은 make:model Artisan 명령어로 생성합니다.
php artisan make:model Flight모델 생성과 함께 데이터베이스 마이그레이션도 함께 만들고 싶다면 --migration 또는 -m 옵션을 추가하세요.
php artisan make:model Flight --migration모델을 생성할 때 팩토리, 시더, 정책(Policy), 컨트롤러, Form Request 등 여러 관련 클래스를 한 번에 함께 생성할 수도 있습니다. 원하는 옵션을 조합해 사용할 수 있습니다.
<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 (리소스), Form Request 클래스 생성
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모델 정보 확인
모델의 속성(attribute)과 관계(relation)를 코드만 보고 파악하기 어려울 때가 있습니다. model:show Artisan 명령어를 사용하면 모델의 속성과 관계를 한눈에 볼 수 있습니다.
php artisan model:show FlightEloquent 모델 규칙
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는 기본 키가 자동 증가(auto-incrementing) 정수값이라고 가정하며, 기본 키를 정수형으로 자동 캐스팅합니다. 자동 증가가 아니거나 숫자가 아닌 기본 키를 사용할 경우, $incrementing 속성을 false로 설정하세요.
<?php
class Flight extends Model
{
/**
* 기본 키의 자동 증가 여부
*
* @var bool
*/
public $incrementing = false;
}기본 키가 정수형이 아닌 경우(예: UUID, 문자열 등) $keyType 속성을 'string'으로 지정하세요.
<?php
class Flight extends Model
{
/**
* 기본 키의 데이터 타입
*
* @var string
*/
protected $keyType = 'string';
}복합 기본 키
Eloquent는 각 모델이 하나의 고유한 기본 키를 가져야 한다고 가정합니다. 복합 기본 키(composite primary key)는 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' => '한국 Laravel 커뮤니티 소식']);
$article->id; // "7da inserisci-3cfe-4f2a-b5b2-f29f16e3463b"기본적으로 HasUuids 트레이트는 정렬 가능한 UUID(ordered 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를 사용하고 싶다면 HasUlids 트레이트를 사용하세요. ULID는 26자리로 UUID보다 짧으며, 정렬도 가능한 식별자입니다. 테이블에 ULID 형태의 기본 키 컬럼이 있어야 합니다.
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasUlids;
// ...
}
$article = Article::create(['title' => '한국 Laravel 커뮤니티 소식']);
$article->id; // "01gd6r360bp37zj17nxb55yv40"타임스탬프
Eloquent는 기본적으로 테이블에 created_at과 updated_at 컬럼이 있다고 가정하고, 모델을 생성하거나 업데이트할 때 이 컬럼들의 값을 자동으로 관리합니다.
타임스탬프 자동 관리가 필요 없다면 $timestamps 속성을 false로 설정하세요.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 타임스탬프 자동 관리 여부
*
* @var bool
*/
public $timestamps = false;
}타임스탬프의 저장 형식을 변경하고 싶다면 $dateFormat 속성을 사용하세요. 이 값은 PHP의 date() 함수 형식 문자열을 따르며, 데이터베이스에 저장되는 형식과 모델을 배열이나 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) 값과 동일한 형식이어야 합니다.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 속성의 기본값
*
* @var array
*/
protected $attributes = [
'options' => '[]',
'delayed' => false,
];
}Eloquent 엄격 모드 설정
Laravel은 여러 상황에서 개발자 실수를 방지할 수 있도록 엄격 모드(strictness) 옵션을 제공합니다.
preventLazyLoading 메서드는 지연 로딩(lazy loading)을 금지할지 여부를 설정합니다. 지연 로딩을 허용하는 경우에도 N+1 쿼리 문제가 있으면 로그에 기록하도록 할 수 있습니다. 일반적으로 개발 환경에서만 지연 로딩을 금지하고, 프로덕션 환경에서는 N+1 문제가 발생해도 서비스가 중단되지 않도록 false를 전달합니다.
use Illuminate\Database\Eloquent\Model;
/**
* 애플리케이션 서비스 부트스트랩
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}preventSilentlyDiscardingAttributes 메서드는 fillable에 등록되지 않은 속성을 채우려고 할 때 예외를 발생시킵니다. 이 설정은 개발 중에 예상치 못한 속성 무시로 인한 버그를 방지하는 데 유용합니다.
Model::preventSilentlyDiscardingAttributes(! $this->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 모델은 Laravel 쿼리 빌더의 모든 메서드를 사용할 수 있습니다. 쿼리 빌더 문서를 꼭 참고하세요.
모델 새로고침
이미 조회한 Eloquent 인스턴스를 데이터베이스의 최신 값으로 갱신하고 싶다면 fresh나 refresh 메서드를 사용하세요.
fresh는 데이터베이스에서 새 인스턴스를 반환하며 기존 인스턴스에는 영향을 주지 않습니다.
$flight = Flight::where('number', 'FR 900')->first();
$freshFlight = $flight->fresh();refresh는 기존 인스턴스 자체를 데이터베이스의 최신 데이터로 갱신합니다. 로드된 관계(relation)도 함께 갱신됩니다.
$flight = Flight::where('number', 'FR 900')->first();
$flight->number = 'FR 456';
$flight->refresh();
$flight->number; // "FR 900" — 데이터베이스의 값으로 되돌아감컬렉션
all이나 get 메서드는 단순한 PHP 배열이 아니라 Illuminate\Database\Eloquent\Collection 인스턴스를 반환합니다.
Eloquent 컬렉션은 Laravel의 기본 컬렉션을 확장하며, 결과 집합을 다루는 수십 가지 유용한 메서드를 제공합니다. 예를 들어 reject 메서드로 조건에 맞지 않는 항목을 걸러낼 수 있습니다.
$flights = Flight::where('destination', '인천')->get();
$flights = $flights->reject(function (Flight $flight) {
return $flight->cancelled;
});Laravel 컬렉션은 foreach로 순회할 수 있는 이터러블이며, 배열처럼 동작합니다.
foreach ($flights as $flight) {
echo $flight->name;
}Eloquent 컬렉션에 대한 더 자세한 내용은 컬렉션 문서를 참고하세요.
결과 청크 처리
all이나 get으로 수만 건의 레코드를 한 번에 가져오면 메모리 부족이 발생할 수 있습니다. 대용량 데이터를 처리할 때는 chunk 메서드를 사용해 결과를 나눠서 처리하세요.
chunk 메서드는 지정한 개수만큼 레코드를 가져와 클로저에 전달하고, 다음 배치를 가져오는 방식을 반복합니다. 메모리를 효율적으로 사용할 수 있어 대규모 데이터 처리에 적합합니다.
use App\Models\Flight;
use Illuminate\Database\Eloquent\Collection;
Flight::chunk(200, function (Collection $flights) {
foreach ($flights as $flight) {
// ...
}
});chunk의 첫 번째 인수는 청크당 레코드 수입니다. 클로저는 데이터베이스에서 각 청크를 가져올 때마다 호출됩니다.
청크를 처리하는 도중 컬럼 값을 기준으로 레코드를 필터링하면서 해당 컬럼의 값을 업데이트할 경우, 예상치 못한 결과가 발생할 수 있습니다. 이런 상황에서는 chunkById 메서드가 더 안전합니다. chunkById는 이전 청크의 마지막 기본 키를 기준으로 다음 청크를 가져오므로, 처리 도중 데이터가 변경되더라도 일관된 결과를 보장합니다.
Flight::where('departed', true)
->chunkById(200, function (Collection $flights) {
$flights->each->update(['departed' => false]);
}, column: 'id');지연 컬렉션으로 청크 처리
lazy 메서드는 내부적으로 청크 방식으로 쿼리를 실행하지만, 결과를 하나의 연속적인 LazyCollection 스트림으로 제공합니다. 덕분에 마치 단일 쿼리처럼 foreach로 순회할 수 있습니다.
use App\Models\Flight;
foreach (Flight::lazy() as $flight) {
// ...
}청크를 처리하는 도중 해당 레코드의 컬럼 값을 업데이트한다면 lazyById 또는 lazyByIdDesc 메서드를 사용하세요. 이들 메서드는 기본 키를 기준으로 안전하게 다음 배치를 가져옵니다.
Flight::where('departed', true)
->lazyById(200, column: 'id')
->each->update(['departed' => false]);커서
lazy와 유사하게, cursor 메서드는 수만 건의 레코드를 순회할 때 메모리 사용량을 크게 줄여줍니다.
cursor는 단 하나의 데이터베이스 쿼리만 실행하지만, Eloquent 모델을 한 번에 하나씩 메모리에 로드합니다. 따라서 한 시점에 메모리에는 단 하나의 Eloquent 모델만 존재합니다.
use App\Models\Flight;
foreach (Flight::where('destination', '부산')->cursor() as $flight) {
// ...
}cursor는 Illuminate\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 메모리는 절약되지만 데이터베이스 서버의 커서 리소스는 열린 채로 유지됩니다. 청크 방식(chunk, lazy)과 커서 방식의 차이를 상황에 맞게 선택하세요.
| 방식 | DB 쿼리 수 | 메모리 사용 | 특징 |
|---|---|---|---|
chunk | 여러 번 | 청크 단위 | 가장 안전, 처리 중 변경에도 안정적 |
lazy | 여러 번 (내부) | 청크 단위 | foreach 순회 편의성 |
cursor | 1번 | 1건씩 | DB 커서 유지, 단일 쿼리 |
고급 서브쿼리
서브쿼리 Select
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 등의 메서드로 단일 레코드를 조회할 수 있습니다. 이들 메서드는 컬렉션이 아닌 단일 Eloquent 모델 인스턴스를 반환합니다.
use App\Models\Flight;
// 기본 키로 조회
$flight = Flight::find(1);
// 조건에 맞는 첫 번째 레코드 조회
$flight = Flight::where('active', 1)->first();
// firstWhere: 위와 동일한 단축 표현
$flight = Flight::firstWhere('active', 1);결과가 없을 때 다른 동작을 취하고 싶다면 findOr이나 firstOr 메서드를 사용하세요. 레코드가 없으면 클로저를 실행하고, 그 반환값이 메서드의 결과가 됩니다.
$flight = Flight::findOr(1, function () {
// ...
});
$flight = Flight::where('legs', '>', 3)->firstOr(function () {
// ...
});레코드 없을 때 예외 발생
레코드가 없을 때 자동으로 404 HTTP 응답을 반환하고 싶다면 findOrFail이나 firstOrFail을 사용하세요. 레코드가 없으면 Illuminate\Database\Eloquent\ModelNotFoundException이 발생하고, 처리하지 않으면 Laravel이 자동으로 404 응답을 반환합니다.
$flight = Flight::findOrFail(1);
$flight = Flight::where('legs', '>', 3)->firstOrFail();모델 조회 또는 생성
firstOrCreate 메서드는 조건에 맞는 레코드를 찾고, 없으면 새로 생성합니다. firstOrNew는 레코드가 없으면 새 인스턴스를 반환하지만 저장하지는 않습니다. 저장하려면 save를 직접 호출해야 합니다.
use App\Models\Flight;
// 'name'으로 조회하고 없으면 생성
$flight = Flight::firstOrCreate([
'name' => '김포-제주 노선',
]);
// 'name'으로 조회하고 없으면 추가 속성과 함께 생성
$flight = Flight::firstOrCreate(
['name' => '김포-제주 노선'],
['delayed' => 1, 'arrival_time' => '11:30']
);
// 'name'으로 조회하고 없으면 인스턴스 반환 (저장 안 함)
$flight = Flight::firstOrNew([
'name' => '김포-제주 노선',
]);
// 'name'으로 조회하고 없으면 추가 속성과 함께 인스턴스 반환
$flight = Flight::firstOrNew(
['name' => '김포-제주 노선'],
['delayed' => 1, 'arrival_time' => '11:30']
);집계 조회
Eloquent 모델을 사용할 때도 Laravel 쿼리 빌더의 count, sum, max 등 집계 메서드를 그대로 사용할 수 있습니다. 이들 메서드는 Eloquent 모델 인스턴스가 아닌 스칼라 값을 반환합니다.
$count = Flight::where('active', 1)->count();
$max = Flight::where('active', 1)->max('duration');모델 삽입 및 업데이트
삽입
Eloquent를 사용하면 SQL 쿼리를 직접 작성하지 않고도 데이터를 삽입할 수 있습니다. 새 레코드를 삽입하려면 새 모델 인스턴스를 만들고 속성을 설정한 뒤 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_at과 updated_at은 자동으로 설정되므로 직접 지정하지 않아도 됩니다.
create 메서드를 사용하면 한 줄로 저장하고 인스턴스를 반환받을 수 있습니다. 단, 이 방식은 대량 할당(Mass Assignment) 설정이 필요합니다.
use App\Models\Flight;
$flight = Flight::create([
'name' => '김포-부산 노선',
]);업데이트
이미 존재하는 모델을 업데이트할 때도 save를 사용합니다. 인스턴스를 조회하고 속성을 변경한 뒤 save를 호출하면 됩니다. updated_at은 자동으로 갱신됩니다.
use App\Models\Flight;
$flight = Flight::find(1);
$flight->name = '파리-런던 노선';
$flight->save();대량 업데이트
여러 레코드를 한 번에 업데이트하려면 쿼리 빌더 방식을 사용합니다. 아래는 활성 상태이며 목적지가 특정 공항인 항공편을 모두 지연(delayed) 처리하는 예시입니다.
Flight::where('active', 1)
->where('destination', 'ICN')
->update(['delayed' => 1]);update 메서드는 컬럼명과 값의 배열을 받아 UPDATE 쿼리를 실행합니다.
WARNING
Eloquent의 대량 업데이트 시에는 saving, saved, updating, updated 모델 이벤트가 발생하지 않습니다. 대량 업데이트는 모델 인스턴스를 조회하지 않고 직접 쿼리를 실행하기 때문입니다.
속성 변경 사항 확인
Eloquent는 모델의 내부 상태를 추적할 수 있도록 isDirty, isClean, wasChanged 메서드를 제공합니다.
isDirty는 마지막으로 조회한 이후 속성이 변경되었는지 확인합니다. 특정 속성명을 전달해 해당 속성만 확인할 수도 있습니다. isClean은 반대로 변경이 없는지 확인합니다.
use App\Models\User;
$user = User::create([
'first_name' => '김',
'last_name' => '철수',
'title' => '개발자',
]);
$user->title = '시니어 개발자';
$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(); // truewasChanged는 현재 요청 사이클 내에서 마지막으로 save된 이후 속성이 변경되었는지 확인합니다.
$user = User::create([
'first_name' => '김',
'last_name' => '철수',
'title' => '개발자',
]);
$user->title = '시니어 개발자';
$user->save();
$user->wasChanged(); // true
$user->wasChanged('title'); // true
$user->wasChanged(['title', 'last_name']); // true
$user->wasChanged('first_name'); // falsegetOriginal 메서드는 변경과 무관하게 모델의 원래 속성값을 배열로 반환합니다.
$user = User::find(1);
$user->name = '이영희';
$user->getOriginal('name'); // 홍길동
$user->getOriginal(); // 원래 속성 배열 전체대량 할당
create 메서드를 사용하면 배열로 한 번에 여러 속성을 지정해 모델을 저장할 수 있습니다. 이 방식을 **대량 할당(Mass Assignment)**이라고 합니다.
단, 악의적인 사용자가 임의의 HTTP 파라미터를 전달해 의도치 않은 컬럼 값을 바꾸는 대량 할당 취약점이 발생할 수 있습니다. 예를 들어, is_admin 컬럼 값을 사용자가 임의로 바꿀 수 있는 상황이 이에 해당합니다.
이를 방지하기 위해 Eloquent는 모든 속성을 기본적으로 대량 할당 불가 상태로 설정합니다. 대량 할당을 허용할 속성은 $fillable 속성에 명시해야 합니다.
<?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' => '김포-제주 직항']);반대로 $guarded 속성을 사용하면 대량 할당을 금지할 속성을 지정할 수 있습니다. $guarded에 없는 속성은 모두 대량 할당이 허용됩니다.
/**
* 대량 할당을 금지할 속성 목록
*
* @var array<int, string>
*/
protected $guarded = ['price'];$guarded를 빈 배열로 설정하면 모든 속성에 대량 할당을 허용합니다.
protected $guarded = [];WARNING
$guarded = []로 설정하면 모든 속성이 대량 할당 가능해집니다. 사용자 입력을 그대로 전달하지 않도록 각별히 주의하세요.
대량 할당 예외 처리
기본적으로 $fillable에 없는 속성은 대량 할당 시 조용히 무시됩니다. 프로덕션 환경에서는 이 동작이 적절하지만, 개발 환경에서는 왜 변경이 반영되지 않는지 혼란스러울 수 있습니다.
preventSilentlyDiscardingAttributes 메서드를 사용하면 허용되지 않은 속성에 대량 할당 시 예외를 발생시킬 수 있습니다. 보통 서비스 프로바이더의 boot 메서드에 설정합니다.
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());Upsert
레코드가 있으면 업데이트하고 없으면 삽입하는 Upsert 작업은 upsert 메서드로 수행할 수 있습니다.
- 첫 번째 인수: 삽입하거나 업데이트할 값 배열
- 두 번째 인수: 레코드를 고유하게 식별하는 컬럼(들)
- 세 번째 인수: 레코드가 이미 존재할 때 업데이트할 컬럼(들)
Flight::upsert([
['departure' => '서울', 'destination' => '제주', 'price' => 89000],
['departure' => '서울', 'destination' => '부산', 'price' => 59000],
], uniqueBy: ['departure', 'destination'], update: ['price']);WARNING
SQL Server를 제외한 모든 데이터베이스에서 upsert의 두 번째 인수 컬럼은 "primary" 또는 "unique" 인덱스가 있어야 합니다. 또한 MySQL 드라이버는 두 번째 인수를 무시하고 항상 테이블의 "primary" 및 "unique" 인덱스로 기존 레코드를 판별합니다.
모델 삭제
모델 인스턴스의 delete 메서드를 호출하면 해당 레코드가 삭제됩니다.
use App\Models\Flight;
$flight = Flight::find(1);
$flight->delete();기본 키로 삭제
모델을 먼저 조회하지 않고 기본 키로 바로 삭제하려면 destroy 메서드를 사용하세요. 단일 키, 여러 키, 배열, 컬렉션 모두 지원합니다.
Flight::destroy(1);
Flight::destroy(1, 2, 3);
Flight::destroy([1, 2, 3]);
Flight::destroy(collect([1, 2, 3]));WARNING
destroy 메서드는 각 모델을 개별적으로 조회한 뒤 delete를 호출하므로, deleting과 deleted 이벤트가 정상적으로 발생합니다.
쿼리로 일괄 삭제
조건에 맞는 모든 레코드를 한 번에 삭제하려면 쿼리를 작성한 뒤 delete를 호출하세요.
$deleted = Flight::where('active', 0)->delete();WARNING
쿼리를 통한 일괄 삭제는 모델 인스턴스를 조회하지 않으므로 deleting과 deleted 모델 이벤트가 발생하지 않습니다. 이벤트 처리가 필요하다면 각 모델을 개별 조회 후 삭제하세요.
소프트 삭제
데이터베이스에서 레코드를 실제로 삭제하는 대신 삭제된 시각만 기록해두는 방식을 소프트 삭제라고 합니다. 소프트 삭제된 레코드는 일반 쿼리에서는 자동으로 제외되지만 완전히 사라지지는 않으므로, 나중에 복구하거나 감사 추적에 활용할 수 있습니다.
소프트 삭제를 활성화하려면 모델에 Illuminate\Database\Eloquent\SoftDeletes 트레이트를 추가하세요.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Flight extends Model
{
use SoftDeletes;
}테이블에는 deleted_at 컬럼이 필요합니다. 마이그레이션에서 softDeletes 헬퍼를 사용하면 됩니다.
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()) {
// 소프트 삭제된 상태
}소프트 삭제 복원
소프트 삭제된 모델을 복원하려면 restore 메서드를 사용하세요. deleted_at이 null로 설정됩니다.
$flight->restore();쿼리를 통해 여러 모델을 한 번에 복원할 수도 있습니다.
Flight::withTrashed()
->where('airline_id', 1)
->restore();관계(relation) 쿼리에서도 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 메서드를 정의하면, 레코드 삭제 전에 추가 작업(예: 파일 삭제)을 수행할 수 있습니다.
/**
* 모델 정리 전 처리 작업
*/
protected function pruning(): void
{
// 연관 파일 삭제 등 추가 처리
}Prunable 모델 설정 후, app/Console/Kernel.php에 model:prune Artisan 명령어를 스케줄에 등록하세요.
use Illuminate\Support\Facades\Schedule;
Schedule::command('model:prune')->daily();model:prune 명령어는 app/Models 디렉터리에서 Prunable 모델을 자동으로 탐지합니다. 모델 경로가 다른 경우 --model 옵션으로 지정할 수 있습니다.
php artisan model:prune --model="App\Models\Flight"특정 모델만 제외하려면 --except 옵션을 사용하세요.
php artisan model:prune --except="App\Models\Flight"--pretend 옵션으로 실제 삭제 없이 몇 건이 삭제될지 미리 확인할 수 있습니다.
php artisan model:prune --pretendWARNING
소프트 삭제 모델도 prunable 쿼리에 해당하면 영구 삭제(forceDelete)됩니다.
대량 정리
MassPrunable 트레이트를 사용하면 모델 인스턴스를 개별 조회하지 않고 대량 DELETE 쿼리로 빠르게 삭제합니다. 이 경우 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());
}
}모델 복제
기존 모델 인스턴스의 저장되지 않은 복사본을 만들려면 replicate 메서드를 사용하세요. 동일한 속성을 많이 공유하는 모델 인스턴스를 여러 개 만들어야 할 때 유용합니다.
use App\Models\Address;
$shipping = Address::create([
'type' => 'shipping',
'line_1' => '서울특별시 강남구 테헤란로 123',
'city' => '서울',
'state' => '서울특별시',
'zip' => '06234',
]);
$billing = $shipping->replicate()->fill([
'type' => 'billing',
]);
$billing->save();복제 시 제외할 속성은 replicate에 배열로 전달하세요.
$flight = Flight::create([
'destination' => '제주',
'origin' => '서울',
'last_flown' => '2023-01-05 22:00:00',
'last_pilot_id' => 747,
]);
$flight = $flight->replicate([
'last_flown',
'last_pilot_id',
]);쿼리 스코프
쿼리 스코프를 사용하면 자주 쓰는 쿼리 조건을 모델에 메서드로 정의해 재사용할 수 있습니다. 코드 중복을 줄이고 가독성을 높이는 데 효과적입니다.
글로벌 스코프
글로벌 스코프는 해당 모델의 모든 쿼리에 자동으로 적용되는 조건입니다. Laravel의 소프트 삭제 기능이 글로벌 스코프를 활용한 대표적인 예시입니다(deleted_at IS NULL 조건이 자동으로 붙습니다).
글로벌 스코프 정의
글로벌 스코프는 Illuminate\Database\Eloquent\Scope 인터페이스를 구현한 클래스로 만들거나, 모델의 booted 메서드에서 클로저로 등록할 수 있습니다.
클래스 방식의 경우 apply 메서드에서 쿼리 빌더에 조건을 추가합니다.
<?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 절을 덮어쓰는 문제를 방지할 수 있습니다.
글로벌 스코프 적용
글로벌 스코프를 모델에 적용하려면 모델의 booted 메서드를 오버라이드하고 addGlobalScope를 호출하세요.
<?php
namespace App\Models;
use App\Models
<h1 id="global-scopes">Eloquent: 시작하기</h1>
<h2 id="generating-scopes">소개</h2>
Laravel에는 데이터베이스와의 상호작용을 즐겁게 만들어 주는 ORM(Object-Relational Mapper)인 Eloquent가 내장되어 있습니다. Eloquent를 사용하면 각 데이터베이스 테이블에 대응하는 **모델** 클래스를 통해 데이터를 조회, 삽입, 수정, 삭제할 수 있습니다.
> [!NOTE]
> 시작하기 전에 `config/database.php` 설정 파일에서 데이터베이스 연결을 먼저 구성해야 합니다. 데이터베이스 설정에 대한 자세한 내용은 [데이터베이스 설정 문서](/docs/10.x/database/database#configuration)를 참고하세요.
<h4 id="writing-global-scopes">Laravel Bootcamp</h4>
Laravel과 Eloquent를 처음 접한다면 [Laravel Bootcamp](https://bootcamp.laravel.com)를 먼저 살펴보는 것을 추천합니다. Bootcamp에서는 Eloquent를 활용해 첫 번째 Laravel 애플리케이션을 직접 만들어 보면서 Laravel의 핵심 기능을 자연스럽게 익힐 수 있습니다.
<h2 id="applying-global-scopes">모델 클래스 생성하기</h2>
Eloquent 모델은 보통 `app/Models` 디렉터리에 위치하며, `Illuminate\Database\Eloquent\Model` 클래스를 상속합니다. 다음 Artisan 명령어로 새 모델을 생성할 수 있습니다:
```shell
php artisan make:model Flight모델을 생성할 때 데이터베이스 마이그레이션도 함께 만들고 싶다면 --migration 또는 -m 옵션을 추가하세요:
php artisan make:model Flight --migration모델 생성 시 팩토리, 시더, 정책(Policy), 컨트롤러, 폼 리퀘스트 등 다양한 관련 클래스를 함께 생성할 수도 있습니다. 옵션을 조합하면 여러 클래스를 한 번에 만들 수 있어 초기 세팅이 훨씬 편리합니다:
<h1 id="anonymous-global-scopes">모델과 FlightFactory 클래스 생성...</h1>
php artisan make:model Flight --factory
php artisan make:model Flight -f
<h1 id="removing-global-scopes">모델과 FlightSeeder 클래스 생성...</h1>
php artisan make:model Flight --seed
php artisan make:model Flight -s
<h1 id="local-scopes">모델과 FlightController 클래스 생성...</h1>
php artisan make:model Flight --controller
php artisan make:model Flight -c
<h1 id="utilizing-a-local-scope">모델, FlightController 리소스 클래스, 폼 리퀘스트 클래스 생성...</h1>
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR
<h1 id="dynamic-scopes">모델과 FlightPolicy 클래스 생성...</h1>
php artisan make:model Flight --policy
<h1 id="comparing-models">모델, 마이그레이션, 팩토리, 시더, 컨트롤러 한 번에 생성...</h1>
php artisan make:model Flight -mfsc
<h1 id="events">모델, 마이그레이션, 팩토리, 시더, 정책, 컨트롤러, 폼 리퀘스트 모두 생성...</h1>
php artisan make:model Flight --all
<h1 id="events-using-closures">피벗 모델 생성...</h1>
php artisan make:model Member --pivot
php artisan make:model Member -p모델 정보 확인하기
모델 코드만 훑어보는 것만으로는 사용 가능한 속성과 관계(Relationship)를 한눈에 파악하기 어려울 때가 있습니다. 이럴 때는 model:show Artisan 명령어를 사용하면 해당 모델의 속성과 관계를 깔끔하게 정리해서 보여줍니다:
php artisan model:show FlightEloquent 모델 컨벤션
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라는 이름의 기본 키 컬럼이 존재한다고 가정합니다. 다른 컬럼을 기본 키로 사용하려면 $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는 각 모델에 단일 기본 키(고유 식별자)가 있어야 합니다. 복합 기본 키(composite primary keys)는 Eloquent 모델에서 지원하지 않습니다. 다만, 기본 키 외에 여러 컬럼을 조합한 고유 인덱스(unique index)를 추가하는 것은 자유롭게 할 수 있습니다.
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는 사전순(lexicographically) 정렬이 가능하므로 인덱스 성능에 유리합니다.
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를 사용하려면 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_at과 updated_at 컬럼이 존재한다고 가정하며, 모델이 생성되거나 수정될 때 이 컬럼들을 자동으로 관리합니다.
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_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 = 'sqlite';
}속성 기본값
새로 인스턴스화한 모델은 기본적으로 아무런 속성 값을 갖지 않습니다. 일부 속성에 기본값을 지정하려면 $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의 동작 방식과 엄격성(strictness)을 설정할 수 있는 몇 가지 메서드를 제공합니다.
지연 로딩 방지
preventLazyLoading 메서드는 지연 로딩(lazy loading)을 차단할지 여부를 결정합니다. 예를 들어, 프로덕션 환경에서는 기존 동작을 유지하면서 개발 환경에서만 지연 로딩을 비활성화하는 방식으로 설정할 수 있습니다. 보통 AppServiceProvider의 boot 메서드에서 호출합니다.
use Illuminate\Database\Eloquent\Model;
/**
* 애플리케이션 서비스 부트스트랩
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}fillable에 없는 속성 할당 시 예외 발생
preventSilentlyDiscardingAttributes 메서드를 사용하면, 모델의 fillable 배열에 없는 속성을 채우려 할 때 예외를 발생시킵니다. 로컬 개발 중 실수로 fillable에 추가하지 않은 속성을 설정하려는 경우를 조기에 발견하는 데 유용합니다.
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());NOTE
preventLazyLoading과 preventSilentlyDiscardingAttributes 모두 프로덕션 환경(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 모델 인스턴스가 있을 때, fresh와 refresh 메서드로 최신 데이터로 갱신할 수 있습니다.
fresh 메서드는 데이터베이스에서 모델을 다시 조회해 새 인스턴스를 반환합니다. 기존 인스턴스는 영향을 받지 않습니다:
$flight = Flight::where('number', 'KE 001')->first();
$freshFlight = $flight->fresh();refresh 메서드는 기존 인스턴스 자체를 데이터베이스의 최신 데이터로 다시 채웁니다. 이미 로드된 관계(relationship)도 함께 갱신됩니다:
$flight = Flight::where('number', 'KE 001')->first();
$flight->number = 'KE 999';
$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의 이터러블 인터페이스를 구현하므로, 배열처럼 반복문을 사용할 수 있습니다:
foreach ($flights as $flight) {
echo $flight->name;
}결과 청크 처리
all이나 get으로 수만 건의 레코드를 한 번에 불러오면 메모리가 부족해질 수 있습니다. 이런 경우에는 chunk 메서드를 사용해 대용량 데이터를 효율적으로 처리하세요.
chunk 메서드는 모델을 일정 단위로 나누어 조회하고, 각 묶음을 클로저에 전달합니다. 한 번에 하나의 청크만 메모리에 올리기 때문에 메모리 사용량이 크게 줄어듭니다:
use App\Models\Flight;
use Illuminate\Database\Eloquent\Collection;
Flight::chunk(200, function (Collection $flights) {
foreach ($flights as $flight) {
// ...
}
});첫 번째 인자는 한 번에 가져올 레코드 수이고, 두 번째 인자인 클로저는 각 청크가 조회될 때마다 호출됩니다.
WARNING
청크를 반복하는 도중 필터링에 사용한 컬럼의 값을 함께 업데이트하는 경우, chunk 대신 chunkById를 사용해야 합니다. chunk를 사용하면 페이지네이션이 엇갈려 예상치 못한 결과가 발생할 수 있습니다. chunkById는 내부적으로 이전 청크의 마지막 id보다 큰 레코드만 조회하므로 안전합니다:
Flight::where('departed', true)
->chunkById(200, function (Collection $flights) {
$flights->each->update(['departed' => false]);
}, $column = 'id');지연 컬렉션을 활용한 청크 처리
lazy 메서드는 내부적으로 chunk처럼 청크 단위로 쿼리를 실행하지만, 각 청크를 클로저에 따로 전달하는 대신 하나로 이어진 LazyCollection 스트림으로 반환합니다. 덕분에 대용량 데이터를 하나의 연속된 흐름처럼 다룰 수 있습니다:
use App\Models\Flight;
foreach (Flight::lazy() as $flight) {
// ...
}반복 중 필터링한 컬럼을 동시에 업데이트해야 한다면 lazyById를 사용하세요. 내부적으로 이전 청크의 마지막 id보다 큰 레코드만 순차적으로 가져옵니다:
Flight::where('departed', true)
->lazyById(200, $column = 'id')
->each->update(['departed' => false]);id의 내림차순으로 결과를 필터링해야 할 경우에는 lazyByIdDesc 메서드를 사용할 수 있습니다.
커서
cursor 메서드도 수만 건의 레코드를 처리할 때 메모리 소비를 크게 줄여줍니다.
cursor는 데이터베이스에 쿼리를 단 한 번만 실행하고, 실제로 반복할 때 비로소 각 Eloquent 모델을 인스턴스화합니다. 따라서 어느 시점에서든 메모리에는 Eloquent 모델 하나만 올라와 있습니다.
WARNING
cursor는 한 번에 모델 하나만 메모리에 유지하므로, 관계(relationship)를 즉시 로드(eager load)할 수 없습니다. 관계를 함께 로드해야 한다면 lazy 메서드를 사용하세요.
내부적으로 cursor는 PHP 제너레이터(generator)를 활용해 구현됩니다:
use App\Models\Flight;
foreach (Flight::where('destination', '제주')->cursor() as $flight) {
// ...
}cursor는 Illuminate\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 메서드를 우선적으로 고려하세요.
아래 표는 대용량 데이터 처리 시 각 방법의 특성을 비교한 것입니다:
| 메서드 | DB 쿼리 횟수 | 메모리 사용 | 관계 즉시 로드 |
|---|---|---|---|
chunk | 청크 수만큼 | 청크 단위 | 가능 |
lazy | 청크 수만큼 | 청크 단위 | 가능 |
cursor | 1회 | 모델 1개 | 불가 |
고급 서브쿼리
서브쿼리 SELECT
Eloquent는 관련 테이블의 정보를 단일 쿼리로 가져올 수 있는 고급 서브쿼리 기능을 지원합니다. 예를 들어, 항공편 destinations(목적지) 테이블과 flights(항공편) 테이블이 있고, flights 테이블에는 도착 시각을 나타내는 arrived_at 컬럼이 있다고 가정해 보겠습니다.
쿼리 빌더의 select와 addSelect 메서드에서 서브쿼리를 활용하면, 모든 목적지와 그 목적지에 가장 최근에 도착한 항공편의 이름을 단일 쿼리로 조회할 수 있습니다:
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);결과가 없을 때 특정 동작을 수행하고 싶다면 findOr나 firstOr 메서드를 사용하세요. 이 메서드들은 결과가 없을 경우 전달한 클로저를 실행하고, 클로저의 반환값을 결과로 사용합니다.
$flight = Flight::findOr(1, function () {
// 결과가 없을 때 실행할 코드
});
$flight = Flight::where('legs', '>', 3)->firstOr(function () {
// 결과가 없을 때 실행할 코드
});모델을 찾지 못했을 때 예외 발생
라우트나 컨트롤러에서 모델을 찾지 못했을 때 예외를 던지고 싶다면 findOrFail이나 firstOrFail 메서드를 사용하세요. 결과가 없으면 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 라우트에서 특정 리소스를 조회할 때 findOrFail을 활용하면 별도의 예외 처리 코드 없이도 깔끔하게 404 응답을 처리할 수 있습니다.
모델 조회 또는 생성
firstOrCreate 메서드는 전달한 컬럼/값 쌍으로 데이터베이스 레코드를 찾으려 시도합니다. 모델을 찾지 못하면 첫 번째 배열 인수와 선택적인 두 번째 배열 인수를 합쳐 새 레코드를 데이터베이스에 삽입합니다.
firstOrNew 메서드도 동일하게 데이터베이스에서 레코드를 찾지만, 찾지 못했을 때 데이터베이스에 저장하지 않고 새 모델 인스턴스만 반환합니다. 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']
);NOTE
firstOrCreate는 즉시 DB에 저장하고, firstOrNew는 인스턴스만 만들어 반환한다는 차이를 기억하세요. 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_at과 updated_at 타임스탬프는 save 호출 시 자동으로 설정되므로 별도로 지정할 필요가 없습니다.
또는 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();대량 업데이트
특정 쿼리 조건에 일치하는 여러 모델을 한 번에 업데이트할 수도 있습니다. 아래 예제는 active 상태이면서 목적지가 부산인 모든 항공편을 지연(delayed) 상태로 변경합니다.
Flight::where('active', 1)
->where('destination', '부산')
->update(['delayed' => 1]);update 메서드는 컬럼과 값의 쌍으로 이루어진 배열을 인수로 받으며, 실제로 변경된 행의 수를 반환합니다.
WARNING
Eloquent를 통해 대량 업데이트를 수행할 경우, 업데이트 대상 모델에 대해 saving, saved, updating, updated 모델 이벤트가 발생하지 않습니다. 대량 업데이트 시에는 모델을 실제로 조회하지 않기 때문입니다.
속성 변경 여부 확인
Eloquent는 모델의 내부 상태를 검사하고 속성이 어떻게 변경되었는지 확인할 수 있는 isDirty, isClean, wasChanged 메서드를 제공합니다.
isDirty: 모델을 조회한 이후 속성이 변경되었는지 확인합니다.isClean: 모델을 조회한 이후 속성이 변경되지 않았는지 확인합니다.wasChanged: 현재 요청 사이클 내에서 마지막으로 저장될 때 속성이 변경되었는지 확인합니다.
각 메서드에 특정 속성명 또는 속성명 배열을 전달하여 해당 속성에 대해서만 확인할 수도 있습니다.
use App\Models\User;
$user = User::create([
'first_name' => '길동',
'last_name' => '홍',
'title' => '개발자',
]);
$user->title = '디자이너';
$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(); // truewasChanged는 save 이후에 실제로 변경된 속성을 확인합니다.
$user = User::create([
'first_name' => '길동',
'last_name' => '홍',
'title' => '개발자',
]);
$user->title = '디자이너';
$user->save();
$user->wasChanged(); // true
$user->wasChanged('title'); // true
$user->wasChanged(['title', 'slug']); // true
$user->wasChanged('first_name'); // false
$user->wasChanged(['first_name', 'title']); // truegetOriginal 메서드는 모델을 조회했을 당시의 원래 속성값을 배열로 반환합니다. 특정 속성명을 전달하면 해당 속성의 원래 값만 반환합니다.
$user = User::find(1);
$user->name; // 길동
$user->email; // gildong@example.com
$user->name = '철수';
$user->name; // 철수
$user->getOriginal('name'); // 길동
$user->getOriginal(); // 원래 속성값 배열...대량 할당 (Mass Assignment)
앞서 소개한 create 메서드를 사용하면 단 한 줄로 새 모델을 데이터베이스에 저장할 수 있습니다.
use App\Models\Flight;
$flight = Flight::create([
'name' => '서울 to 오사카',
]);그러나 create 메서드를 사용하기 전에 모델에 $fillable 또는 $guarded 속성을 반드시 정의해야 합니다. Eloquent 모델은 기본적으로 대량 할당 취약점으로부터 보호되어 있기 때문입니다.
대량 할당 취약점이란? 악의적인 사용자가 예상치 못한 HTTP 요청 필드(예: is_admin)를 전송하고, 이 값이 그대로 모델의 create 메서드에 전달될 경우 데이터베이스의 의도하지 않은 컬럼이 변경될 수 있습니다. 예를 들어 일반 사용자가 is_admin=1을 요청에 포함시켜 관리자 권한을 탈취하는 상황이 이에 해당합니다.
이를 방지하기 위해 대량 할당을 허용할 속성을 $fillable에 명시적으로 지정합니다.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 대량 할당을 허용할 속성 목록.
*
* @var array
*/
protected $fillable = ['name'];
}$fillable을 지정한 후에는 create 메서드로 레코드를 삽입할 수 있습니다. 새로 생성된 모델 인스턴스가 반환됩니다.
$flight = Flight::create(['name' => '서울 to 오사카']);이미 모델 인스턴스가 있는 경우에는 fill 메서드를 사용해 속성을 일괄 설정할 수 있습니다.
$flight->fill(['name' => '인천 to 프랑크푸르트']);대량 할당과 JSON 컬럼
JSON 컬럼에 대량 할당을 사용할 때는 각 컬럼의 키를 $fillable 배열에 명시해야 합니다. 보안상의 이유로, Laravel은 $guarded를 사용할 때 중첩된 JSON 속성 업데이트를 지원하지 않습니다.
/**
* 대량 할당을 허용할 속성 목록.
*
* @var array
*/
protected $fillable = [
'options->enabled',
];대량 할당 전체 허용
모든 속성에 대해 대량 할당을 허용하려면 $guarded를 빈 배열로 정의하면 됩니다. 단, 이 경우 fill, create, update 메서드에 전달하는 배열을 항상 신중하게 직접 구성해야 합니다.
/**
* 대량 할당을 차단할 속성 목록.
*
* @var array
*/
protected $guarded = [];대량 할당 예외 처리
기본적으로 $fillable에 포함되지 않은 속성은 대량 할당 시 조용히 무시됩니다. 프로덕션 환경에서는 이것이 의도된 동작이지만, 로컬 개발 환경에서는 값이 왜 반영되지 않는지 파악하기 어려울 수 있습니다.
preventSilentlyDiscardingAttributes 메서드를 호출하면, 허용되지 않은 속성에 대해 할당을 시도할 때 예외가 발생하도록 설정할 수 있습니다. 이 메서드는 보통 서비스 프로바이더의 boot 메서드에서 호출합니다.
use Illuminate\Database\Eloquent\Model;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}NOTE
위 설정은 로컬 환경(isLocal())에서만 예외를 발생시키도록 구성하는 것이 일반적입니다. 프로덕션에서 예외가 발생하면 서비스 장애로 이어질 수 있으므로 주의하세요.
Upsert (업서트)
기존 레코드가 있으면 업데이트하고, 없으면 새로 생성해야 하는 경우가 종종 있습니다. updateOrCreate 메서드는 모델을 자동으로 저장하므로 별도로 save를 호출할 필요가 없습니다.
아래 예제에서, departure가 인천이고 destination이 오사카인 항공편이 이미 존재하면 price와 discounted 컬럼이 업데이트됩니다. 해당 항공편이 없으면, 첫 번째 인수 배열과 두 번째 인수 배열을 합친 속성으로 새 레코드가 생성됩니다.
$flight = Flight::updateOrCreate(
['departure' => '인천', 'destination' => '오사카'],
['price' => 99, 'discounted' => 1]
);여러 건의 업서트를 단일 쿼리로 처리하려면 upsert 메서드를 사용하세요. 첫 번째 인수는 삽입하거나 업데이트할 값의 배열, 두 번째 인수는 레코드를 고유하게 식별하는 컬럼 목록, 세 번째 인수는 레코드가 이미 존재할 때 업데이트할 컬럼 목록입니다. 모델에 타임스탬프가 활성화되어 있으면 created_at과 updated_at이 자동으로 설정됩니다.
Flight::upsert([
['departure' => '인천', 'destination' => '오사카', 'price' => 99],
['departure' => '김포', 'destination' => '제주', 'price' => 150]
], ['departure', 'destination'], ['price']);WARNING
SQL Server를 제외한 모든 데이터베이스는 upsert 메서드의 두 번째 인수에 지정된 컬럼에 "primary" 또는 "unique" 인덱스가 있어야 합니다. 또한 MySQL 데이터베이스 드라이버는 upsert의 두 번째 인수를 무시하고, 테이블의 "primary" 및 "unique" 인덱스를 기준으로 기존 레코드를 감지합니다.
Eloquent: 시작하기
모델 삭제
모델 인스턴스에서 delete 메서드를 호출하면 해당 레코드를 삭제할 수 있습니다.
use App\Models\Flight;
$flight = Flight::find(1);
$flight->delete();모델과 연결된 테이블의 모든 레코드를 삭제하려면 truncate 메서드를 사용합니다. truncate는 테이블의 레코드를 모두 지우는 동시에 자동 증가(auto-increment) ID도 초기화합니다.
Flight::truncate();기본 키로 모델 삭제하기
위 예시에서는 먼저 데이터베이스에서 모델을 조회한 뒤 delete를 호출했습니다. 하지만 기본 키를 이미 알고 있다면, 굳이 모델을 먼저 조회하지 않아도 됩니다. destroy 메서드를 사용하면 단일 기본 키, 여러 기본 키, 배열, 또는 컬렉션 형태로 한 번에 삭제할 수 있습니다.
Flight::destroy(1);
Flight::destroy(1, 2, 3);
Flight::destroy([1, 2, 3]);
Flight::destroy(collect([1, 2, 3]));WARNING
destroy 메서드는 각 모델을 개별적으로 로드한 뒤 delete 메서드를 호출합니다. 따라서 모델마다 deleting, deleted 이벤트가 정상적으로 발생합니다.
쿼리를 사용한 모델 삭제
특정 조건에 맞는 모델을 대량으로 삭제하는 쿼리를 작성할 수도 있습니다. 아래 예시는 비활성 상태인 항공편을 모두 삭제합니다. 대량 업데이트와 마찬가지로, 대량 삭제 시에는 각 모델에 대한 이벤트가 발생하지 않습니다.
$deleted = Flight::where('active', 0)->delete();WARNING
Eloquent로 대량 삭제를 실행하면, 삭제된 모델에 대해 deleting, deleted 이벤트가 발생하지 않습니다. 삭제 구문 실행 시 실제로 모델 인스턴스를 조회하지 않기 때문입니다.
소프트 삭제 (Soft Delete)
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()) {
// ...
}소프트 삭제된 모델 복원하기
소프트 삭제된 모델을 다시 복원하려면 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
{
// 연관 파일 삭제 등 추가 정리 작업을 여기서 수행합니다.
}모델 설정이 끝났으면, 애플리케이션의 App\Console\Kernel 클래스에서 model:prune Artisan 커맨드를 스케줄에 등록합니다. 실행 주기는 프로젝트 상황에 맞게 자유롭게 설정하면 됩니다:
/**
* 애플리케이션의 커맨드 스케줄을 정의합니다.
*/
protected function schedule(Schedule $schedule): void
{
$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();실제 삭제 없이 몇 건이 정리될지 미리 확인하려면 --pretend 옵션을 사용하세요:
php artisan model:prune --pretendWARNING
소프트 삭제(Soft Delete)를 사용하는 모델이 정리 쿼리 조건에 해당하면, forceDelete로 영구 삭제됩니다.
대량 정리(Mass Pruning)
Illuminate\Database\Eloquent\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());
}
}NOTE
Prunable과 MassPrunable의 선택 기준은 간단합니다. 삭제 전에 파일 제거나 이벤트 처리 등 부가 작업이 필요하다면 Prunable을, 단순히 DB 레코드만 빠르게 대량 삭제한다면 MassPrunable을 사용하세요.
모델 복제
기존 모델 인스턴스의 저장되지 않은 복사본을 만들려면 replicate 메서드를 사용하세요. 이 메서드는 여러 속성이 동일한 모델 인스턴스를 생성할 때 특히 유용합니다.
use App\Models\Address;
$shipping = Address::create([
'type' => 'shipping',
'line_1' => '서울특별시 강남구 테헤란로 123',
'city' => '서울',
'state' => '경기',
'postcode' => '06134',
]);
$billing = $shipping->replicate()->fill([
'type' => 'billing'
]);
$billing->save();복제 시 특정 속성을 제외하려면, 제외할 속성명을 배열로 replicate 메서드에 전달하세요.
$flight = Flight::create([
'destination' => 'ICN',
'origin' => 'NRT',
'last_flown' => '2024-03-04 11:00:00',
'last_pilot_id' => 747,
]);
$flight = $flight->replicate([
'last_flown',
'last_pilot_id'
]);NOTE
replicate로 생성된 인스턴스는 아직 데이터베이스에 저장되지 않은 상태입니다. 필요한 속성을 수정한 뒤 반드시 save()를 호출해야 합니다.
쿼리 스코프
글로벌 스코프
글로벌 스코프(Global Scope)를 사용하면 특정 모델의 모든 쿼리에 공통 조건을 자동으로 추가할 수 있습니다. 예를 들어 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);
}
}위와 같이 App\Models\User 모델에 스코프를 적용한 뒤 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();로컬 스코프
로컬 스코프(Local Scope)는 특정 모델에서 자주 사용하는 쿼리 조건을 메서드로 정의해 두고 재사용하는 기능입니다. 예를 들어 "인기 있는 사용자"나 "활성 사용자"처럼 반복적으로 쓰는 조건을 스코프로 만들어 두면 코드가 훨씬 간결해집니다.
로컬 스코프는 모델 메서드 이름 앞에 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은 이를 위한 "고차(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();모델 비교
두 모델 인스턴스가 "같은" 모델인지 확인해야 할 때가 있습니다. is와 isNot 메서드를 사용하면 두 모델의 기본 키, 테이블, 데이터베이스 커넥션이 모두 동일한지 빠르게 검사할 수 있습니다.
if ($post->is($anotherPost)) {
// ...
}
if ($post->isNot($anotherPost)) {
// ...
}is와 isNot 메서드는 belongsTo, hasOne, morphTo, morphOne 관계를 사용할 때도 동일하게 활용할 수 있습니다. 연관 모델을 비교하기 위해 별도의 쿼리를 실행하지 않아도 된다는 점에서 특히 유용합니다.
if ($post->author()->is($user)) {
// ...
}NOTE
is 메서드는 기본 키 값뿐만 아니라 테이블명과 데이터베이스 커넥션까지 함께 비교합니다. 단순히 기본 키 값만 비교하고 싶다면 $post->id === $anotherPost->id처럼 직접 비교하는 편이 더 명확합니다.
이벤트
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— 생성 또는 수정 시 모두 발생 (속성이 변경되지 않았더라도 발생)deleting/deleted,forceDeleting/forceDeleted— 모델을 삭제할 때restoring/restored— 소프트 삭제된 모델을 복원할 때replicating— 모델을 복제할 때
이름 규칙 참고:
-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
*/
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
{
//
}또는 App\Providers\EventServiceProvider의 boot 메서드에서 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 메서드를 사용하세요. 이 메서드는 클로저를 인자로 받으며, 클로저 내부의 코드는 모델 이벤트를 발송하지 않습니다. 클로저의 반환값은 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();