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

Eloquent: 팩토리 (Eloquent: Factories)

소개

애플리케이션을 테스트하거나 데이터베이스 시딩을 할 때, 데이터베이스에 여러 레코드를 삽입해야 할 경우가 있습니다. 모든 컬럼 값을 직접 지정하지 않고, 라라벨에서는 각 Eloquent 모델에 대해 모델 팩토리를 통해 기본 속성 집합을 정의할 수 있도록 지원합니다.

팩토리를 작성하는 방법을 알고 싶다면, 애플리케이션의 database/factories/UserFactory.php 파일을 참고하세요. 이 팩토리는 모든 새로운 라라벨 애플리케이션에 기본으로 포함되어 있으며, 다음과 같이 팩토리가 정의되어 있습니다.

namespace Database\Factories;

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

class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
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 로케일은 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 Database\Factories\Administration\FlightFactory;

/**
* Create a new factory instance for the model.
*
* @return \Illuminate\Database\Eloquent\Factories\Factory
*/
protected static function newFactory()
{
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 string
*/
protected $model = Flight::class;
}

팩토리 상태

상태(States) 조작 메서드를 사용하면, 모델 팩토리의 속성 값을 다양한 조합으로 선택적으로 수정할 수 있습니다. 예를 들어, Database\Factories\UserFactory에서 기본 속성 값을 변경하는 suspended 상태 메서드를 추가할 수 있습니다.

상태 변환 메서드는 주로 라라벨의 기본 팩토리 클래스가 제공하는 state 메서드를 호출하여 구현합니다. state 메서드는 현재 팩토리의 원시 속성 배열을 파라미터로 전달하는 클로저를 인자로 받으며, 변경할 속성 배열을 반환해야 합니다.

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

"Trashed" 상태

Eloquent 모델이 소프트 삭제를 지원한다면, 생성된 모델을 "소프트 삭제" 상태로 만들기 위해 내장된 trashed 상태 메서드를 사용할 수 있습니다. 이 상태는 모든 팩토리에서 자동으로 사용할 수 있으므로 별도로 정의할 필요가 없습니다.

use App\Models\User;

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

팩토리 콜백

팩토리 콜백은 afterMakingafterCreating 메서드를 사용해 등록할 수 있으며, 모델을 생성(make)하거나 저장(create)한 후 추가 작업을 할 수 있게 해줍니다. 이러한 콜백들은 팩토리 클래스 안에 configure 메서드를 정의함으로써 등록할 수 있습니다. 라라벨은 팩토리 인스턴스를 생성할 때 자동으로 이 메서드를 호출합니다.

namespace Database\Factories;

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

