본문으로 건너뛰기
버전: 10.x

Eloquent: 팩토리 (Eloquent: Factories)

소개

애플리케이션을 테스트하거나 데이터베이스를 시딩할 때, 데이터베이스에 몇 개의 레코드를 삽입해야 할 수 있습니다. 각 컬럼의 값을 일일이 지정하는 대신, 라라벨에서는 각 Eloquent 모델에 대해 모델 팩토리를 정의하여 기본 속성 값을 지정할 수 있습니다.

팩토리 작성 예를 보고 싶다면, 애플리케이션의 database/factories/UserFactory.php 파일을 확인해 보시기 바랍니다. 이 팩토리는 모든 새로운 라라벨 프로젝트에 포함되어 있으며 아래와 같은 정의를 가지고 있습니다:

namespace Database\Factories;

use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
];
}
}

보시다시피, 팩토리는 기본적으로 라라벨의 팩토리 기본 클래스를 상속받아 definition 메서드를 정의하는 클래스입니다. definition 메서드는 팩토리로 모델을 생성할 때 적용할 기본 속성 값을 반환합니다.

팩토리에서는 fake 헬퍼를 통해 Faker PHP 라이브러리를 사용할 수 있습니다. 이를 통해 테스트 및 데이터 시딩을 위해 여러 종류의 임의 데이터를 생성할 수 있습니다.

[!NOTE] 애플리케이션의 Faker 로케일(locale)은 config/app.php 설정 파일에 faker_locale 옵션을 추가하여 설정할 수 있습니다.

모델 팩토리 정의하기

팩토리 생성하기

팩토리를 생성하려면, make:factory Artisan 명령어를 실행합니다:

php artisan make:factory PostFactory

새로 생성된 팩토리 클래스는 database/factories 디렉터리에 생성됩니다.

모델과 팩토리의 관례

팩토리를 정의한 후에는, 모델에 Illuminate\Database\Eloquent\Factories\HasFactory 트레이트를 적용하면 제공되는 정적 factory 메서드를 사용하여 해당 모델의 팩토리 인스턴스를 생성할 수 있습니다.

HasFactory 트레이트의 factory 메서드는 관례를 기반으로, 할당된 모델에 대한 적절한 팩토리를 찾습니다. 구체적으로 이 메서드는 Database\Factories 네임스페이스 아래에서 모델명과 동일하면서 Factory로 끝나는 클래스를 찾습니다. 만약 이러한 관례가 애플리케이션에 맞지 않으면, 모델에서 newFactory 메서드를 직접 오버라이드(재정의)하여 해당 모델에 대한 팩토리 인스턴스를 직접 반환할 수 있습니다:

use Illuminate\Database\Eloquent\Factories\Factory;
use Database\Factories\Administration\FlightFactory;

/**
* Create a new factory instance for the model.
*/
protected static function newFactory(): Factory
{
return FlightFactory::new();
}

그리고, 해당 팩토리에서 model 속성을 명시적으로 지정해줍니다:

use App\Administration\Flight;
use Illuminate\Database\Eloquent\Factories\Factory;

class FlightFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<\Illuminate\Database\Eloquent\Model>
*/
protected $model = Flight::class;
}

팩토리 상태(state) 사용하기

상태(state) 조작 메서드를 사용하면 하나의 모델 팩토리에 대해 다양한 변경점을 조합해서 적용할 수 있습니다. 예를 들어, Database\Factories\UserFactory 팩토리에 기본 속성 중 하나를 변경하는 suspended 상태 메서드를 정의할 수 있습니다.

상태 변환 메서드는 일반적으로 라라벨의 팩토리 기본 클래스에서 제공하는 state 메서드를 호출합니다. state 메서드는 팩토리에 정의된 속성 배열을 입력으로 받는 클로저(익명 함수)를 인자로 받고, 수정할 속성을 포함한 배열을 반환해야 합니다:

use Illuminate\Database\Eloquent\Factories\Factory;

/**
* Indicate that the user is suspended.
*/
public function suspended(): Factory
{
return $this->state(function (array $attributes) {
return [
'account_status' => 'suspended',
];
});
}

"Trashed" 상태

만약 Eloquent 모델이 소프트 삭제를 지원한다면, 내장된 trashed 상태 메서드를 호출하여 생성된 모델이 소프트 삭제된 상태가 되도록 할 수 있습니다. 별도로 trashed 상태를 정의하지 않아도 모든 팩토리에서 자동으로 사용할 수 있습니다:

use App\Models\User;

$user = User::factory()->trashed()->create();

팩토리 콜백(callbacks)

