프로젝트에서 대량의 ListView를 구현해야 할 일이 생겼다.
과거에는 페이지네이션을 사용해 문제를 해결했다. 페이지네이션은 클라이언트가 한 번에 전체 데이터를 요청하는 대신, 필요한 만큼만 나눠서 가져오는 방식이다. 이를 통해 한 번에 처리해야 하는 데이터 양을 줄여서 성능을 개선할 수 있었다.
그러나 최근에 django-debug-toolbar를 알게 되면서 내가 작성한 코드가 성능 면에서 부족하다는 것을 깨달았다. 특히 API 호출과 SQL 쿼리 실행 과정에서 쿼리 수가 많아지는 문제를 발견했다. 이를 해결하기 위해 쿼리 수를 줄이는 방법에 대해 찾아보았다.
Django ORM에서 쿼리 개수를 줄이면 성능을 크게 향상할 수 있다는 것을 알게 되었고, ORM 최적화 기법을 사용하면 데이터베이스에 대한 불필요한 호출을 줄일 수 있다는 것을 배웠다.
Django ORM
Django의 ORM에 대한 과거 정리 글
[ORM] Django의 ORM이란 무엇인가?
ORMORM은 객체(Object)와 관계형 데이터베이스(Relational Database)를 연결해준다.SQL을 작성하는 것이 아니라, ORM을 이용하여 프로그래밍 언어로 DML을 수행할 수 있다. 즉, ORM은 객체 지향 프로그래밍에
jongseoung.tistory.com
이점
Django의 ORM에서 쿼리 갯수를 줄이는 것은 어떤 이점을 얻을 수 있을까?
성능 향상
쿼리의 갯수를 줄이면 데이터베이스와의 통신 횟수를 줄일 수 있다. 각 쿼리는 네트워크 요청을 발생시키고, 데이터 베이스에서 데이터를 가져오는데 시간이 소요된다. 쿼리가 많아질수록 이 과정이 반복되고 응답시간이 길어질 수 있다. 그래서 쿼리를 최적화 함으로써 네트워크 오버헤드를 최소화하고 애플리케이션의 성능을 향상할 수 있다.
데이터베이스 부하 감소
많은 쿼리가 실행되면 데이터베이스 서버에 부하를 줄 수 있다. 특히 고트래픽 환경에서는 다량의 쿼리는 데이터베이스 리소스를 소모하게 되므로, 쿼리수를 줄이면 서버 부하를 줄이고 안정성을 높일 수 있다.
N+1 문제
사실, 많은 이점이 있지만 이부분이 제일 큰 문제라고 생각한다. N+1문제는 하나의 쿼리를 실행한 후, 각 객체마다 추가로 쿼리가 실행되어 수십 수백 번의 추가 쿼리가 발생하는 문제이다. 처음에는 하나 추가되는 게 뭐가 대수겠어했지만, 막상 N+1 문제를 해결하니 30여 개의 쿼리가 10개로 줄어드는 것을 확인하였다. 작은 서비스에서 이 정도인데 서비스의 규모가 커지면 얼마나 많은 비용일 아낄 수 있을지 생각해 보니 정말 중요하다고 생각했다.
메모리 사용량 감소
쿼리가 많아지면 쿼리의 결과가 서버 메모리에 저장되고, 이를 처리하는데 메모리를 많이 소모하게 된다. 특히 대량의 데이터를 처리하고 쿼리 수가 많다면 메모리 부담이 커진다. 따라서 쿼리수를 줄이는것이 메모리 사용량을 줄여, 시스템의 메모리 효율성도 높아진다.
쿼리 갯수 줄이기
쿼리의 갯수를 줄이기 위해서 django에서 ORM 쿼리 최적화 기법을 이용하여 데이터베이스 쿼리의 성능을 크게 향상할 수 있다.
처음에는 뭐가 정참조고, 역참조인지 어떨때 최적화 기법을 사용해야 할지 막막할 수도 있다. 하지만 차근차근 이해하고 사용하면서 익히니 생각보다 너무 간단하고 유용하다는 것을 알 수 있었다.
정참조 와 역참조
쿼리 최적화 기법에 대해서 알기전에 역참조와 정참조에 대한 간단한 정리가 필요할 것 같아서 넣게 되었다.
정참조와 역참조는 이해하고 나면 생각보다 간단한 개념인데, 찾아보면 생각보다 너무 어렵게 나와서 이해하는데 시간이 조금 걸렸다.
정참조는 말 그대로 한 모델이 다른 모델을 참조하는 것이다. 예를 들어, 책과 저자의 관계를 생각해 보자.
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
책이라는 모델에서는 저자를 알수 있는 author이라는 필드가 정의되어 있다. 이렇게 되어 있으면 책 이라는 모델에서 객체 저자를 찾아갈 수 있다. 이런 관계를 정참조라고 한다.
역참조는 반대로 생각하면 된다. 저자 모델만 가지고 저자가 작성한 책을 알 수 없는 관계이다.
물론 Django에서는 역참조가 자동으로 생성되기 때문에 _set을 통해서 역참조를 할 수 있다.
select_related
select_related는 정참조 관계에서 데이터베이스의 JOIN을 사용하여 데이터를 미리 불러오는 방법이다. 이를 통해 여러 쿼리 대신 하나의 쿼리로 데이터를 가지고 올 수 있다.
- ForeignKey
- OneToOneField
# Book 모델에서 author(저자)를 ForeignKey로 참조
books = Book.objects.select_related('author').all()
이렇게 사용할 경우, 각 책에 대해 저자를 가져올 때 쿼리가 1회만 발생한다.
prefetch_related
prefect_related는 역참조 관계나 다대다 관계에서 여러 쿼리를 미리 실행한 후, 메모리에서 데이터를 병합하나는 방식으로 성능을 최적화한다. 예를 들어, 저자가 자신이 쓴 모든 책을 참조하는 경우가 해당된다.
- ManyToManyField
- 역참조 관계
# Author 모델에서 자신이 쓴 책(Book)을 가져옴
authors = Author.objects.prefetch_related('book_set').all()
values() & values_list()
필요한 필드만 선택적으로 가져와 데이터 양을 줄 일 수 있다. 객체 전체를 가지고 오는 대신, 특정 필드만 선택해 성능을 최적화할 수 있다.
# Book 모델에서 title과 author의 name 필드만 가져옴
books = Book.objects.values('title', 'author__name')
책의 제목과 저자의 이름만 조회하여 불필요한 데이터를 가지고 오지 않으므로 성능이 향상된다.
values, values_list와 only의 차이점
설명을 읽고 only()를 생각할 수 도 있는데 이것과는 조금 차이가 있다.
- values() : 딕셔너리 형태로 결과를 반환하여 ORM 객체를 반환하지 않는다.
- values_list(): 튜플 형태로 결과를 반환하며 ORM 객체를 반환하지 않는다.
- only(): ORM 객체를 유지한 채 특정 필드만 불러오며, 나머지 필드에 접근할 때 추가 쿼리를 발생할 수 있다.
only()와 defer()
only와 defer를 이용하여 필요한 필드만 로드할 수 있다. only()는 지정 필드만 데이터베이스에서 가지고 오고, defer은 특정 필드를 제외하고 나머지 필드만 가지고 온다.
# only() 사용
books = Book.objects.only('title', 'author')
# defer() 사용
books = Book.objects.defer('description')
쿼리셋 캐시 활용
동일한 쿼리셋을 여러번 조회하는 경우, 쿼리셋 캐시를 활용해 데이터 베이스 효율을 최소화할 수 있다.
# 매번 데이터베이스에 호출
books = Book.objects.all()
print(books.count()) # 첫 번째 쿼리 발생
print(books[0]) # 두 번째 쿼리 발생
# 캐싱된 쿼리셋 사용
books = list(Book.objects.all()) # 쿼리 한 번 발생
print(len(books)) # 캐시에서 처리
print(books[0]) # 캐시에서 처리
exists()
특정 객체가 존재하는지 여부만 확인하여, 데이터 전체를 불러오지 않고 빠르게 해결할 수 있다.
# 비효율적인 방식: 전체 데이터를 불러온 후 확인
if Book.objects.filter(title='Django').count() > 0:
print("Exists")
# 최적화된 방식: exists() 사용
if Book.objects.filter(title='Django').exists():
print("Exists")
bulk_create()와 bulk_update()
많은 데이터를 한번에 삽입하거나, 수정할 때 사용하여 한번에 삽입, 수정할 수 있다.
# 비효율적인 방식: 하나씩 저장
for book in books:
book.save()
# 최적화된 방식: bulk_create 사용
Book.objects.bulk_create(books)
# bulk_update 예시
Book.objects.bulk_update(books, ['title'])
annotate()와 aggregate()
데이터베이스에서 필요한 통계를 미리 계산하여 쿼리 수를 줄일 수 있다. annotate()는 각각의 객체에 대한 통계를 추가하고, arregate()는 전체 데이터를 대상으로 통계를 구한다.
from django.db.models import Count
# Author 모델에서 각 저자가 쓴 책(Book)의 개수를 구함
authors = Author.objects.annotate(book_count=Count('book'))
저자마다 자신이 쓴 책의 개수가 추가된 상태로 데이터를 가지고 올 수 있다. 추가적인 쿼리 없이 통계를 함께 불러오기 때문에 성능을 개선할 수 있다.
Raw SQL 쿼리
Django ORM이 잘 되어 있지만, 비효율적인 경우 RAW SQL을 사용하는것도 좋은 방법이 될 수 있다.
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM book WHERE title = %s", ['Django'])
books = cursor.fetchall()
트랜잭션 관리
여러 데이터베이스 작업을 트랜잭션으로 묶어서 성능을 향상시키고 데이터 일관성을 유지할 수 있다. 즉, 트랜잭션 내의 모든 작업이 성공을 하면 커밋하고 도중 에러가 발생하면 롤백이 되는 것이다.
from django.db import transaction
with transaction.atomic():
book1.save()
book2.save()
'Django > DRF' 카테고리의 다른 글
[Django] Locust를 이용한 부하 테스트 회고 (feat. Redis, Celery, Kafka) (3) | 2025.01.11 |
---|---|
@property (0) | 2025.01.11 |
[Django] 테스트 : pytest-django, factory-boy, facker (2) | 2024.10.21 |
[Django] 읽기 전용 데이터베이스 설정 및 테스트 (0) | 2024.10.11 |
[Django] 랜덤 객체를 가지고 오는 방법 (1) | 2024.09.13 |