class UserFactory extends Factory
{
/**
* Configure the model factory.
*
* @return $this
*/
public function configure()
{
return $this->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();

상태 적용하기

모델을 생성할 때, 원하는 상태를 적용할 수도 있습니다. 여러 상태 변환을 동시에 적용하고 싶다면, 해당 상태 메서드를 연속적으로 호출하면 됩니다.

$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 인스턴스 3개 생성 및 저장...
$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으로 생성됩니다.

필요하다면, 시퀀스 값으로 클로저를 사용할 수도 있습니다. 이 클로저는 시퀀스가 새 값을 필요로 할 때마다 호출됩니다.

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

시퀀스 클로저 안에서는, 시퀀스 인스턴스에 주입된 $index$count 속성을 사용할 수 있습니다. $index는 지금까지 시퀀스가 반복된 횟수를, $count는 시퀀스가 호출될 총 횟수를 나타냅니다.

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

편의상, 시퀀스는 sequence 메서드를 이용해 적용할 수도 있습니다. 이 메서드는 내부적으로 state 메서드를 호출합니다. sequence 메서드는 클로저나, 순차적으로 적용할 속성들의 배열을 받을 수 있습니다.

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

팩토리로 관계 정의하기

Has Many 관계

이제 라라벨의 플루언트 팩토리 메서드를 사용해 Eloquent 모델의 관계를 만들어보겠습니다. 먼저, 애플리케이션에 App\Models\UserApp\Models\Post 모델이 있다고 가정하겠습니다. 또한, User 모델이 PosthasMany 관계를 정의한다고 할 때, 사용자가 3개의 포스트를 가지게 하려면 팩토리의 has 메서드를 이용할 수 있습니다. has 메서드는 팩토리 인스턴스를 인자로 받습니다.

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

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

관례상, Post 모델을 has 메서드에 전달하면, 라라벨은 User 모델에 반드시 posts 메서드가 있어야 한다고 가정합니다. 만약 조작하려는 관계명을 명시하고 싶다면 두 번째 인자로 관계 이름을 지정할 수 있습니다.

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

물론, 관계 모델에도 상태 변경을 적용할 수 있습니다. 부모 모델 데이터에 따라 관계 모델의 속성을 변경하려면, 클로저를 이용한 상태 변환을 전달할 수 있습니다.

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

매직 메서드 사용하기

더 간편하게 팩토리 관계를 설정하고 싶다면, 라라벨의 매직 팩토리 관계 메서드를 사용할 수 있습니다. 아래 예시는 User 모델에 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" 관계를 팩토리로 만드는 방법을 살펴봤으니, 이제 그 반대 관계도 알아보겠습니다. for 메서드를 사용하면 팩토리가 생성하는 모델이 어떤 부모 모델에 속하는지를 지정할 수 있습니다. 예를 들어, App\Models\Post 모델 인스턴스 3개를 생성하면서, 이들이 모두 하나의 사용자에 속하도록 할 수 있습니다.

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" 관계를 지정하고 싶다면, 라라벨의 매직 팩토리 메서드를 사용할 수 있습니다. 예를 들어 아래 예시는 3개의 포스트가 모두 Post 모델의 user 관계에 속하게 생성됩니다.

$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 메서드에 그 인스턴스들을 넘길 수 있습니다. 아래 예시처럼 세 개의 역할(role)이 각각 세 명의 사용자에 모두 연결됩니다.

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

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

매직 메서드 사용하기

다대다 관계를 더욱 쉽게 정의하려면, 라라벨의 매직 팩토리 메서드를 사용할 수 있습니다. 아래 예시는 User 모델의 roles 관계를 통해 관련 모델을 생성합니다.

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

폴리모픽(Polymorphic) 관계

폴리모픽 관계도 팩토리를 이용해 만들 수 있습니다. 폴리모픽 "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 관계가 있다면, 다음과 같이 해당 관계에 속한 댓글을 생성할 수 있습니다.

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

폴리모픽 다대다 관계

폴리모픽 "many to many"(morphToMany / morphedByMany) 관계 역시 일반 다대다 관계와 마찬가지로 생성할 수 있습니다.

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

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

물론, 매직 has 메서드를 사용해서도 폴리모픽 다대다 관계를 생성할 수 있습니다.

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

팩토리 내부에서 관계 정의

팩토리 내부에서 관계를 정의할 때는, 보통 외래 키(foreign key)에 새 팩토리 인스턴스를 할당합니다. 이는 주로 belongsTomorphTo 같은 "역방향" 관계에 적용됩니다. 예를 들어, 포스트를 생성할 때 새 사용자를 함께 생성하고 싶다면 아래와 같이 작업할 수 있습니다.

use App\Models\User;

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

관계의 컬럼 값이 팩토리에서 정의된 데이터에 따라 달라져야 할 때는, 속성에 클로저를 할당할 수도 있습니다. 이때 클로저에는 팩토리에서 평가된 속성 배열이 전달됩니다.

/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
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 메서드는 매우 유용합니다.

recycle 메서드는 기존 모델의 컬렉션도 인자로 받을 수 있습니다. 컬렉션이 주어지면, 팩토리가 해당 타입의 모델이 필요할 때마다 컬렉션에서 임의의 모델 하나를 선택해서 사용하게 됩니다.

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