Laravel 성능 마스터: N+1 쿼리 방지를 위한 즉시 로딩 심층 분석

발행: (2026년 6월 5일 PM 10:20 GMT+9)
7 분 소요
원문: Dev.to

Source: Dev.to

Laravel 성능 마스터하기: N+1 쿼리 방지를 위한 Eager Loading 심층 탐구

Introduction

Laravel는 우아한 문법과 개발자 친화적인 기능으로 유명하며, 인상적인 속도로 견고한 웹 애플리케이션을 구축할 수 있게 해줍니다. 하지만 가장 아름답게 만든 Laravel 애플리케이션이라도 숨겨진 성능 병목 현상에 시달릴 수 있습니다. 가장 교활하고 흔한 원인 중 하나가 바로 **“N+1 쿼리 문제”**입니다. 이는 Eloquent 모델 컬렉션을 가져온 뒤, 루프 안에서 각 아이템에 대한 연관 모델에 접근할 때 발생합니다. Laravel은 기본적으로 부모 모델을 위한 하나의 쿼리(“1”)와 연관 모델 각각을 위한 N개의 별도 쿼리(“N”)를 실행하게 되며, 이로 인해 데이터베이스 부하가 급격히 증가하고 페이지 렌더링이 느려집니다. 확장 가능한 애플리케이션에서는 이것이 치명적인 결함이 됩니다.

다행히 Laravel은 eager loading이라는 우아하고 강력한 해결책을 제공합니다. Eloquent에게 관계를 미리 로드하도록 명시하면, 수백 개의 개별 쿼리를 단 두 개의 효율적인 쿼리(또는 하나의 조인)로 변환할 수 있어 데이터베이스 왕복 횟수를 크게 줄이고 애플리케이션 성능을 크게 향상시킵니다. 이 튜토리얼에서는 N+1 문제를 이해하고 eager loading을 효과적으로 구현하는 방법을 단계별로 안내합니다.

Code Layout and Walkthrough: N+1 문제 진단 및 해결

1. 시나리오 설정: 모델 및 관계

먼저 UserPost 두 개의 Eloquent 모델이 있다고 가정합니다.

  • User는 여러 Post를 가질 수 있고,
  • Post는 하나의 User에 속합니다.
// app/Models/User.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

2. Eager Loading 없이 발생하는 N+1 문제

다음은 모든 포스트를 가져와 뷰에 전달하는 컨트롤러 메서드 예시입니다.

// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::all(); // Query 1: 모든 포스트를 가져옴
        return view('posts.index', compact('posts'));
    }
}

posts/index.blade.php 뷰에서 포스트를 순회하며 작성자의 이름을 출력합니다.

## Blog Posts

@foreach ($posts as $post)
    
        
## {{ $post->title }}

        
Author: {{ $post->user->name }}
 {{-- N Queries: 각 포스트의 user마다 하나씩 쿼리 실행 --}}
        
{{ $post->content }}

    
@endforeach

만약 포스트가 100개라면, 이 코드는 101개의 데이터베이스 쿼리를 실행합니다.

  • 1개의 쿼리로 모든 포스트를 가져오고,
  • 각 포스트마다 user를 가져오기 위해 100개의 별도 쿼리를 수행합니다.

이는 N+1 문제의 가장 전형적인 형태이며, Laravel Debugbar 같은 도구가 즉시 과도한 쿼리 수를 알려줄 것입니다.

3. Eager Loading 해결책

이를 방지하기 위해 with() 메서드를 사용해 user 관계를 미리 로드합니다.

// app/Http/Controllers/PostController.php (Optimized)
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        // Query 1: 모든 포스트를 가져옴
        // Query 2: 가져온 포스트와 연관된 모든 유저를 한 번에 배치로 가져옴
        $posts = Post::with('user')->get();
        return view('posts.index', compact('posts'));
    }
}

Post::with('user')->get()을 사용하면 Laravel은 두 개의 쿼리만 실행합니다.

  1. SELECT * FROM posts
  2. SELECT * FROM users WHERE id IN (1, 5, 8, ...) ← 포스트와 연결된 유저들의 ID를 한 번에 조회

posts/index.blade.php 뷰 코드는 그대로 유지되지만, 내부적으로 user 관계가 이미 메모리에 로드되어 있어 개별 데이터베이스 호출이 필요 없게 됩니다.

Advanced Eager Loading

  • Multiple Relationships: 배열을 전달해 여러 관계를 한 번에 eager load합니다.

    $posts = Post::with(['user', 'category'])->get();
  • Nested Relationships: 점 표기법을 사용해 깊게 중첩된 관계까지 미리 로드합니다.

    // 주문, 주문의 고객, 그리고 그 고객의 주소를 모두 로드
    $orders = Order::with('customer.address')->get();

Conclusion

Eager loading은 단순한 최적화가 아니라, 성능 좋고 확장 가능한 Laravel 애플리케이션을 만들기 위한 기본적인 베스트 프랙티스입니다. 컬렉션을 가져온 직후 바로 접근할 관계가 있다면 with()를 사용하는 습관을 들이면 데이터베이스 부하를 크게 줄이고, 애플리케이션 속도를 높이며, 사용자에게 더 부드러운 경험을 제공할 수 있습니다. eager loading을 이해하고 구현하는 것은 Laravel Eloquent ORM을 마스터하고, 애플리케이션이 트래픽과 시간의 시험을 견디게 하는 중요한 단계입니다. 개발 워크플로우에 이를 필수 요소로 삼으면, 애플리케이션 성능이 스스로 보답할 것입니다.

0 조회
Back to Blog

관련 글

더 보기 »

모바일 한여름 열풍

!Cover image for Mobile Midsommer Madnesshttps://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploa...