prefetch_related
는 Django ORM에서 관련 객체들을 미리 가져오기 위해 사용하는 메서드로, 주로 “many-to-many” 또는 “one-to-many” 관계에서 발생하는 N+1 쿼리 문제를 해결하기 위해 설계되었다.
1
2
3
4
| # 기본 사용법
books = Book.objects.prefetch_related('authors').all()
# 이렇게 하면 모든 책과 그 책의 저자들을 단 두 번의 쿼리로 가져옵니다
|
Django의 prefetch_related
는 ORM 쿼리 최적화의 핵심 도구이다.
올바르게 사용하면 데이터베이스 쿼리 수를 크게 줄이고 애플리케이션 성능을 향상시킬 수 있다. 특히 many-to-many 또는 one-to-many 관계가 있는 복잡한 데이터 구조에서 효과적이다.
최적의 결과를 위해서는 select_related
와 함께 사용하고, 데이터베이스 쿼리 패턴을 정기적으로 모니터링하여 성능 병목 현상을 식별하는 것이 중요하다. Django Debug Toolbar와 같은 도구를 활용하면 쿼리 성능을 추적하고 최적화할 수 있습니다.
N+1 쿼리 문제란?#
N+1 쿼리 문제는 ORM을 사용할 때 흔히 발생하는 성능 이슈이다다:
- 첫 번째 쿼리로 N개의 객체를 검색한다.
- 그 후 각 객체의 관련 항목을 가져오기 위해 추가로 N번의 쿼리를 실행한다.
예를 들어, 100개의 책을 가져오고 각 책의 저자들을 조회한다면:
- 책 목록을 가져오는 1개의 쿼리
- 각 책의 저자를 가져오는 100개의 쿼리
- 총 101개 쿼리가 발생하게 된다.
prefetch_related
는 이 문제를 단 2개의 쿼리로 줄여준다.
Django는 관련 객체를 미리 가져오는 두 가지 주요 방법을 제공한다:
- select_related:
- SQL JOIN을 사용하여 한 번의 쿼리로 관련 객체를 가져온다.
- “one-to-one” 또는 “many-to-one” 관계에 적합하다.
- 외래 키 관계에서 효율적이다.
- prefetch_related:
- 별도의 쿼리를 실행한 후 Python에서 JOIN을 수행한다.
- “one-to-many” 또는 “many-to-many” 관계에 적합하다.
- 역참조 관계에서 효율적이다.
1
2
3
4
5
6
7
| # select_related 예시 (외래 키 관계)
book = Book.objects.select_related('publisher').get(id=1)
# SQL JOIN을 사용하여 한 번의 쿼리로 책과 출판사 정보를 가져옵니다
# prefetch_related 예시 (many-to-many 관계)
book = Book.objects.prefetch_related('authors').get(id=1)
# 별도의 쿼리로 저자 정보를 가져온 후 Python에서 결합합니다
|
중첩 관계 프리페치#
prefetch_related
는 중첩된 관계도 처리할 수 있다:
1
2
| # 책 -> 저자 -> 저자의 출판사 정보 가져오기
books = Book.objects.prefetch_related('authors__publisher').all()
|
Prefetch 객체 사용하기#
더 복잡한 프리페치는 Prefetch
객체를 사용할 수 있다:
1
2
3
4
5
6
| from django.db.models import Prefetch
# 오직 활성 상태인 저자만 프리페치
books = Book.objects.prefetch_related(
Prefetch('authors', queryset=Author.objects.filter(active=True))
).all()
|
다중 관계 프리페치#
여러 관계를 동시에 프리페치할 수 있다:
1
2
| # 책의 저자와 리뷰를 동시에 프리페치
books = Book.objects.prefetch_related('authors', 'reviews').all()
|
필터링된 관계 프리페치하고 재사용하기#
1
2
3
4
5
6
| active_authors = Prefetch('authors', queryset=Author.objects.filter(active=True), to_attr='active_authors')
books = Book.objects.prefetch_related(active_authors).all()
# 이제 각 책 객체에 'active_authors' 속성으로 접근 가능
for book in books:
print(book.active_authors) # 활성 저자만 포함
|
성능 영향과 최적화 전략#
성능 측정#
prefetch_related
의 효과를 측정하기 위해 Django Debug Toolbar를 사용할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Django Debug Toolbar 설치
# pip install django-debug-toolbar
# settings.py에 추가
INSTALLED_APPS = [
# …
'debug_toolbar',
]
MIDDLEWARE = [
# …
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
|
최적화 전략#
필요한 관계만 프리페치하기: 모든 관계를 프리페치하면 메모리 사용량이 증가할 수 있다.
쿼리셋 특화하기: 실제로 필요한 필드만 가져오도록 .only()
또는 .defer()
와 결합한다:
1
2
3
| books = Book.objects.only('title', 'publication_date').prefetch_related(
Prefetch('authors', queryset=Author.objects.only('name', 'email'))
).all()
|
캐싱 활용하기: 동일한 데이터를 여러 번 접근할 경우 결과를 변수에 저장한다:
1
2
3
4
5
6
| # 좋은 방법
books = Book.objects.prefetch_related('authors').all()
for book in books:
authors = book.authors.all() # 캐시된 결과 사용
for author in authors:
print(author.name)
|
실제 사용 사례#
블로그 애플리케이션 예시#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # 모델 정의
class Category(models.Model):
name = models.CharField(max_length=100)
class Tag(models.Model):
name = models.CharField(max_length=50)
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
category = models.ForeignKey(Category, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag)
class Comment(models.Model):
post = models.ForeignKey(Post, related_name='comments', on_delete=models.CASCADE)
text = models.TextField()
|
최적화된 쿼리:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 비효율적인 방법 (N+1 쿼리 발생)
posts = Post.objects.all()
for post in posts:
print(post.category.name) # 각 게시물마다 카테고리 조회 쿼리 실행
print(post.comments.count()) # 각 게시물마다 댓글 수 조회 쿼리 실행
print([tag.name for tag in post.tags.all()]) # 각 게시물마다 태그 조회 쿼리 실행
# 최적화된 방법 (select_related와 prefetch_related 결합)
posts = Post.objects.select_related('category').prefetch_related('tags', 'comments').all()
for post in posts:
print(post.category.name) # 추가 쿼리 없음
print(post.comments.count()) # 추가 쿼리 없음
print([tag.name for tag in post.tags.all()]) # 추가 쿼리 없음
|
주의사항과 제한 사항#
메모리 사용량: 대량의 데이터를 프리페치하면 메모리 사용량이 크게 증가할 수 있다.
쿼리셋 평가 시점: 프리페치는 쿼리셋이 평가될 때만 실행된다:
1
2
3
4
5
| # 아직 쿼리 실행 안 됨
books = Book.objects.prefetch_related('authors')
# 여기서 쿼리가 실행되고 프리페치됨
book_list = list(books)
|
프리페치 무효화: 프리페치된 쿼리셋에 추가 필터를 적용하면 프리페치가 무효화될 수 있습니다:
1
2
3
4
5
6
7
| # 이 경우 authors 프리페치는 유지됨
books = Book.objects.prefetch_related('authors').all()
first_book = books[0]
# 이 경우 프리페치가 무효화됨 (새로운 쿼리 실행)
books = Book.objects.prefetch_related('authors').all()
recent_authors = books[0].authors.filter(joined_date__year=2023)
|
용어 정리#
참고 및 출처#