Django ORM에서 N+1 문제, 왜 이렇게 자주 언급될까요?
Django ORM을 사용하다 보면 "N+1 문제"라는 말을 자주 듣게 됩니다. 하지만 직접 경험하지 않으면 이 문제가 얼마나 심각한지 체감하기 어렵습니다.
간단히 말하면, "한 번만 실행하면 될 쿼리가 예상보다 훨씬 많이 실행되는 문제"입니다. 그 결과, 페이지 로딩 속도가 현저히 느려질 수 있으며, 데이터가 많아질수록 성능 저하가 심각해집니다. 특히 모델 간 관계가 있는 데이터를 조회할 때 발생하는 경우가 많기 때문에, ORM을 사용할 때 반드시 이해하고 있어야 합니다.
💡 N+1 문제, 쉽게 이해해봅시다
예를 들어 블로그 시스템을 만든다고 가정해보겠습니다. 각 저자(Author)는 여러 개의 게시물(Post)을 작성할 수 있습니다. Django 모델로 표현하면 다음과 같습니다.
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
class Post(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
author = models.ForeignKey(Author, on_delete=models.CASCADE)
각 저자(Author)는 여러 개의 게시물(Post)을 작성할 수 있으므로 1:N 관계입니다.
이제 모든 저자의 이름과 그가 작성한 게시물의 제목을 출력하는 기능을 구현한다고 가정해보겠습니다. 일반적으로 아래와 같이 작성할 가능성이 높습니다.
⚠️ N+1 문제가 발생하는 코드
authors = Author.objects.all() # (1) 첫 번째 쿼리 (Author 테이블 조회)
for author in authors:
print(author.post_set.all()) # (N) 각 Author의 Post를 개별 조회
한 번 실행해보면 예상보다 로딩이 오래 걸릴 수 있습니다. 데이터가 많지 않다면 티가 나지 않을 수도 있지만, 만약 저자가 100명, 게시물이 500개 이상이라면? 이제 슬슬 문제가 눈에 보이기 시작할 것입니다.
🧐 실제 실행되는 SQL 쿼리
SELECT * FROM author; -- 1번 실행
SELECT * FROM post WHERE author_id = 1; -- N번 실행
SELECT * FROM post WHERE author_id = 2;
SELECT * FROM post WHERE author_id = 3;
...
이처럼 저자가 많아질수록 쿼리 수도 기하급수적으로 증가합니다. 이것이 바로 N+1 문제입니다.
🚀 N+1 문제는 어떤 상황에서 자주 발생할까요?
사실 이 문제는 Django ORM을 잘 사용하든 못 사용하든, 두 개 이상의 모델이 연관된 데이터를 조회할 때는 거의 반드시 신경 써야 하는 문제입니다. 특히 다음과 같은 경우에서 자주 발생합니다.
1️⃣ 템플릿에서 반복문 안에서 데이터를 참조하는 경우
{% for author in authors %}
{{ author.name }}
{% for post in author.post_set.all %}
- {{ post.title }}
{% endfor %}
{% endfor %}
템플릿에서 반복문을 돌며 데이터에 접근하는 경우, 각 객체마다 추가적인 쿼리가 발생하면서 성능이 저하됩니다.
2️⃣ 뷰 함수에서 반복문 내에서 관련 모델 참조
def blog_view(request):
authors = Author.objects.all() # 첫 번째 쿼리 (Author 조회)
author_list = []
for author in authors:
author_list.append({
'name': author.name,
'posts': author.post_set.all() # 추가적인 N개의 쿼리 발생
})
return render(request, 'blog.html', {'authors': author_list})
템플릿이 아닌 뷰 함수에서 데이터를 가공하는 경우에도 동일한 문제가 발생할 수 있습니다.
💡 직접 겪었던 N+1 문제
예전에 get_absolute_url()
이라는 커스텀 메서드를 만든 적이 있었습니다.
모델의 여러 필드를 조합하여 URL을 생성하는 함수였는데,
성능이 느려지는 문제를 겪고 나서야 원인을 찾았습니다.
class Post(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
def get_absolute_url(self):
return f"/author/{self.author.name}/{self.slug}/"
이 메서드를 호출할 때마다 self.author.name
이 실행되며,
N개의 추가적인 쿼리가 발생하고 있었습니다.
이 문제를 해결하기까지 꽤 오랜 시간이 걸렸습니다.
✅ 정리: N+1 문제를 반드시 이해해야 하는 이유
- Django ORM을 사용하는 한, 이 문제는 언제든지 발생할 수 있습니다.
- 페이지 속도가 느려진다면, ORM이 예상보다 많은 쿼리를 실행하고 있을 가능성이 높습니다.
- 템플릿, 뷰 함수, 모델 메서드 어디에서든 발생할 수 있습니다.
- 데이터가 많아질수록 성능 저하가 심각해질 수 있습니다.
- ORM이 자동으로 최적화를 해주지는 않으므로, 실행되는 쿼리를 직접 확인하고 최적화해야 합니다.
✅ 다음 글에서는 select_related
와 prefetch_related
를 사용해 N+1 문제를 해결하는 방법을 다루겠습니다.
Add a New Comment