Eloquent: 시작하기
번역일: 2026년 7월 2일
Eloquent: 시작하기
- 소개
- 모델 클래스 생성
- Eloquent 모델 컨벤션
- 모델 조회
- 단일 모델 / 집계 조회
- 모델 삽입과 수정
- 모델 삭제
- 모델 정리(Pruning)
- 모델 복제
- 쿼리 스코프
- 모델 비교
- 이벤트
소개
Laravel은 데이터베이스와의 상호작용을 직관적이고 즐겁게 만들어주는 Eloquent ORM을 기본으로 제공합니다. Eloquent를 사용하면 각 데이터베이스 테이블에 대응하는 모델을 정의하고, 이 모델을 통해 데이터를 조회하거나 저장할 수 있습니다. 복잡한 SQL을 직접 작성하지 않아도 됩니다.
시작하기 전에, config/database.php에서 데이터베이스 연결 설정이 올바르게 되어 있는지 확인하세요. 데이터베이스 설정에 대한 자세한 내용은 데이터베이스 설정 문서를 참고하세요.
NOTE
Eloquent를 사용하려면 먼저 마이그레이션으로 테이블 구조를 정의해야 합니다. 마이그레이션에 대한 자세한 내용은 마이그레이션 문서를 참고하세요.
모델 클래스 생성
모델은 make:model Artisan 명령어로 생성합니다.
php artisan make:model Flight모델을 생성할 때 데이터베이스 마이그레이션도 함께 생성하려면 --migration 또는 -m 옵션을 사용하세요.
php artisan make:model Flight --migration모델 생성 시 팩토리, 시더, 폼 리퀘스트, 정책(Policy), 컨트롤러 등 다양한 클래스를 함께 생성할 수 있습니다. 여러 옵션을 조합해서 사용하는 것도 가능합니다.
# 모델과 FlightFactory 생성php artisan make:model Flight --factoryphp artisan make:model Flight -f# 모델과 FlightSeeder 생성php artisan make:model Flight --seedphp artisan make:model Flight -s# 모델과 FlightController 생성php artisan make:model Flight --controllerphp artisan make:model Flight -c# 모델, FlightController (리소스), 폼 리퀘스트 클래스 생성php artisan make:model Flight --controller --resource --requestsphp artisan make:model Flight -crR# 모델과 FlightPolicy 생성php artisan make:model Flight --policy# 모델, 마이그레이션, 팩토리, 시더, 컨트롤러 한 번에 생성php artisan make:model Flight -mfsc# 모델에 필요한 모든 클래스를 한 번에 생성 (shortcut)php artisan make:model Flight --allphp artisan make:model Flight -a# 피벗 모델 생성php artisan make:model Member --pivotphp artisan make:model Member -p모델 파일 위치 확인
생성된 모델은 기본적으로 app/Models 디렉터리에 저장됩니다. 원하는 위치가 있다면 make:model 명령어에 경로를 직접 지정할 수 있습니다.
php artisan make:model Models/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는 기본적으로 모델의 기본 키(primary key) 컬럼 이름이 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;
}기본 키가 정수형이 아닌 경우에는 keyType 프로퍼티를 string으로 지정하세요.
<?php
class Flight extends Model
{
/**
* 기본 키의 데이터 타입
*
* @var string
*/
protected $keyType = 'string';
}복합 기본 키
Eloquent 모델은 기본 키로 단일 컬럼만 지원합니다. 복합 기본 키(여러 컬럼을 묶은 기본 키)는 지원하지 않습니다. 단, 데이터베이스 테이블에 복합 유니크 인덱스를 추가하는 것은 가능합니다.
UUID와 ULID 키
정수형 자동 증가 기본 키 대신 UUID를 사용할 수 있습니다. UUID는 36자 길이의 전 세계적으로 고유한 영숫자 식별자입니다.
모델에서 UUID를 기본 키로 사용하려면 Illuminate\Database\Eloquent\Concerns\HasUuids 트레이트를 적용하세요. 또한 해당 컬럼이 UUID 타입 컬럼으로 정의되어 있어야 합니다.
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasUuids;
// ...
}
$article = Article::create(['title' => '한국 Laravel 커뮤니티의 역사']);
$article->id; // "7da77cfc-42b4-4e0a-9154-43f2da24add1"HasUuids 트레이트는 기본적으로 순서가 있는 UUID(ordered UUID)를 생성합니다. 순서가 있는 UUID는 인덱싱 시 정렬 효율이 더 좋아 데이터베이스 성능 면에서 유리합니다.
모델의 newUniqueId 메서드를 오버라이드하면 UUID 생성 방식을 커스터마이즈할 수 있습니다. 또한 uniqueIds 메서드를 통해 UUID를 적용할 컬럼을 직접 지정할 수도 있습니다.
use Ramsey\Uuid\Uuid;
/**
* 모델에 사용할 새 UUID 생성
*/
public function newUniqueId(): string
{
return (string) Uuid::uuid4();
}
/**
* UUID를 받을 컬럼 목록
*
* @return array<int, string>
*/
public function uniqueIds(): array
{
return ['id', 'discount_code'];
}UUID 대신 ULID를 사용하고 싶다면 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() 함수에서 사용하는 형식 문자열을 따릅니다.
<?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은 다양한 상황에서 Eloquent의 동작 방식과 "엄격함" 수준을 조절할 수 있는 여러 메서드를 제공합니다.
preventLazyLoading 메서드는 지연 로딩(lazy loading)을 방지할지 여부를 설정합니다. 지연 로딩을 방지하면 N+1 쿼리 문제를 개발 단계에서 미리 발견할 수 있어 유용합니다. 이 메서드는 보통 애플리케이션의 AppServiceProvider에서 호출합니다.
use Illuminate\Database\Eloquent\Model;
/**
* 애플리케이션 서비스 부트스트랩
*/
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}NOTE
지연 로딩 방지는 개발 환경에서만 활성화하는 것을 권장합니다. 운영(production) 환경에서는 동작에 영향을 주지 않도록 비활성화하는 것이 일반적입니다.
지연 로딩이 감지되면 Illuminate\Database\LazyLoadingViolationException 예외가 발생합니다. handleLazyLoadingViolationUsing 메서드를 사용하면 예외를 던지는 대신 원하는 동작(예: 로그 기록)을 정의할 수 있습니다.
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
$class = $model::class;
info("지연 로딩 감지: [{$class}] 모델에서 [{$relation}] 관계");
});preventSilentlyDiscardingAttributes 메서드를 사용하면, fillable에 등록되지 않은 속성을 설정하려 할 때 예외가 발생하도록 설정할 수 있습니다. 이는 로컬 개발 중에 설정 누락으로 인한 실수를 방지하는 데 도움이 됩니다.
Model::preventSilentlyDiscardingAttributes(! 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는 데이터베이스의 최신 데이터로 기존 인스턴스를 직접 업데이트합니다. 이미 로드된 연관 관계도 함께 새로고침됩니다.
$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의 Collection 클래스는 Laravel의 기본 Collection 클래스를 상속하며, 다양한 유용한 메서드를 제공합니다. 컬렉션은 이터러블(iterable)이므로 일반 배열처럼 반복(foreach)해서 사용할 수 있습니다.
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) {
// ...
}
});chunk의 첫 번째 인자는 청크(묶음)당 레코드 수입니다. 두 번째 인자로 전달한 클로저는 데이터베이스에서 청크를 가져올 때마다 호출됩니다.
청킹 도중 조회 중인 컬럼을 기준으로 레코드를 필터링하면서 업데이트하는 경우, 결과가 예상과 다르게 나올 수 있습니다. 예를 들어 active 컬럼을 기준으로 필터링하면서 그 값을 동시에 변경하면, 일부 레코드가 처리되지 않을 수 있습니다. 이런 경우에는 chunkById 메서드를 사용하는 것이 안전합니다. chunkById는 이전 청크의 마지막 레코드 ID를 기준으로 다음 청크를 가져오므로 일관성이 보장됩니다.
Flight::where('departed', true)
->chunkById(200, function (Collection $flights) {
$flights->each->update(['departed' => false]);
}, column: 'id');지연 컬렉션으로 청킹
lazy 메서드는 내부적으로 청크 방식으로 쿼리를 실행하면서도, 결과를 단일 스트림으로 처리할 수 있는 LazyCollection을 반환합니다. 즉, 메모리를 적게 사용하면서 단일 컬렉션처럼 다룰 수 있습니다.
use App\Models\Flight;
foreach (Flight::lazy() as $flight) {
// ...
}마찬가지로 청킹 도중 조회 컬럼을 기준으로 레코드를 업데이트한다면 lazyById 또는 lazyByIdDesc 메서드를 사용하세요.
Flight::where('departed', true)
->lazyById(200, column: 'id')
->each->update(['departed' => false]);커서
lazy 메서드와 비슷하게, cursor 메서드를 사용하면 대용량 데이터를 순회할 때 메모리 사용량을 크게 줄일 수 있습니다.
cursor는 단 한 번의 쿼리만 실행하되, 각 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 PDO 드라이버의 버퍼링되지 않은 쿼리를 사용합니다. lazy에 비해 메모리를 더 절약하지만, 한 번에 하나씩만 처리할 수 있어 청크 병렬 처리는 불가합니다.
고급 서브쿼리
서브쿼리 SELECT
Eloquent는 고급 서브쿼리를 지원합니다. 단일 쿼리로 연관 테이블의 정보를 함께 가져올 수 있습니다. 예를 들어, 항공편 목적지(destinations) 테이블과 목적지로 향하는 항공편(flights) 테이블이 있다고 가정해 보겠습니다. flights 테이블에는 항공편이 목적지에 도착한 시각을 나타내는 arrived_at 컬럼이 있습니다.
쿼리 빌더의 addSelect와 select 메서드를 이용하면, 각 목적지에 가장 최근 도착한 항공편명을 단일 쿼리로 함께 조회할 수 있습니다.
use App\Models\Destination;
use App\Models\Flight;
return Destination::addSelect(['last_flight' => Flight::select('name')
->whereColumn('destination_id', 'destinations.id')
->orderByDesc('arrived_at')
->limit(1)
])->get();서브쿼리 정렬
쿼리 빌더의 orderBy에도 서브쿼리를 사용할 수 있습니다. 위 예시에서 각 목적지에 마지막으로 도착한 항공편 시각을 기준으로 목적지를 정렬하려면 아래와 같이 작성합니다.
return Destination::orderByDesc(
Flight::select('arrived_at')
->whereColumn('destination_id', 'destinations.id')
->orderByDesc('arrived_at')
->limit(1)
)->get();단일 모델 / 집계 조회
테이블의 모든 레코드를 조회하는 것 외에도, find, first, firstWhere 등을 사용해 단일 레코드를 조회할 수 있습니다. 이 메서드들은 컬렉션 대신 단일 모델 인스턴스를 반환합니다.
use App\Models\Flight;
// 기본 키로 조회
$flight = Flight::find(1);
// 조건에 맞는 첫 번째 레코드 조회
$flight = Flight::where('active', 1)->first();
// 조건에 맞는 첫 번째 레코드 조회 (단축 방법)
$flight = Flight::firstWhere('active', 1);여러 기본 키를 배열로 전달하면 해당하는 모든 레코드를 컬렉션으로 반환합니다.
$flights = Flight::find([1, 2, 3]);조건에 맞는 레코드가 없을 때 다른 동작을 하고 싶다면 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이 자동으로 HTTP 404 응답을 반환합니다. 따라서 라우트에서 아래처럼 작성하면 별도의 예외 처리 없이도 404 응답이 자연스럽게 동작합니다.
use App\Models\Flight;
Route::get('/api/flights/{id}', function (string $id) {
return Flight::findOrFail($id);
});모델 조회 또는 생성
firstOrCreate 메서드는 조건에 맞는 레코드를 찾고, 없으면 새로 생성합니다. 첫 번째 인자는 조회 조건이고, 두 번째 인자(선택사항)는 레코드 생성 시 추가로 설정할 속성입니다.
firstOrNew는 firstOrCreate와 비슷하지만, 레코드가 없을 때 데이터베이스에 저장하지 않고 새 모델 인스턴스만 반환합니다. 저장하려면 직접 save를 호출해야 합니다.
use App\Models\Flight;
// 이름으로 조회하고 없으면 생성
$flight = Flight::firstOrCreate([
'name' => '서울-제주 노선'
]);
// 이름으로 조회하고 없으면 지연 및 도착지 속성을 포함해서 생성
$flight = Flight::firstOrCreate(
['name' => '서울-부산 노선'],
['delayed' => 1, 'arrival_time' => '11:30']
);
// 이름으로 조회하고 없으면 새 인스턴스 반환 (미저장)
$flight = Flight::firstOrNew([
'name' => '서울-인천 노선'
]);
// 이름으로 조회하고 없으면 지연 및 도착지 속성 포함 새 인스턴스 반환 (미저장)
$flight = Flight::firstOrNew(
['name' => '서울-대구 노선'],
['delayed' => 1, 'arrival_time' => '09:00']
);집계 조회
Eloquent 모델에서도 쿼리 빌더가 제공하는 count, sum, max 등의 집계 메서드를 그대로 사용할 수 있습니다. 이 메서드들은 모델 인스턴스가 아닌 스칼라 값을 반환합니다.
$count = Flight::where('active', 1)->count();
$max = Flight::where('active', 1)->max('duration');모델 삽입과 수정
삽입
Eloquent로 새 레코드를 삽입할 때는 새 모델 인스턴스를 만들고 속성을 설정한 뒤 save 메서드를 호출하면 됩니다.
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Flight;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class FlightController extends Controller
{
/**
* 새 항공편을 데이터베이스에 저장
*/
public function store(Request $request): Response
{
// 요청 유효성 검사 ...
$flight = new Flight;
$flight->name = $request->name;
$flight->save();
return response()->noContent();
}
}save 메서드를 호출하면 레코드가 데이터베이스에 삽입됩니다. created_at과 updated_at 타임스탬프는 자동으로 설정되므로 별도로 지정할 필요가 없습니다.
수정
이미 데이터베이스에 있는 모델은 save 메서드로 수정할 수 있습니다. 모델을 조회하고 원하는 속성을 변경한 뒤 save를 호출하면 됩니다. updated_at은 자동으로 갱신됩니다.
use App\Models\Flight;
$flight = Flight::find(1);
$flight->name = '서울-도쿄 노선 (수정)';
$flight->save();특정 속성이 실제로 변경되지 않은 경우 불필요한 UPDATE 쿼리가 실행되지 않도록, saveIfDirty 메서드를 사용할 수도 있습니다.
대량 수정
조건에 맞는 여러 레코드를 한 번에 업데이트하려면 update 메서드를 사용하세요. 아래 예시는 목적지가 인천이고 활성화된 모든 항공편의 delayed 컬럼을 1로 업데이트합니다.
Flight::where('active', 1)
->where('destination', '인천')
->update(['delayed' => 1]);update 메서드는 업데이트할 컬럼명과 값을 배열로 받습니다. 반환값은 영향받은 레코드 수입니다.
NOTE
update를 통한 대량 수정(mass update)은 Eloquent 이벤트(saving, saved, updating, updated)를 발생시키지 않습니다. 대량 수정 시 모델 인스턴스를 개별적으로 조회하지 않기 때문입니다.
변경된 속성 확인
Eloquent는 모델이 처음 로드된 이후 변경된 속성을 내부적으로 추적합니다. isDirty, isClean, wasChanged 등의 메서드로 변경 상태를 확인할 수 있습니다.
isDirty는 모델의 속성이 변경되었는지 확인합니다. 특정 속성 이름을 인자로 전달하면 해당 속성만 확인합니다.
isClean은 isDirty의 반대로, 속성이 변경되지 않았는지 확인합니다.
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(); // 전체 원본 속성 배열대량 할당
create 메서드를 사용하면 하나의 메서드 호출로 새 모델을 저장할 수 있습니다. 이 메서드는 저장된 모델 인스턴스를 반환합니다.
use App\Models\Flight;
$flight = Flight::create([
'name' => '서울-런던 노선',
]);단, create를 사용하려면 모델에 fillable 또는 guarded 프로퍼티를 반드시 설정해야 합니다. 모든 Eloquent 모델은 기본적으로 대량 할당 취약점(mass assignment vulnerability)으로부터 보호됩니다.
대량 할당 취약점은 사용자가 HTTP 요청으로 예상치 못한 값을 전달했을 때, 그 값이 그대로 데이터베이스에 저장되는 보안 문제입니다. 예를 들어 악의적인 사용자가 is_admin=1을 요청에 추가해 권한을 탈취할 수 있습니다.
이를 방지하기 위해 fillable 프로퍼티로 대량 할당을 허용할 컬럼을 명시적으로 지정하거나, guarded로 차단할 컬럼을 지정합니다.
`fillable` 사용
fillable 프로퍼티에 허용할 속성 목록을 지정합니다.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 대량 할당을 허용할 속성 목록
*
* @var array<int, string>
*/
protected $fillable = ['name'];
}이제 create 메서드로 새 레코드를 안전하게 삽입할 수 있습니다.
$flight = Flight::create(['name' => '서울-파리 노선']);`guarded` 사용
fillable과 반대로 guarded는 대량 할당을 차단할 속성을 지정합니다. guarded에 없는 속성은 모두 대량 할당이 허용됩니다.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 대량 할당을 차단할 속성 목록
* (빈 배열이면 모든 속성 허용)
*
* @var array<int, string>
*/
protected $guarded = [];
}WARNING
guarded를 빈 배열로 설정하면 모든 속성이 대량 할당 가능해집니다. 이 경우 사용자 입력을 직접 create나 fill에 전달하지 않도록 각별히 주의해야 합니다.
대량 할당 예외 처리
기본적으로 fillable에 없는 속성은 조용히 무시됩니다. 운영 환경에서는 이것이 의도된 동작이지만, 개발 중에는 실수를 파악하기 어려울 수 있습니다.
preventSilentlyDiscardingAttributes 메서드를 호출하면, 보호된 속성을 대량 할당하려 할 때 예외가 발생하도록 설정할 수 있습니다.
Model::preventSilentlyDiscardingAttributes(! app()->isProduction());Upsert
upsert 메서드는 레코드가 존재하면 수정하고, 없으면 삽입하는 단일 작업입니다.
첫 번째 인자는 삽입하거나 수정할 값의 배열이고, 두 번째 인자는 레코드를 고유하게 식별하는 컬럼 목록입니다. 세 번째 인자는 레코드가 이미 있을 때 업데이트할 컬럼 목록입니다.
Flight::upsert([
['departure' => '서울', 'destination' => '도쿄', 'price' => 99],
['departure' => '부산', 'destination' => '오사카', 'price' => 150],
], uniqueBy: ['departure', 'destination'], update: ['price']);WARNING
SQL Server를 제외한 모든 데이터베이스에서 upsert의 두 번째 인자로 지정한 컬럼에는 "primary" 또는 "unique" 인덱스가 있어야 합니다. 또한 MySQL 드라이버는 upsert의 두 번째 인자를 무시하고 항상 테이블의 "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
쿼리를 통한 대량 삭제(mass delete)는 deleting과 deleted Eloquent 이벤트를 발생시키지 않습니다. 대량 삭제 시 모델 인스턴스를 개별로 조회하지 않기 때문입니다.
소프트 삭제
데이터베이스에서 레코드를 실제로 제거하는 대신, "삭제된 것처럼" 표시만 해두는 방식을 소프트 삭제(Soft Deleting)라고 합니다. 소프트 삭제를 사용하려면 모델에 Illuminate\Database\Eloquent\SoftDeletes 트레이트를 추가하면 됩니다.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Flight extends Model
{
use SoftDeletes;
}NOTE
SoftDeletes 트레이트는 deleted_at 속성을 자동으로 DateTime / Carbon 인스턴스로 캐스팅합니다.
테이블에 deleted_at 컬럼도 추가해야 합니다. Laravel 스키마 빌더가 이를 위한 헬퍼 메서드를 제공합니다.
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Schema::table('flights', function (Blueprint $table) {
$table->softDeletes();
});
Schema::table('flights', function (Blueprint $table) {
$table->dropSoftDeletes();
});이제 모델에서 delete를 호출하면 실제 삭제 대신 deleted_at 컬럼에 현재 시각이 기록됩니다. 소프트 삭제가 활성화된 모델을 조회하면 deleted_at이 설정된 레코드는 자동으로 결과에서 제외됩니다.
trashed 메서드로 특정 모델 인스턴스가 소프트 삭제 상태인지 확인할 수 있습니다.
if ($flight->trashed()) {
// ...
}소프트 삭제된 모델 조회
소프트 삭제 포함 조회
소프트 삭제된 레코드를 포함해서 조회하려면 withTrashed 메서드를 사용하세요.
use App\Models\Flight;
$flights = Flight::withTrashed()
->where('account_id', 1)
->get();연관 관계 쿼리에도 사용할 수 있습니다.
$flight->history()->withTrashed()->get();소프트 삭제된 레코드만 조회
소프트 삭제된 레코드만 조회하려면 onlyTrashed 메서드를 사용하세요.
$flights = Flight::onlyTrashed()
->where('airline_id', 1)
->get();소프트 삭제 복원
소프트 삭제된 모델을 활성 상태로 복원하려면 restore 메서드를 호출하세요. deleted_at이 null로 초기화됩니다.
$flight->restore();쿼리를 통한 일괄 복원도 가능합니다.
Flight::withTrashed()
->where('airline_id', 1)
->restore();영구 삭제
소프트 삭제된 레코드를 데이터베이스에서 완전히 제거하려면 forceDelete 메서드를 사용하세요.
$flight->forceDelete();연관 관계 쿼리에도 사용할 수 있습니다.
$flight->history()->forceDelete();모델 정리(Pruning)
더 이상 필요 없는 모델을 주기적으로 삭제하고 싶을 때는 Illuminate\Database\Eloquent\Prunable 또는 Illuminate\Database\Eloquent\MassPrunable 트레이트를 모델에 추가하면 됩니다.
<?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
{
// 스토리지에서 관련 파일 삭제 등
}정리할 모델을 설정했다면 애플리케이션의 routes/console.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)됩니다.
대량 정리(Mass Pruning)
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());
}
}모델 복제
기존 모델 인스턴스를 복제하려면 replicate 메서드를 사용하세요. 비슷한 속성을 가진 새 레코드를 만들 때 유용합니다.
use App\Models\Address;
$shipping = Address::create([
'type' => 'shipping',
'line_1' => '서울시 강남구 테헤란로 123',
'city' => '서울',
'country' => 'KR',
]);
$billing = $shipping->replicate()->fill([
'type' => 'billing',
]);
$billing->save();복제 시 특정 속성을 제외하려면 배열로 전달하세요.
$flight = Flight::create([
'destination' => '도쿄',
'origin' => '서울',
'last_flown' => '2024-01-15 14:30:00',
'last_pilot_id' => 747,
]);
$copy = $flight->replicate([
'last_flown',
'last_pilot_id',
]);쿼리 스코프
글로벌 스코프
글로벌 스코프를 사용하면 특정 모델의 모든 쿼리에 자동으로 조건을 추가할 수 있습니다. Laravel의 소프트 삭제 기능도 내부적으로 글로벌 스코프를 사용합니다.
글로벌 스코프 정의
글로벌 스코프는 Illuminate\Database\Eloquent\Scope 인터페이스를 구현한 클래스로 작성합니다. 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
<h1 id="writing-global-scopes">Eloquent: 시작하기</h1>
<h2 id="applying-global-scopes">소개</h2>
Laravel은 데이터베이스와의 상호작용을 즐겁게 만들어주는 ORM(Object-Relational Mapper)인 Eloquent를 내장하고 있습니다. Eloquent를 사용하면 각 데이터베이스 테이블에 대응하는 "모델"을 통해 해당 테이블과 상호작용할 수 있습니다. 모델을 통해 레코드를 조회하는 것은 물론, 삽입·수정·삭제 작업도 자연스럽게 처리할 수 있습니다.
> [!NOTE]
> 시작하기 전에 애플리케이션의 `config/database.php` 설정 파일에서 데이터베이스 연결을 구성해 두어야 합니다. 데이터베이스 설정에 대한 자세한 내용은 [데이터베이스 설정 문서](/docs/12.x/database/database#configuration)를 참고하세요.
<h2 id="anonymous-global-scopes">모델 클래스 생성하기</h2>
Eloquent 모델은 보통 `app/Models` 디렉터리에 위치하며, `Illuminate\Database\Eloquent\Model` 클래스를 상속합니다. `make:model` [Artisan 명령어](/docs/12.x/packages/artisan)로 새 모델을 생성할 수 있습니다:
```shell
php artisan make:model Flight모델을 생성할 때 데이터베이스 마이그레이션도 함께 만들고 싶다면 --migration 또는 -m 옵션을 사용하세요:
php artisan make:model Flight --migration모델 생성 시 팩토리, 시더, 정책, 컨트롤러, 폼 리퀘스트 등 다양한 관련 클래스를 함께 생성할 수도 있습니다. 옵션을 조합하면 여러 클래스를 한 번에 만들 수 있어 편리합니다:
<h1 id="removing-global-scopes">모델과 FlightFactory 클래스 생성...</h1>
php artisan make:model Flight --factory
php artisan make:model Flight -f
<h1 id="local-scopes">모델과 FlightSeeder 클래스 생성...</h1>
php artisan make:model Flight --seed
php artisan make:model Flight -s
<h1 id="utilizing-a-local-scope">모델과 FlightController 클래스 생성...</h1>
php artisan make:model Flight --controller
php artisan make:model Flight -c
<h1 id="dynamic-scopes">모델, FlightController 리소스 클래스, 폼 리퀘스트 클래스 생성...</h1>
php artisan make:model Flight --controller --resource --requests
php artisan make:model Flight -crR
<h1 id="pending-attributes">모델과 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
php artisan make:model Flight -a
<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 모델은 단일 컬럼으로 구성된 기본 키를 필요로 합니다. 복합 기본 키(여러 컬럼의 조합)는 지원되지 않습니다. 다만, 기본 키와는 별개로 여러 컬럼을 묶은 고유 인덱스(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; // "018f2b5c-6a7f-7b12-9d6f-2f8a4e0c9c11"HasUuids 트레이트는 기본적으로 UUIDv7 형식의 식별자를 생성합니다. UUIDv7은 시간 순으로 정렬 가능(lexicographically sortable)하여 인덱스 성능이 더 효율적입니다.
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 컬럼이 존재한다고 가정하고, 모델이 생성되거나 수정될 때 이 컬럼들을 자동으로 관리합니다. 자동 관리를 비활성화하려면 $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
{
/**
* "created at" 컬럼 이름
*
* @var string|null
*/
public const CREATED_AT = 'creation_date';
/**
* "updated at" 컬럼 이름
*
* @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\Model;
class Flight extends Model
{
/**
* 이 모델이 사용할 데이터베이스 연결 이름
*
* @var string
*/
protected $connection = 'mysql';
}기본 속성값
새로 인스턴스화된 모델에는 기본적으로 아무 속성값도 없습니다. 일부 속성의 기본값을 지정하려면 $attributes 프로퍼티를 정의하세요. 이 배열에 넣는 값은 데이터베이스에서 읽어온 것과 동일한 "저장 가능한" 원시(raw) 형식이어야 합니다.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 모델 속성의 기본값
*
* @var array
*/
protected $attributes = [
'options' => '[]',
'delayed' => false,
];
}Eloquent 엄격 모드 설정
Laravel은 Eloquent의 동작 방식과 엄격함(strictness)을 상황에 맞게 조정할 수 있는 여러 메서드를 제공합니다.
지연 로딩 방지
preventLazyLoading 메서드는 지연 로딩(lazy loading)을 차단할지 여부를 설정합니다. 프로덕션 환경에서는 정상 동작을 유지하면서, 개발 환경에서만 지연 로딩을 비활성화하는 방식이 일반적입니다. 이 설정은 보통 AppServiceProvider의 boot 메서드에서 호출합니다.
use Illuminate\Database\Eloquent\Model;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}채울 수 없는 속성 할당 시 예외 발생
preventSilentlyDiscardingAttributes 메서드를 사용하면, fillable 배열에 추가되지 않은 속성에 값을 할당하려 할 때 예외를 발생시킵니다. 개발 환경에서 실수로 누락된 속성을 조기에 발견하는 데 유용합니다.
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());NOTE
위 두 메서드 모두 프로덕션 환경에서는 비활성화하는 것이 권장됩니다. 개발 중 문제를 조기에 발견하기 위한 용도로 활용하세요.
모델 조회하기
모델과 연결된 데이터베이스 테이블을 생성했다면, 이제 데이터를 조회할 준비가 된 것입니다. 각 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 모델 인스턴스가 있다면, fresh와 refresh 메서드를 사용해 최신 데이터로 갱신할 수 있습니다.
fresh 메서드는 데이터베이스에서 모델을 새로 조회해 새 인스턴스를 반환합니다. 기존 인스턴스는 영향을 받지 않습니다:
$flight = Flight::where('number', 'KE 001')->first();
$freshFlight = $flight->fresh();반면 refresh 메서드는 기존 인스턴스를 데이터베이스의 최신 데이터로 다시 채웁니다. 이미 로드된 관계(relationship)도 함께 갱신됩니다:
$flight = Flight::where('number', 'KE 001')->first();
$flight->number = 'OZ 123';
$flight->refresh();
$flight->number; // "KE 001"컬렉션
앞서 살펴본 것처럼 all이나 get 같은 Eloquent 메서드는 여러 레코드를 반환합니다. 이때 반환값은 일반 PHP 배열이 아니라 Illuminate\Database\Eloquent\Collection 인스턴스입니다.
Eloquent Collection은 Laravel의 기본 Illuminate\Support\Collection 클래스를 상속하며, 데이터를 다루기 위한 다양한 유용한 메서드를 제공합니다. 예를 들어 reject 메서드를 사용하면 클로저 조건에 따라 컬렉션에서 특정 모델을 제거할 수 있습니다:
$flights = Flight::where('destination', 'ICN')->get();
$flights = $flights->reject(function (Flight $flight) {
return $flight->cancelled;
});기본 컬렉션 메서드 외에도, Eloquent 컬렉션은 Eloquent 모델 전용 추가 메서드를 별도로 제공합니다.
Laravel의 모든 컬렉션은 PHP의 이터러블(iterable) 인터페이스를 구현하므로, 배열처럼 반복(foreach)할 수 있습니다:
foreach ($flights as $flight) {
echo $flight->name;
}결과 청크 처리
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 대신 chunkById를 사용해야 합니다. chunk를 사용하면 오프셋 기반 페이지네이션이 깨져 예상치 못한 결과가 발생할 수 있습니다. chunkById는 내부적으로 이전 청크의 마지막 모델보다 id가 큰 레코드를 조회하는 방식으로 동작합니다:
Flight::where('departed', true)
->chunkById(200, function (Collection $flights) {
$flights->each->update(['departed' => false]);
}, column: 'id');chunkById와 lazyById는 내부적으로 자체 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와 유사하게 쿼리를 청크 단위로 실행합니다. 다만 각 청크를 콜백에 전달하는 대신, 모든 결과를 하나의 스트림처럼 다룰 수 있는 평탄화된 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 모델은 실제로 반복될 때에야 비로소 생성(hydrate)됩니다. 따라서 반복 중 어느 시점에도 메모리에는 Eloquent 모델 인스턴스가 하나만 존재합니다.
WARNING
cursor 메서드는 한 번에 하나의 Eloquent 모델만 메모리에 유지하기 때문에, 관계(relationship)를 eager load할 수 없습니다. 관계를 함께 로드해야 한다면 lazy 메서드 사용을 고려하세요.
내부적으로 cursor는 PHP 제너레이터를 사용해 구현됩니다:
use App\Models\Flight;
foreach (Flight::where('destination', 'GMP')->cursor() as $flight) {
// ...
}cursor는 Illuminate\Support\LazyCollection 인스턴스를 반환합니다. Lazy 컬렉션을 사용하면 일반 컬렉션의 메서드 대부분을 활용하면서도 메모리에는 모델 하나만 유지할 수 있습니다:
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 메서드를 사용하는 것이 더 적합합니다.
NOTE
lazy, cursor, chunk 중 어떤 것을 선택할지 헷갈린다면 다음 기준을 참고하세요.
- 관계 eager load가 필요하다 →
lazy/chunk - 단순 반복, 메모리 최소화가 목표다 →
cursor - 반복 중 해당 컬럼을 업데이트한다 →
chunkById/lazyById
고급 서브쿼리
서브쿼리 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 라우트에서 존재하지 않는 리소스를 요청받았을 때 직접 404 응답을 작성하는 대신 findOrFail을 사용하면 코드가 훨씬 간결해집니다.
조회 또는 생성
firstOrCreate 메서드는 주어진 컬럼/값 쌍으로 레코드를 조회합니다. 해당 레코드가 없으면 첫 번째 배열 인자와 두 번째 배열 인자(선택적)를 합쳐 새 레코드를 데이터베이스에 바로 삽입합니다.
firstOrNew 메서드도 동일하게 레코드를 조회하지만, 찾지 못했을 때 새 모델 인스턴스만 반환하고 데이터베이스에는 저장하지 않습니다. 직접 save 메서드를 호출해야 저장됩니다.
use App\Models\Flight;
// 이름으로 항공편 조회, 없으면 생성
$flight = Flight::firstOrCreate([
'name' => '서울 to 도쿄'
]);
// 이름으로 조회, 없으면 추가 속성과 함께 생성
$flight = Flight::firstOrCreate(
['name' => '서울 to 도쿄'],
['delayed' => 1, 'arrival_time' => '11:30']
);
// 이름으로 조회, 없으면 새 인스턴스 반환 (저장 안 됨)
$flight = Flight::firstOrNew([
'name' => '서울 to 도쿄'
]);
// 이름으로 조회, 없으면 추가 속성과 함께 새 인스턴스 반환 (저장 안 됨)
$flight = Flight::firstOrNew(
['name' => '부산 to 오사카'],
['delayed' => 1, 'arrival_time' => '11:30']
);집계 함수 사용
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\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();경우에 따라 일치하는 모델이 있으면 업데이트하고, 없으면 새로 생성해야 할 때가 있습니다. firstOrCreate와 마찬가지로 updateOrCreate 메서드도 모델을 자동으로 저장하므로 save를 별도로 호출할 필요가 없습니다.
아래 예시에서는 departure가 '인천'이고 destination이 '오사카'인 항공편이 존재하면 price와 discounted 컬럼을 업데이트하고, 없으면 첫 번째 배열과 두 번째 배열을 병합한 속성으로 새 항공편을 생성합니다.
$flight = Flight::updateOrCreate(
['departure' => '인천', 'destination' => '오사카'],
['price' => 99, 'discounted' => 1]
);firstOrCreate나 updateOrCreate를 사용할 때 새 레코드가 생성되었는지, 아니면 기존 레코드가 업데이트되었는지 구분해야 할 경우 wasRecentlyCreated 속성을 활용할 수 있습니다.
$flight = Flight::updateOrCreate(
// ...
);
if ($flight->wasRecentlyCreated) {
// 새 항공편 레코드가 삽입된 경우...
}대량 업데이트
특정 쿼리 조건과 일치하는 여러 모델을 한 번에 업데이트할 수도 있습니다. 아래 예시에서는 active 상태이고 destination이 '오사카'인 모든 항공편을 지연(delayed) 처리합니다.
Flight::where('active', 1)
->where('destination', '오사카')
->update(['delayed' => 1]);update 메서드는 업데이트할 컬럼과 값의 쌍으로 이루어진 배열을 인자로 받으며, 영향을 받은 행(row)의 수를 반환합니다.
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 메서드는 현재 요청 사이클 내에서 마지막으로 모델이 저장될 때 속성이 변경되었는지 확인합니다. 특정 속성명을 전달하면 해당 속성의 변경 여부를 확인할 수 있습니다.
$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(); // 원래 속성값 배열...getChanges 메서드는 마지막으로 저장될 때 변경된 속성들을 배열로 반환하고, getPrevious 메서드는 마지막 저장 이전의 원래 속성값을 배열로 반환합니다.
$user = User::find(1);
$user->name; // 길동
$user->email; // gildong@example.com
$user->update([
'name' => '철수',
'email' => 'cheolsu@example.com',
]);
$user->getChanges();
/*
[
'name' => '철수',
'email' => 'cheolsu@example.com',
]
*/
$user->getPrevious();
/*
[
'name' => '길동',
'email' => 'gildong@example.com',
]
*/대량 할당 (Mass Assignment)
앞서 살펴본 것처럼 create 메서드를 사용하면 한 줄로 새 모델을 저장할 수 있으며, 새로 생성된 모델 인스턴스가 반환됩니다.
use App\Models\Flight;
$flight = Flight::create([
'name' => '서울 to 도쿄',
]);그런데 create 메서드를 사용하려면 모델에 $fillable 또는 $guarded 속성을 반드시 정의해야 합니다. Eloquent 모델은 기본적으로 대량 할당 취약점으로부터 보호되기 때문입니다.
대량 할당 취약점이란? 사용자가 예상치 못한 HTTP 요청 파라미터를 전송하고, 그 파라미터가 의도하지 않은 데이터베이스 컬럼을 변경하는 보안 문제입니다. 예를 들어 악의적인 사용자가 is_admin 파라미터를 HTTP 요청에 포함해서 보내면, 이 값이 모델의 create 메서드에 그대로 전달되어 스스로를 관리자로 권한 상승시킬 수 있습니다.
이러한 위험을 막기 위해 모델에서 대량 할당을 허용할 속성을 $fillable 배열로 명시적으로 지정해야 합니다. 아래는 Flight 모델의 name 속성만 대량 할당 가능하도록 설정하는 예시입니다.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* 대량 할당을 허용할 속성 목록.
*
* @var array<int, string>
*/
protected $fillable = ['name'];
}$fillable을 지정한 뒤에는 create 메서드로 새 레코드를 삽입할 수 있습니다. create는 새로 생성된 모델 인스턴스를 반환합니다.
$flight = Flight::create(['name' => '서울 to 도쿄']);이미 모델 인스턴스가 있다면 fill 메서드를 사용해 배열로 속성을 한 번에 채울 수도 있습니다.
$flight->fill(['name' => '부산 to 후쿠오카']);대량 할당과 JSON 컬럼
JSON 컬럼을 대량 할당할 때는 해당 컬럼의 키를 모델의 $fillable 배열에 반드시 지정해야 합니다. 보안상의 이유로 Laravel은 $guarded 속성을 사용할 때 중첩된 JSON 속성 업데이트를 지원하지 않습니다.
/**
* 대량 할당을 허용할 속성 목록.
*
* @var array<int, string>
*/
protected $fillable = [
'options->enabled',
];대량 할당 전체 허용
모든 속성을 대량 할당 가능하게 하려면 $guarded를 빈 배열로 정의합니다. 모델의 보호를 해제할 경우, fill, create, update 등의 메서드에 전달하는 배열을 항상 직접 명시적으로 구성해야 합니다.
/**
* 대량 할당을 막을 속성 목록.
*
* @var array<string>
*/
protected $guarded = [];대량 할당 예외 처리
기본적으로 $fillable에 포함되지 않은 속성은 대량 할당 시 조용히 무시됩니다. 프로덕션 환경에서는 이것이 정상 동작이지만, 로컬 개발 환경에서는 모델 변경이 반영되지 않는 원인을 찾기 어렵게 만들 수 있습니다.
허용되지 않은 속성을 채우려 할 때 예외를 발생시키려면 preventSilentlyDiscardingAttributes 메서드를 호출하면 됩니다. 일반적으로 AppServiceProvider의 boot 메서드에서 호출합니다.
use Illuminate\Database\Eloquent\Model;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Model::preventSilentlyDiscardingAttributes($this->app->isLocal());
}Upsert
Eloquent의 upsert 메서드를 사용하면 단일 원자적(atomic) 연산으로 레코드를 업데이트하거나 삽입할 수 있습니다. 첫 번째 인자는 삽입 또는 업데이트할 값의 배열이고, 두 번째 인자는 테이블 내에서 레코드를 고유하게 식별하는 컬럼 목록입니다. 세 번째 인자는 일치하는 레코드가 이미 존재할 경우 업데이트할 컬럼 목록입니다. 모델에 타임스탬프가 활성화되어 있으면 created_at과 updated_at은 자동으로 설정됩니다.
Flight::upsert([
['departure' => '인천', 'destination' => '오사카', 'price' => 99],
['departure' => '김포', 'destination' => '도쿄', 'price' => 150]
], uniqueBy: ['departure', 'destination'], update: ['price']);WARNING
SQL Server를 제외한 모든 데이터베이스에서 upsert 메서드의 두 번째 인자로 전달하는 컬럼에는 "primary" 또는 "unique" 인덱스가 있어야 합니다. 또한 MariaDB와 MySQL 드라이버는 두 번째 인자를 무시하고, 항상 테이블의 "primary" 및 "unique" 인덱스를 기준으로 기존 레코드를 탐색합니다.
Eloquent: 시작하기
모델 삭제
모델 인스턴스에서 delete 메서드를 호출하면 해당 레코드를 삭제할 수 있습니다.
use App\Models\Flight;
$flight = Flight::find(1);
$flight->delete();기본 키로 모델 삭제하기
위 예시에서는 먼저 모델을 조회한 뒤 삭제했지만, 기본 키를 이미 알고 있다면 destroy 메서드를 사용해 모델을 조회하지 않고 바로 삭제할 수 있습니다. destroy 메서드는 단일 기본 키뿐만 아니라 여러 개의 기본 키, 배열, 또는 컬렉션도 받을 수 있습니다.
Flight::destroy(1);
Flight::destroy(1, 2, 3);
Flight::destroy([1, 2, 3]);
Flight::destroy(collect([1, 2, 3]));소프트 삭제를 사용 중이라면 forceDestroy 메서드로 영구 삭제할 수 있습니다.
Flight::forceDestroy(1);WARNING
destroy 메서드는 각 모델을 개별적으로 조회한 뒤 delete 메서드를 호출합니다. 따라서 각 모델에 대해 deleting 및 deleted 이벤트가 올바르게 발생합니다.
쿼리로 모델 일괄 삭제하기
Eloquent 쿼리를 통해 조건에 맞는 모델을 한 번에 삭제할 수도 있습니다. 아래 예시는 비활성 상태(active = 0)인 항공편을 모두 삭제합니다. 일괄 업데이트와 마찬가지로, 일괄 삭제 시에는 삭제되는 모델에 대해 모델 이벤트가 발생하지 않습니다.
$deleted = Flight::where('active', 0)->delete();테이블의 모든 레코드를 삭제하려면 조건 없이 쿼리를 실행합니다.
$deleted = Flight::query()->delete();WARNING
Eloquent로 일괄 삭제를 실행할 때는 실제로 모델 인스턴스를 조회하지 않기 때문에, deleting 및 deleted 모델 이벤트가 발생하지 않습니다.
소프트 삭제 (Soft Delete)
Eloquent는 데이터베이스에서 레코드를 실제로 제거하는 대신, "소프트 삭제(soft delete)" 방식도 지원합니다. 소프트 삭제된 모델은 DB에서 지워지지 않고, 대신 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();쿼리와 함께 사용하면 여러 모델을 한 번에 복원할 수도 있습니다. 다른 일괄 작업과 마찬가지로, 이 경우에도 모델 이벤트는 발생하지 않습니다.
Flight::withTrashed()
->where('airline_id', 1)
->restore();관계(relationship) 쿼리를 작성할 때도 restore를 사용할 수 있습니다.
$flight->history()->restore();모델 영구 삭제하기
소프트 삭제된 모델을 데이터베이스에서 완전히 제거해야 할 경우에는 forceDelete 메서드를 사용합니다.
$flight->forceDelete();관계 쿼리에서도 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 트레이트를 모델에 추가하면 됩니다. 트레이트를 추가한 후, 삭제 대상 레코드를 반환하는 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;
/**
* 정리 대상 모델 쿼리를 반환합니다.
*/
public function prunable(): Builder
{
return static::where('created_at', '<=', now()->minus(months: 1));
}
}모델에 pruning 메서드를 추가로 정의하면, 모델이 삭제되기 직전에 해당 메서드가 호출됩니다. 예를 들어, 데이터베이스에서 레코드가 삭제되기 전에 연결된 파일 등의 외부 리소스를 함께 정리할 때 유용합니다.
/**
* 모델 정리 전 처리 작업을 수행합니다.
*/
protected function pruning(): void
{
// 연결된 파일이나 외부 리소스 정리 등
}prunable 모델 설정이 끝나면, 애플리케이션의 routes/console.php 파일에서 model:prune Artisan 명령어를 스케줄에 등록합니다. 실행 주기는 프로젝트 상황에 맞게 자유롭게 설정하면 됩니다.
use Illuminate\Support\Facades\Schedule;
Schedule::command('model:prune')->daily();model:prune 명령어는 내부적으로 app/Models 디렉터리에서 Prunable 트레이트를 사용하는 모델을 자동으로 감지합니다. 모델 파일이 다른 위치에 있다면 --model 옵션으로 직접 지정할 수 있습니다.
Schedule::command('model:prune', [
'--model' => [Address::class, Flight::class],
])->daily();반대로, 감지된 모델 중 특정 모델만 정리 대상에서 제외하고 싶다면 --except 옵션을 사용합니다.
Schedule::command('model:prune', [
'--except' => [Address::class, Flight::class],
])->daily();실제로 삭제하기 전에 prunable 쿼리가 몇 건을 대상으로 하는지 미리 확인하려면 --pretend 옵션을 사용하세요. 실제 삭제는 일어나지 않고 대상 건수만 출력됩니다.
php artisan model:prune --pretendWARNING
소프트 삭제(Soft Delete) 모델도 prunable 쿼리에 해당하면 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()->minus(months: 1));
}
}모델 복제
replicate 메서드를 사용하면 기존 모델 인스턴스의 저장되지 않은 복사본을 만들 수 있습니다. 동일한 속성을 많이 공유하는 모델 인스턴스가 여러 개 필요할 때 특히 유용합니다.
use App\Models\Address;
$shipping = Address::create([
'type' => 'shipping',
'line_1' => '서울특별시 강남구 테헤란로 123',
'city' => '서울',
'state' => '서울특별시',
'postcode' => '06234',
]);
$billing = $shipping->replicate()->fill([
'type' => 'billing'
]);
$billing->save();복제 시 특정 속성을 제외하려면 replicate 메서드에 배열로 전달하면 됩니다.
$flight = Flight::create([
'destination' => 'ICN',
'origin' => 'LHR',
'last_flown' => '2020-03-04 11:00:00',
'last_pilot_id' => 747,
]);
$flight = $flight->replicate([
'last_flown',
'last_pilot_id'
]);NOTE
replicate로 생성된 인스턴스는 아직 데이터베이스에 저장되지 않은 상태입니다. 필요한 속성을 수정한 뒤 save()를 호출해야 실제로 저장됩니다.
쿼리 스코프
글로벌 스코프
글로벌 스코프를 사용하면 특정 모델의 모든 쿼리에 자동으로 조건을 추가할 수 있습니다. 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()->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익명 글로벌 스코프
별도 클래스를 만들 필요 없이, 간단한 글로벌 스코프는 클로저로도 정의할 수 있습니다. 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();여러 개 또는 모든 글로벌 스코프를 한꺼번에 제거하려면 withoutGlobalScopes나 withoutGlobalScopesExcept를 사용합니다.
// 모든 글로벌 스코프 제거
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();여러 스코프를 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();보류 중인 어트리뷰트 (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' => '작성 중인 글']);
$draft->hidden; // truewhere 조건 추가 없이 어트리뷰트 값만 설정하고 싶다면, asConditions 인자를 false로 지정하세요.
$query->withAttributes([
'hidden' => true,
], asConditions: false);모델 비교
두 모델 인스턴스가 "같은" 모델인지 비교해야 할 때가 있습니다. is와 isNot 메서드를 사용하면 두 모델의 기본 키, 테이블, 데이터베이스 연결이 모두 동일한지 빠르게 확인할 수 있습니다.
if ($post->is($anotherPost)) {
// ...
}
if ($post->isNot($anotherPost)) {
// ...
}is와 isNot 메서드는 belongsTo, hasOne, morphTo, morphOne 관계를 사용할 때도 동일하게 활용할 수 있습니다. 관련 모델을 가져오기 위해 별도의 쿼리를 실행하지 않고도 비교할 수 있어 특히 유용합니다.
if ($post->author()->is($user)) {
// ...
}Eloquent: 이벤트
NOTE
Eloquent 이벤트를 클라이언트 애플리케이션에 직접 브로드캐스트하고 싶다면 Laravel의 모델 이벤트 브로드캐스팅 문서를 참고하세요.
Eloquent 모델은 라이프사이클의 주요 시점마다 이벤트를 자동으로 발생시킵니다. 다음 이벤트들을 훅(hook)으로 활용할 수 있습니다:
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— 모델을 복제할 때
-ingvs-ed이벤트: 이름이-ing로 끝나는 이벤트는 변경 사항이 데이터베이스에 반영되기 전에 발생하고,-ed로 끝나는 이벤트는 반영된 후에 발생합니다.
모델 이벤트를 수신하려면 Eloquent 모델에 $dispatchesEvents 프로퍼티를 정의하세요. 이 프로퍼티는 모델 라이프사이클의 각 시점을 이벤트 클래스에 매핑합니다. 각 이벤트 클래스는 생성자에서 해당 모델 인스턴스를 받아야 합니다:
<?php
namespace App\Models;
use App\Events\UserDeleted;
use App\Events\UserSaved;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
/**
* 모델 이벤트 매핑 목록
*
* @var array<string, string>
*/
protected $dispatchesEvents = [
'saved' => UserSaved::class,
'deleted' => UserDeleted::class,
];
}이벤트 클래스를 정의하고 매핑한 뒤에는 이벤트 리스너를 통해 이벤트를 처리할 수 있습니다.
WARNING
Eloquent로 일괄 업데이트(mass update) 또는 일괄 삭제(mass delete) 쿼리를 실행하면, 영향받는 모델에 대해 saved, updated, deleting, deleted 이벤트가 발생하지 않습니다. 일괄 처리 시에는 모델 인스턴스를 실제로 조회하지 않기 때문입니다.
클로저로 이벤트 등록하기
별도의 이벤트 클래스를 만들지 않고, 모델의 booted 메서드 안에서 클로저를 직접 등록할 수도 있습니다:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* 모델 부트 시 실행
*/
protected static function booted(): void
{
static::created(function (User $user) {
// 사용자 생성 후 처리 로직
});
}
}필요하다면 큐 처리 가능한 익명 이벤트 리스너를 사용할 수도 있습니다. 이렇게 하면 Laravel이 해당 리스너를 애플리케이션의 큐를 통해 백그라운드에서 실행합니다:
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
{
// ...
}
}옵저버를 등록하는 방법은 두 가지입니다.
방법 1: 모델에 ObservedBy 어트리뷰트를 붙이는 방법 (권장):
use App\Observers\UserObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
#[ObservedBy([UserObserver::class])]
class User extends Authenticatable
{
//
}방법 2: AppServiceProvider의 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();