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을 사용할 때 흔히 발생하는 성능 이슈이다다:

  1. 첫 번째 쿼리로 N개의 객체를 검색한다.
  2. 그 후 각 객체의 관련 항목을 가져오기 위해 추가로 N번의 쿼리를 실행한다.

예를 들어, 100개의 책을 가져오고 각 책의 저자들을 조회한다면:

prefetch_related는 이 문제를 단 2개의 쿼리로 줄여준다.

Django는 관련 객체를 미리 가져오는 두 가지 주요 방법을 제공한다:

  1. select_related:
    • SQL JOIN을 사용하여 한 번의 쿼리로 관련 객체를 가져온다.
    • “one-to-one” 또는 “many-to-one” 관계에 적합하다.
    • 외래 키 관계에서 효율적이다.
  2. 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의 고급 사용법

중첩 관계 프리페치

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',
]

최적화 전략

  1. 필요한 관계만 프리페치하기: 모든 관계를 프리페치하면 메모리 사용량이 증가할 수 있다.

  2. 쿼리셋 특화하기: 실제로 필요한 필드만 가져오도록 .only() 또는 .defer()와 결합한다:

    1
    2
    3
    
    books = Book.objects.only('title', 'publication_date').prefetch_related(
        Prefetch('authors', queryset=Author.objects.only('name', 'email'))
    ).all()
    
  3. 캐싱 활용하기: 동일한 데이터를 여러 번 접근할 경우 결과를 변수에 저장한다:

    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. 쿼리셋 평가 시점: 프리페치는 쿼리셋이 평가될 때만 실행된다:

    1
    2
    3
    4
    5
    
    # 아직 쿼리 실행 안 됨
    books = Book.objects.prefetch_related('authors')
    
    # 여기서 쿼리가 실행되고 프리페치됨
    book_list = list(books)
    
  3. 프리페치 무효화: 프리페치된 쿼리셋에 추가 필터를 적용하면 프리페치가 무효화될 수 있습니다:

    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)
    

용어 정리

용어설명

참고 및 출처