팩토리 콜백은 afterMakingafterCreating 메서드를 사용해 등록하며, 모델을 만들거나 생성한 후에 추가적인 작업을 수행할 수 있도록 도와줍니다. 이러한 콜백은 팩토리 클래스에서 configure 메서드를 정의하여 등록하며, 팩토리가 인스턴스화될 때 라라벨이 자동으로 호출합니다:

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
/**
* Configure the model factory.
*/
public function configure(): static
{
return $this->afterMaking(function (User $user) {
// ...
})->afterCreating(function (User $user) {
// ...
});
}

// ...
}

또한, 개별 상태(state) 메서드에서도 팩토리 콜백을 등록할 수 있습니다. 이를 사용하면 특정 상태에서만 필요한 추가 작업을 수행할 수 있습니다:

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
* Indicate that the user is suspended.
*/
public function suspended(): Factory
{
return $this->state(function (array $attributes) {
return [
'account_status' => 'suspended',
];
})->afterMaking(function (User $user) {
// ...
})->afterCreating(function (User $user) {
// ...
});
}

팩토리를 사용한 모델 생성

모델 인스턴스화하기

팩토리를 정의했다면, Illuminate\Database\Eloquent\Factories\HasFactory 트레이트를 추가한 모델의 정적 factory 메서드를 사용하여 팩토리 인스턴스를 생성할 수 있습니다. 몇 가지 예시를 살펴보겠습니다. 먼저, make 메서드로 데이터베이스에 저장하지 않고 모델 인스턴스만 생성해보겠습니다:

use App\Models\User;

$user = User::factory()->make();

count 메서드를 이용하면 여러 개의 모델 컬렉션을 생성할 수 있습니다:

$users = User::factory()->count(3)->make();

상태(state) 적용하기

정의한 상태를 모델에 적용할 수도 있습니다. 여러 상태 변환을 동시에 적용하고 싶다면, 원하는 상태 변환 메서드를 연이어 호출하면 됩니다:

$users = User::factory()->count(5)->suspended()->make();

속성 덮어쓰기

모델의 기본 값을 일부만 직접 지정하고 싶다면, make 메서드에 배열로 값을 지정해주면 됩니다. 지정한 속성만 새 값으로 덮어쓰고, 나머지는 팩토리에서 지정한 기본 값이 유지됩니다:

$user = User::factory()->make([
'name' => 'Abigail Otwell',
]);

또는, 팩토리 인스턴스에서 직접 state 메서드를 호출하여 인라인 상태 변환을 수행할 수도 있습니다:

$user = User::factory()->state([
'name' => 'Abigail Otwell',
])->make();

[!NOTE] 팩토리를 통해 모델을 생성할 때는 대량 할당 보호가 자동으로 비활성화됩니다.

모델 저장하기

create 메서드를 사용하면, 모델 인스턴스를 생성하고 Eloquent의 save 메서드로 데이터베이스에 저장합니다:

use App\Models\User;

// 단일 App\Models\User 인스턴스 생성...
$user = User::factory()->create();

// 세 개의 App\Models\User 인스턴스 생성...
$users = User::factory()->count(3)->create();

create 메서드에도 속성 배열을 넘겨주어 팩토리의 기본 속성 값을 덮어쓸 수 있습니다:

$user = User::factory()->create([
'name' => 'Abigail',
]);

시퀀스(Sequences)

여러 모델을 생성할 때 특정 속성 값을 반복적으로 교차 변경하고 싶을 때는, 상태 변환을 시퀀스(Sequence)로 정의할 수 있습니다. 예를 들어, 사용자 각각의 admin 컬럼 값을 YN으로 번갈아 설정하고 싶다면 다음과 같이 작성할 수 있습니다:

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Sequence;

$users = User::factory()
->count(10)
->state(new Sequence(
['admin' => 'Y'],
['admin' => 'N'],
))
->create();

위 예제에서는 5명의 사용자는 admin 값이 Y, 나머지 5명은 N으로 설정됩니다.

필요하다면 시퀀스 값으로 클로저(익명 함수)를 사용할 수도 있습니다. 시퀀스를 사용할 때마다 클로저가 실행되어 새로운 값을 반환합니다:

use Illuminate\Database\Eloquent\Factories\Sequence;

$users = User::factory()
->count(10)
->state(new Sequence(
fn (Sequence $sequence) => ['role' => UserRoles::all()->random()],
))
->create();

시퀀스 클로저 내부에서는 $sequence 인스턴스의 $index 또는 $count 속성에 접근할 수 있습니다. $index는 지금까지 시퀀스가 몇 번째 순환인지, $count는 시퀀스가 전체 몇 번 실행될 것인지를 나타냅니다:

