Laravel Lazy Loading Eager Loading (N+1 Query)



라라벨에서 Builder 로 데이터를 조회할 때

Lazy Loading (지연 로딩) 과 Eager Loading (즉시 로딩) 의 차이점을 알아보겠습니다.

기본적으로 라라벨에서는 Lazy Loading 을 사용합니다.


Lazy Loading

지연 로딩은 연관된 데이터에 접근하지 전에는 로딩하지 않습니다.

Eager Loading

즉시 로딩은 연관된 데이터를 한번에 로딩합니다.

Lazy LoadingEager Loading 의 차이점을 알기 위해 실제로 데이터를 생성한 뒤 테스트를 해보겠습니다.


테스트 환경 구축

관계 맺을 두 개의 모델을 준비합니다.

UserPostN:1 관계를 맺고 있습니다. (HasMany)

Post 모델을 생성합니다. 이 때 mf 옵션도 포함합니다.

$ php artisan make:model -mf Post

위 옵션이 포함되지 않은 경우 아래처럼 두 개의 artisan 명령어로 추가 생성을 해야합니다.

$ php artisan make:migration create_posts_table # m
$ php artisan make:factory PostFactory # f

Post 의 데이터 생성을 위해 생성한 팩토리의 definition 메서드를 구현합니다.

// database/factories/PostFactory.php
public function definition()
{
    return [
        'user_id' => User::factory(),
        'title' => $this->faker->sentence(),
        'content' => $this->faker->realText(),
        'ip' => $this->faker->ipv4(),
    ];
}

그 후 UserFactory 를 이용해 데이터를 생성하기 위한 Seeder 를 생성합니다.

$ php artisan make:seeder UserSeeder

생성한 시더에 run 메서드를 구현합니다.

// database/seeders/UserSeeder.php
public function run()
{
    User::factory()
        ->count(100) // 100개 생성
        ->hasPosts() // 1:N 관계를 맺은 Post 모델을 가집니다.
        ->create(); // User 모델 데이터를 생성
}

라라벨에서 seed 명령어를 실행 시 DatabaseSeeder 를 호출하게 됩니다.

해당 클래스의 run 메서드 안에서 Seeder 를 구현해도 상관없지만,

따로 생성하여 구현한 UserSeederrun 메서드에서 호출하겠습니다.

// DatabaseSeeder.php
public function run()
{
    $this->call([
        UserSeeder::class,
    ]);
}

아래의 명령어는 마이그레이션 및 DatabaseSeeder (—seed) 를 호출합니다.

$ php artisan migrate --seed

이제 각 테이블에 데이터가 생성된 것을 확인할 수 있습니다.

이제 UserPost 모델에서 1:N (일대다) 연관 관계를 정의하겠습니다.


// app/Models/User.php
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    .
    .
    .

    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

// app/Models/Post.php
class Post extends Model
{
    use HasFactory;

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

이제 생성된 데이터를 이용해 실제로 Lazy LoadingEager Loading 의 동작을 살펴보겠습니다.


Lazy Loading (지연 로딩)

public function test_lazy_loading()
{
    DB::enableQueryLog();

    $posts = Post::query()
        ->get();

    foreach ($posts as $post) {
        echo $post->user->name;
    }

    dump(DB::getQueryLog());
}

Query

Post 모델 조회

select * from `posts`

Post 모델 조회 후 각 객체에 접근하여 User 조회

1개Post 에 대해 N개User 조회 (N+1)

select * from `users` where `users`.`id` = 1 limit 1
select * from `users` where `users`.`id` = 2 limit 1
.
.
.
.
select * from `users` where `users`.`id` = 100 limit 1

Result

대략 4초 소요

Untitled


Eager Loading (즉시 로딩)

public function test_eager_loading()
{
    DB::enableQueryLog();

    $posts = Post::query()
        ->with('user')
        ->get();

    foreach ($posts as $post) {
        echo $post->user->name;
    }

    dump(DB::getQueryLog());
}

Eloquent ORMQuery Builder 에서는 이러한 N+1 문제를 해결할 수 있는 메서드를 지원합니다.

위 코드를 DML 로 확인을 해보겠습니다.

Query

Post 모델 조회

select * from `posts`

모델과 관계를 맺은 Foreign Key 로 연관 모델 조회

select * from `users` where `users`.`id` in (1, 2, 3, 4, 5, ...)

Result

500ms 소요

Untitled


마침

이렇게 실제 데이터를 통해 Lazy LoadingEager Loading 에 대해 알아보았습니다.

N+1 의 문제점은 Eager Loading 를 통해 해결할 수 있습니다.

또한 라라벨에서는 모델을 로딩할 때 연관 모델을 항상 Eager Loading 할 수 있습니다.

https://laravel.kr/docs/9.x/eloquent-relationships#eager-loading-by-default