본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] Django QuerySet 최적화 : select_related와 prefetch_related 차이점 및 2가지 성능 해결 방법

by Papa Martino V 2026. 3. 19.
728x90

Django QuerySet
Django QuerySet

 

Django 프레임워크를 활용하여 대규모 서비스를 개발하다 보면 반드시 마주치는 벽이 있습니다. 바로 N+1 Query 문제입니다. 데이터베이스 호출 횟수가 기하급수적으로 늘어나 서버 성능이 저하되는 이 현상을 해결하기 위해 Django는 select_relatedprefetch_related라는 강력한 도구를 제공합니다. 단순히 '미리 불러온다'는 개념을 넘어, 내부적으로 SQL이 어떻게 생성되는지, 그리고 어떤 상황에서 어떤 메서드를 선택해야 하는지에 대한 전문적인 아키텍처 관점의 분석을 시작합니다.


1. 데이터베이스 히트(Hit)를 줄이는 두 기술의 근본적 차이

가장 먼저 이해해야 할 점은 두 메서드가 데이터를 가져오는 방식(SQL 레벨)이 완전히 다르다는 것입니다. select_related는 SQL의 JOIN을 사용하고, prefetch_related는 Python 레벨에서의 필터링과 추가 쿼리를 사용합니다.

비교 항목 select_related prefetch_related
작동 원리 SQL JOIN (Inner/Left Outer) 추가 Query 발생 후 Python에서 매핑
관계 유형 1:1 (OneToOne), 1:N (ForeignKey) - 정방향 M:N (ManyToMany), 1:N - 역방향
DB 쿼리 횟수 단 1회 (JOIN으로 통합) 최소 2회 (주 쿼리 + 참조 쿼리)
메모리 사용 상대적으로 적음 결과 셋을 메모리에 캐싱하므로 많음

2. 성능 최적화를 위한 2가지 핵심 해결 방법

방법 01. 정방향 참조 시 select_related로 DB 부하 최소화

외래 키(ForeignKey)를 가지고 있는 모델에서 참조 대상을 불러올 때는 select_related가 정답입니다. 이는 데이터베이스 엔진이 가장 잘하는 'JOIN' 연산을 활용하기 때문에 네트워크 오버헤드가 적고 실행 속도가 매우 빠릅니다.

방법 02. 다대다(M:N) 및 역참조 시 prefetch_related 활용

JOIN을 사용할 경우 데이터의 중복(Cartesian Product)이 발생하여 결과 셋이 비대해질 수 있는 다대다 관계에서는 prefetch_related를 사용해야 합니다. Django는 별도의 쿼리로 데이터를 가져온 후, 내부 파이썬 로직으로 객체들을 연결하여 중복 데이터를 효율적으로 관리합니다.


3. 실전 Sample Example: 도서 관리 시스템 모델링

백엔드 개발 현장에서 자주 쓰이는 저자(Author)와 도서(Book)의 관계를 통해 최적화 코드를 살펴보겠습니다.


# models.py
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')

# [Bad Case] N+1 문제 발생
books = Book.objects.all()
for book in books:
    print(book.author.name)  # 각 도서마다 저자를 찾기 위해 DB 쿼리 발생!

# [Good Case 1] select_related 사용 (정방향)
# SQL: SELECT * FROM book INNER JOIN author ON ...
optimized_books = Book.objects.select_related('author').all()
for book in optimized_books:
    print(book.author.name)  # 추가 쿼리 없이 이미 캐싱된 데이터 사용

# [Good Case 2] prefetch_related 사용 (역방향: 저자의 모든 책 조회)
# SQL 1: SELECT * FROM author;
# SQL 2: SELECT * FROM book WHERE author_id IN (...);
authors = Author.objects.prefetch_related('books').all()
for author in authors:
    print([book.title for book in author.books.all()])

4. 전문 개발자가 전하는 선택 기준 가이드

실무에서는 데이터의 양과 인덱스 상태에 따라 선택이 달라집니다. 만약 참조하는 테이블의 크기가 너무 커서 JOIN 비용이 발생한다면, 때로는 select_related보다 prefetch_related를 사용하여 쿼리를 분리하는 것이 캐시 히트율을 높이는 전략이 될 수 있습니다. 또한, Prefetch() 객체를 활용하여 미리 가져오는 데이터에 필터를 걸거나 정렬을 수행하는 등 고도화된 최적화 기법을 병행하는 것이 중요합니다.


5. 결론 및 요약

Django QuerySet 최적화의 핵심은 "언제 데이터베이스에 접근할 것인가"를 제어하는 것입니다. 단일 연결은 JOIN(select_related)으로, 복합 연결은 추가 쿼리(prefetch_related)로 처리하는 원칙을 지킨다면, 사용자에게 훨씬 빠른 응답 속도를 제공하는 고성능 웹 서비스를 구축할 수 있습니다.


내용 출처 및 기술 참조

  • Django Project Documentation: QuerySet API reference (v5.0)
  • Two Scoops of Django 3.x: Best Practices for the Django Web Framework
  • High Performance Django: Database Optimization Techniques
  • Python Software Foundation: Database Access Optimization Guide
728x90