$users = User::factory()
->count(10)
->sequence(fn (Sequence $sequence) => ['name' => 'Name '.$sequence->index])
->create();

편의상 sequence 메서드를 사용해도 동일한 효과를 낼 수 있습니다. sequence 메서드는 내부적으로 state를 호출하며, 클로저 또는 속성 배열들을 인자로 받습니다:

$users = User::factory()
->count(2)
->sequence(
['name' => 'First User'],
['name' => 'Second User'],
)
->create();

팩토리의 연관관계 다루기

Has Many 연관관계 정의

이제 라라벨의 fluet한 팩토리 메서드를 사용해서 Eloquent 모델 간의 연관관계를 어떻게 구성할 수 있는지 알아보겠습니다. 먼저, App\Models\User 모델과 App\Models\Post 모델을 간단히 가정하겠습니다. 그리고 User 모델이 PosthasMany 연관관계를 가진다고 하면, 아래와 같이 has 메서드로 게시글 3개를 가진 사용자를 생성할 수 있습니다. has 메서드에는 팩토리 인스턴스를 전달합니다:

use App\Models\Post;
use App\Models\User;

$user = User::factory()
->has(Post::factory()->count(3))
->create();

관례상, has 메서드에 Post 모델을 전달하면 라라벨은 User 모델이 posts 메서드를 통해 해당 연관관계를 정의한 것으로 간주합니다. 필요하다면 조작할 연관관계 메서드명을 명시적으로 지정할 수도 있습니다:

$user = User::factory()
->has(Post::factory()->count(3), 'posts')
->create();

물론, 연관된 모델에도 상태(state) 변환을 적용할 수 있습니다. 또한, 만약 연관된 모델의 상태 변경 시 상위(부모) 모델에 접근해야 한다면, 클로저 기반 상태 변환도 사용할 수 있습니다:

$user = User::factory()
->has(
Post::factory()
->count(3)
->state(function (array $attributes, User $user) {
return ['user_type' => $user->type];
})
)
->create();

매직 메서드 사용하기

편의상, 라라벨이 제공하는 연관관계용 매직 팩토리 메서드를 이용해 관계를 생성할 수도 있습니다. 아래와 같은 예시는 posts 연관관계 메서드를 관례에 따라 자동으로 사용합니다:

$user = User::factory()
->hasPosts(3)
->create();

매직 메서드를 사용할 때, 관련 모델의 속성을 배열로 덮어쓸 수도 있습니다:

$user = User::factory()
->hasPosts(3, [
'published' => false,
])
->create();

상태 변환에서 부모 모델에 접근이 필요한 경우, 클로저로 두 번째 인자를 전달할 수도 있습니다:

$user = User::factory()
->hasPosts(3, function (array $attributes, User $user) {
return ['user_type' => $user->type];
})
->create();

Belongs To 연관관계 정의

"has many" 연관관계를 다루는 방법을 살펴봤다면, 이제 그 반대인 "belongs to" 관계도 정의해보겠습니다. for 메서드를 사용하면 팩토리로 생성된 모델이 소속될 부모 모델을 지정할 수 있습니다. 예를 들어, 하나의 사용자에게 속한 3개의 App\Models\Post 모델 인스턴스를 만들 수 있습니다:

use App\Models\Post;
use App\Models\User;

$posts = Post::factory()
->count(3)
->for(User::factory()->state([
'name' => 'Jessica Archer',
]))
->create();

이미 부모 모델 인스턴스를 가지고 있다면, 해당 인스턴스를 직접 for 메서드에 전달해도 됩니다:

$user = User::factory()->create();

$posts = Post::factory()
->count(3)
->for($user)
->create();

매직 메서드 사용하기

편리하게도 매직 메서드를 사용해서 "belongs to" 관계도 정의할 수 있습니다. 아래 예시에서는 user 관계명을 자동으로 적용해 3개의 게시글이 동일한 사용자를 참조하도록 합니다:

$posts = Post::factory()
->count(3)
->forUser([
'name' => 'Jessica Archer',
])
->create();

Many to Many 연관관계 정의

Has many 연관관계와 마찬가지로, "many to many" 관계도 has 메서드를 사용해서 생성할 수 있습니다:

use App\Models\Role;
use App\Models\User;

$user = User::factory()
->has(Role::factory()->count(3))
->create();

Pivot 테이블 속성 지정

연결(pivot) 테이블에 값이 기록되어야 한다면, hasAttached 메서드를 사용할 수 있습니다. 이 메서드는 두 번째 인자로 피벗(pivot) 테이블에 저장할 속성 배열을 받습니다:

use App\Models\Role;
use App\Models\User;

