Eloquent 모델은 인스턴스화될 때마다 “부팅 처리(boot)“와 “초기화 처리(initialize)“를 실행합니다. 트레이트에 이러한 메서드를 정의해 두면, 모델이 해당 트레이트를 use 하는 것만으로 자동으로 호출됩니다.
이 메커니즘을 활용하면 모델에 기능을 주입하는 트레이트를 깔끔하게 구현할 수 있습니다. Laravel 프레임워크 자체도 SoftDeletes 등 많은 트레이트에서 이 패턴을 활용하고 있습니다.
boot와 initialize의 차이
| bootXxx() | initializeXxx() |
|---|
| 호출 시점 | 모델 클래스가 최초로 부팅될 때(클래스당 1회) | 인스턴스가 생성될 때마다 |
| 메서드 종류 | static | 인스턴스 메서드 |
| 용도 | 이벤트 리스너 등록, 글로벌 스코프 추가 등 | 기본값 설정, 프로퍼티 초기화 등 |
네이밍 규약
bootXxx()
트레이트 이름 앞에 boot 접두어를 붙인 static 메서드를 정의하면, 모델 부팅 시 한 번만 호출됩니다.
namespace App\Models\Concerns;
trait HasSlug
{
public static function bootHasSlug(): void
{
static::creating(function ($model) {
if (empty($model->slug)) {
$model->slug = str($model->name)->slug()->toString();
}
});
}
}
모델에서의 사용 예:
use App\Models\Concerns\HasSlug;
class Article extends Model
{
use HasSlug;
// bootHasSlug() 는 자동으로 호출된다
}
initializeXxx()
트레이트 이름 앞에 initialize 접두어를 붙인 인스턴스 메서드를 정의하면, 모델을 new 할 때마다 호출됩니다.
namespace App\Models\Concerns;
trait HasDefaultStatus
{
public function initializeHasDefaultStatus(): void
{
$this->attributes['status'] ??= 'draft';
}
}
$casts 에 동적으로 추가할 때에도 사용할 수 있습니다:
trait HasMetadata
{
public function initializeHasMetadata(): void
{
$this->mergeCasts([
'metadata' => 'array',
]);
}
}
PHP Attribute를 통한 명시적 지정
Laravel 11부터는 네이밍 규약에 의존하지 않고 PHP Attribute로 메서드를 명시적으로 지정할 수 있습니다.
| Attribute | 대상 |
|---|
#[Boot] | 클래스 부팅 시 한 번만 실행할 메서드 |
#[Initialize] | 인스턴스 생성 시마다 실행할 메서드 |
use Illuminate\Database\Eloquent\Attributes\Boot;
use Illuminate\Database\Eloquent\Attributes\Initialize;
trait HasAuditLog
{
#[Boot]
public static function registerAuditListeners(): void
{
static::created(function ($model) {
AuditLog::record('created', $model);
});
static::updated(function ($model) {
AuditLog::record('updated', $model);
});
}
#[Initialize]
public function setAuditDefaults(): void
{
$this->attributes['audit_enabled'] ??= true;
}
}
PHP Attribute를 사용하면 메서드 이름이 트레이트 이름에 얽매이지 않기 때문에, 의미 있는 이름을 붙일 수 있습니다. 여러 개의 #[Boot] 메서드나 #[Initialize] 메서드를 같은 트레이트 안에 정의하는 것도 가능합니다.
실전 예제: 패키지 개발에서의 활용
글로벌 스코프의 자동 추가
trait HasTenant
{
public static function bootHasTenant(): void
{
static::addGlobalScope(new TenantScope);
}
}
기본 캐스트 추가
trait HasJsonSettings
{
public function initializeHasJsonSettings(): void
{
$this->mergeCasts([
'settings' => 'array',
]);
}
}
Eloquent 이벤트로 자동 처리
trait Archivable
{
#[Boot]
public static function bootArchivable(): void
{
static::deleting(function ($model) {
Archive::store($model->toArray());
});
}
}
Laravel 프레임워크 내의 예시
Laravel 프레임워크 자체도 많은 트레이트에서 이 패턴을 채택하고 있습니다.
| 트레이트 | 메서드 | 처리 내용 |
|---|
SoftDeletes | bootSoftDeletes() | 소프트 삭제용 글로벌 스코프 추가 |
HasFactory | bootHasFactory() | 특별한 처리 없음(범위 외 메커니즘) |
HasUlids | bootHasUlids() | creating 이벤트에서 ULID를 자동 설정 |
HasUuids | bootHasUuids() | creating 이벤트에서 UUID를 자동 설정 |
이들을 참고 구현으로 읽어 보면, 패턴에 대한 이해가 더 깊어집니다.
주의 사항
모델 캐시 초기화
boot* 는 클래스마다 한 번만 실행되기 때문에, 테스트 환경에서는 주의가 필요합니다. Model::clearBootedModels() 로 부팅된 캐시를 리셋할 수 있습니다.
// 테스트 종료 후 모델의 부팅 상태를 리셋한다
protected function tearDown(): void
{
Model::clearBootedModels();
parent::tearDown();
}
실행 순서
같은 모델에 여러 트레이트가 use 되어 있을 경우, bootXxx() / initializeXxx() 는 PHP의 트레이트 해결 순서(선언 순서)로 호출됩니다. 의존 관계가 있다면 use 순서에 주의하시기 바랍니다.
| 패턴 | 방법 | 적용 상황 |
|---|
| 클래스 부팅 시 처리 | static bootXxx() 또는 #[Boot] | 이벤트 등록, 글로벌 스코프 추가 |
| 인스턴스 초기화 | initializeXxx() 또는 #[Initialize] | 기본값, 캐스트 추가 |
트레이트에 bootXxx() / initializeXxx() 를 구현해 두면, 모델에 기능을 주입하는 일이 use 한 줄로 끝나게 됩니다. 패키지에서 모델에 기능을 추가할 때의 정석 패턴입니다.