Django에서 성능을 저하시키는 문제들 ⚠️

발행: (2026년 3월 1일 오전 05:11 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

위에 제공된 소스 링크 외에 번역할 텍스트를 알려주시면 한국어로 번역해 드리겠습니다.

목차 📑

N+1 쿼리 🐌

데이터베이스와 상호작용하는 애플리케이션에서 흔히 발생하는 성능 병목 현상입니다. 애플리케이션이 단일 쿼리로 얻을 수 있는 데이터를 얻기 위해 N개의 추가 쿼리를 실행할 때 발생합니다. 이로 인해 총 N + 1개의 쿼리가 실행되어, 1개만 실행될 경우에 비해 성능이 크게 저하되며, 특히 데이터셋이 커질수록 그 영향이 크게 나타납니다.

예시

for post in Post.objects.all():
    print(post.author.name)

이 코드는 posts에 대해 1개의 쿼리와 저자에 대해 N개의 쿼리를 생성합니다.

Django에서는 select_related 메서드를 추가하여 해결합니다:

posts = Post.objects.select_related("author")

for post in posts:
    print(post.author.name)

데이터베이스 인덱스 부족 📚

인덱스는 테이블에서 정보 검색을 최적화하여 모든 레코드를 순회하지 않아도 더 빠른 쿼리를 가능하게 합니다. 이는 대용량 데이터 애플리케이션에서 가장 흔한 문제 중 하나입니다.

User.objects.filter(email="test@gmail.com")

인덱스가 없으면 테이블 전체를 스캔하게 됩니다. 프로덕션 환경에서 수백만 행을 처리할 경우 성능이 크게 저하됩니다.

Django ORM에서 인덱스를 추가하려면 db_index=True를 사용해야 합니다:

class User(models.Model):
    email = models.EmailField(db_index=True)

QuerySet의 조기 평가 ⚡

QuerySetlazy하지만, 많은 사람들이 잘못 평가합니다:

qs = User.objects.all()
if len(qs) > 0:
    ...

올바른 사용 방법은 exists()로 존재 여부를 확인하는 것입니다:

if qs.exists():
    ...

exists() 메서드는 객체 존재와 관련된 검색에 유용합니다; QuerySet에 결과가 있으면 True, 없으면 False를 반환합니다. 가능한 가장 간단하고 빠른 방식으로 쿼리를 실행하지만, 이후에 객체에 접근해야 할 경우에는 적합하지 않습니다.

불필요한 데이터 로드 (overfetching) 📦

쿼리를 수행할 때 필요한 필드만 가져오는 것이 좋습니다.

과도하게 로드하는 예시:

users = User.objects.all()

id와 함께 이름만 가져오기:

  • only() 사용 (모델 인스턴스 반환):

    User.objects.only("name")
  • values() 사용 (딕셔너리 반환):

    User.objects.values("name")
  • values_list() 사용 (튜플 리스트 또는 평면 값 반환):

    User.objects.values_list("name", flat=True)

관련 객체에 접근하면 N+1 쿼리 문제가 발생할 수 있습니다. Django는 이를 해결하고 성능을 향상시키기 위해 select_related()prefetch_related()를 제공합니다.

JOIN을 사용하여 단일 SQL 쿼리로 관련 객체를 가져옵니다. ForeignKeyOneToOneField 관계에 더 적합합니다.

Post.objects.select_related("author")

별도의 쿼리를 실행하고 결과를 Python에서 결합합니다. ManyToMany, 역방향 ForeignKey 및 역방향 OneToOne 관계에 더 적합합니다.

books = Book.objects.prefetch_related('authors')

for book in books:
    print(book.title)
    for author in book.authors.all():
        print(author.name)

Prefetch를 활용한 고급 사용

필터링, 정렬 또는 사용자 정의 속성에 할당해야 할 때는 Prefetch 객체를 사용합니다.

from django.db.models import Prefetch

books = Book.objects.prefetch_related(
    Prefetch(
        "authors",
        queryset=Author.objects.filter(active=True)
    )
)

for book in books:
    for author in book.authors.all():
        print(author.name)

count()의 잘못된 사용 🔢

count() 메서드는 정수를 반환하며, 이는 QuerySet과 일치하는 데이터베이스 객체의 개수를 나타냅니다.

User.objects.count()

참고: 레코드가 존재하는지만 확인하면 될 경우, count()보다 exists()가 더 효율적입니다. exists()는 모든 결과를 세지 않기 때문에 성능이 좋습니다.

count()와 같은 결과를 반환하는 잘못된 사용

len(User.objects.all())

count()는 데이터베이스에서 SELECT COUNT(*)를 직접 실행하고, len()은 모든 객체를 메모리로 로드합니다.

트랜잭션을 올바르게 사용하지 않음 🔒

Django는 기본적으로 autocommit을 사용합니다. 이는 각 .save(), .create() 또는 .update() 연산이 별개의 트랜잭션으로 실행된다는 의미입니다.

이는 두 가지 중요한 결과를 초래합니다:

  • 여러 커밋으로 인한 오버헤드 증가.
  • 복잡한 작업 중간에 오류가 발생하면 일관성 문제가 발생할 위험.

일반적인 문제

for item in items:
    item.processed = True
    item.save()

Django는 각 반복마다 1 UPDATE = 1 COMMIT을 실행합니다. 객체가 10 000개라면 10 000개의 커밋이 발생하여 불필요한 오버헤드와 전체 실행 시간이 증가합니다.

transaction.atomic()을 사용한 해결책

from django.db import transaction

with transaction.atomic():
    for item in items:
        item.processed = True
        item.save()

이제 Django는 10 000 UPDATE = 1개의 COMMIT만 실행합니다 (쿼리 수는 줄어들지 않지만).

bulk 작업 사용 금지 🚀

bulk_create()는 제공된 객체 리스트를 효율적으로 데이터베이스에 삽입하고, 생성된 객체들을 리스트로 반환합니다. 반환된 리스트는 제공된 순서와 동일합니다:

users = [
    User(username="user1"),
    User(username="user2"),
]
User.objects.bulk_create(users)

annotate() 효율적인 사용 📊

annotate()는 SQL 집계를 사용하여 QuerySet의 각 객체에 계산된 정보를 추가할 수 있게 해 주며, 불필요한 추가 쿼리를 방지합니다.

일반적인 문제

posts = Post.objects.all()

for post in posts:
    print(post.title, post.comments.count())

이 경우 발생하는 쿼리:

  • 게시물에 대한 1개의 쿼리.
  • 댓글 수를 세기 위한 N개의 추가 쿼리 (N+1 문제).

annotate()를 활용한 해결법

from django.db.models import Count

posts = Post.objects.annotate(comment_count=Count("comments"))

for post in posts:
    print(post.title, post.comment_count)

카운트가 데이터베이스에서 수행되므로 단일 SQL 쿼리만 실행됩니다.

장점

  • 쿼리 수를 줄입니다.
  • 데이터베이스 엔진의 최적화를 활용합니다.
  • 대용량 데이터에서 성능이 크게 향상됩니다.

유용한 기타 함수

from django.db.models import Count, Sum, Avg, Max, Min

User.objects.annotate(
    post_count=Count("posts"),
    avg_score=Avg("posts__score")
)

Django Debug Toolbar 사용 금지 🔎

django-debug-toolbar는 실시간으로 애플리케이션 성능에 대한 상세 정보를 제공하며, 다음과 같은 기능이 있습니다:

  • 실행된 SQL 쿼리 수.
  • 각 쿼리의 실행 시간.
  • 중복된 쿼리.
  • 전체 응답 시간.
  • 캐시 사용량.

이를 통해 다음과 같은 문제를 감지할 수 있습니다:

  • N+1 쿼리.
  • 불필요한 쿼리.
  • 느린 쿼리.
  • 인덱스 부족.

Django Silk을 프로파일링에 사용하지 마세요 🔬

측정 없이 최적화하는 것은 가장 흔한 실수 중 하나입니다. django-silk은 애플리케이션의 실제 성능을 분석할 수 있는 프로파일링 도구입니다. Django Debug Toolbar와 달리 Django Silk은:

  • 요청 히스토리를 저장합니다.
  • 쿼리를 상세히 분석할 수 있습니다.
  • 정확한 실행 시간을 표시합니다.
  • 파이썬 함수의 프로파일링을 지원합니다.

링크 목록 📚

0 조회
Back to Blog

관련 글

더 보기 »

구리지 않은 시맨틱 무효화

캐싱 문제 웹 애플리케이션을 어느 정도 기간 동안 작업해 본 사람이라면 캐싱에 대한 상황을 잘 알 것입니다. 캐시를 추가하면 모든 것이 빨라지고, 그 다음에 누군가…