$user = User::factory()
->hasAttached(
Role::factory()->count(3),
['active' => true]
)
->create();

관련 모델에 접근해서 클로저 기반 상태 변환을 적용하고 싶을 때도 사용할 수 있습니다:

$user = User::factory()
->hasAttached(
Role::factory()
->count(3)
->state(function (array $attributes, User $user) {
return ['name' => $user->name.' Role'];
}),
['active' => true]
)
->create();

이미 생성된 모델 인스턴스를 연결하고 싶다면, hasAttached에 모델 인스턴스를 그대로 넘기면 됩니다. 아래 예시에서는 동일한 3개의 역할(role)이 3명의 사용자 모두와 연결됩니다:

$roles = Role::factory()->count(3)->create();

$user = User::factory()
->count(3)
->hasAttached($roles, ['active' => true])
->create();

매직 메서드 사용하기

매직 팩토리 메서드를 사용해 many to many 관계도 쉽게 정의할 수 있습니다. 아래 예시에서는 관례상 roles 연관관계 메서드를 자동으로 사용합니다:

$user = User::factory()
->hasRoles(1, [
'name' => 'Editor'
])
->create();

폴리모픽 연관관계(Polymorphic Relationships)

폴리모픽 연관관계 역시 팩토리로 손쉽게 생성할 수 있습니다. 폴리모픽 "morph many" 관계는 일반적인 "has many"와 같은 방식으로 구현하면 됩니다. 예를 들어, App\Models\Post 모델이 App\Models\Comment와 morphMany 관계를 가지면:

use App\Models\Post;

$post = Post::factory()->hasComments(3)->create();

Morph To 관계

매직 메서드는 morphTo 연관관계 생성에는 사용할 수 없습니다. 이 경우에는 for 메서드를 명시적으로 사용하고, 관계명을 직접 지정해야 합니다. 예를 들어, Comment 모델에 commentable이라는 morphTo 관계가 있다면, 아래와 같이 3개의 댓글을 하나의 게시글에 연결할 수 있습니다:

$comments = Comment::factory()->count(3)->for(
Post::factory(), 'commentable'
)->create();

폴리모픽 Many to Many 연관관계

폴리모픽 "many to many"(morphToMany / morphedByMany) 관계도 일반 many to many와 동일하게 생성할 수 있습니다:

use App\Models\Tag;
use App\Models\Video;

$videos = Video::factory()
->hasAttached(
Tag::factory()->count(3),
['public' => true]
)
->create();

물론, 매직 has 메서드로도 폴리모픽 "many to many" 관계를 생성할 수 있습니다:

$videos = Video::factory()
->hasTags(3, ['public' => true])
->create();

팩토리 내부에서 연관관계 정의하기

모델 팩토리 내에서 연관관계를 정의할 때는 보통 해당 관계의 외래키(foreign key) 필드에 새로운 팩토리 인스턴스를 할당합니다. 이는 belongsTomorphTo와 같은 "역관계(inverse relationship)"에 주로 사용됩니다. 예를 들어, 게시글을 생성할 때 새로운 사용자를 함께 생성하려면 다음과 같이 작성할 수 있습니다:

use App\Models\User;

/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => fake()->title(),
'content' => fake()->paragraph(),
];
}

관계 컬럼 값이 팩토리에서 평가된 속성 배열에 따라 달라져야 한다면, 해당 속성에 클로저를 할당하면 됩니다. 이 때 클로저는 팩토리에서 평가된 속성 배열을 인자로 받습니다:

/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'user_type' => function (array $attributes) {
return User::find($attributes['user_id'])->type;
},
'title' => fake()->title(),
'content' => fake()->paragraph(),
];
}

관계에서 기존 모델 재활용하기

두 개 이상의 모델이 동일한 연관 모델을 공유해야 할 때는 recycle 메서드를 사용해, 팩토리가 관계를 생성할 때 항상 동일한 인스턴스를 재사용하도록 할 수 있습니다.

예를 들어, Airline, Flight, Ticket 모델이 있고, 각 티켓과 플라이트가 같은 항공사를 참조해야 하는 경우, 항공사 인스턴스를 recycle 메서드에 전달하면 됩니다:

Ticket::factory()
->recycle(Airline::factory()->create())
->create();

특정 사용자의 모든 모델, 또는 특정 팀의 모델처럼 공통의 관계 인스턴스를 사용하는 경우 특히 유용합니다.

recycle 메서드는 기존 모델의 컬렉션도 받을 수 있습니다. 이 경우 컬렉션에서 무작위로 하나의 모델이 선택되어 팩토리에서 해당 타입의 모델이 필요할 때 사용됩니다:

Ticket::factory()
->recycle($airlines)
->create();