장고는 ORM(Object Relational Mapping)을 이용해 데이터를 관리한다.
ORM이란 객체와 관계형 데이터베이스를 연결해주는 작업이라 할 수 있는데, 객체를 클래스로 표현하는 것과 같이 관계형 데이터베이스를 객체처럼 쉽게 사용할 수 있도록 해준다. ORM 덕분에 편하게 db에 접근해 개발할 수 있지만, 반대 급부에 있어 단점은 쿼리가 어떻게 요청되는지 알 수 없어 성능 저하의 문제 발생할 수 있다는 점이다.
이렇게 발생하는 ORM의 단점이 지연 로딩(Lazy-Loading)이다. 지연 로딩이란 단순히 쿼리문이 작성되어있다고 해서 쿼리를 날리는 것이 아니라, 최종적으로 데이터가 필요한 시점에서 쿼리를 날려 데이터베이스에서 데이터를 가지고 오는 것을 의미한다.
books = Book.objects.all() # 실제 DB 작업 X
books = books.filter(category="novel") # 실제 DB 작업 X
print(books) # 실제 DB 작업 O
지연 로딩으로 인해 불필요한 쿼리가 여러번 중복하여 수행될 수 있고, 특히 참조 모델의 데이터를 반복적으로 호출하는 N+1 쿼리 문제로 성능이 크게 저하되는 문제가 생길 수 있다.
성능 개선을 위해 우선적으로 ORM이 실제 발생시키는 쿼리문을 최소화하는 방안이 있고, 추가적으로 Caching, Eager-Loading 등을 활용해 쿼리문 발생을 방지할 수 있을 것이다.
쿼리가 날아가는 시점
쿼리가 날아가는 시점이란 값을 저장하거나 출력하거나 참조하는 시점을 의미하는 데, 장고 프로젝트 공식 홈페이지에 따르면 쿼리가 날아가는 시점은 아래와 같이 7가지 경우가 있다고 한다.
1. Iteration / Asynchronous iteration
반복문이 처음 반복할 때 데이터베이스에 쿼리를 날린다.
for e in Entry.objects.all():
print(e.headline)
2. Slicing
entries = Entry.objects.all()[:3]
3. Pickling/Caching
pickle이란 파이썬 객체를 바이트 스트림으로 저장하게 해주는 모듈. 객체를 직렬화할 때는 dumps() 함수를 사용하고, 직렬화된 객체를 가져올 때는 loads() 함수를 사용한다.
import pickle
query = pickle.loads(s) # s는 직렬화된 쿼리문
qs = MyModel.objects.all()
qs.query = query # 원래 쿼리문 복구
캐시에 데이터를 저장할 때도, 데이터를 가져오기 위해 데이터베이스에 접근한다.
4. repr()
repr은 파이썬 인터프리터를 위한 것이로, API를 인터프리터로 사용할 때 즉시 결과를 볼 수 있다
books = repr(Book.objects.all())
5. len()
갯수를 알아야할 때는 count()를 사용하는 게 더 효율적이다.
books = len(Book.objects.all())
6. list()
배열 형태로 변환할 때도 데이터베이스에 접근한다.
books = list(Book.objects.all())
7. bool()
queryset이 boolean으로 작성되었을 때도 데이터베이스에 접근한다.
데이터의 존재 여부만 확인할 때는 if문보다는 exist()를 사용하는 게 더 효율적이다.
if Entry.objects.filter(headline="Test"):
print("There is at least one Entry with the headline Test")
Caching
장고 쿼리셋의 또다른 특징은, 처음으로 데이터베이스에 쿼리가 발생될 때 쿼리의 결과를 캐시에 저장하는 것이다. 그 후에 같은 요청을 다시 보내면 db에 또다시 접근하는 것이 아니라 캐시에 저장된 결과를 재사용한다. 성능을 고려한다면 쿼리셋의 캐싱을 이용할 수 있도록 코드를 짜는 것이 중요하다.
books = Book.objects.all()
for book in books:
print(book.title) # DB hit
for book in books:
print(book.title) # 캐시 사용
이 외에도 별도로 redis, memcached 등의 캐시 db를 두어 필요한 부분에 캐시를 적용해주면 성능을 크게 개선시킬 수 있다.
Eager-Loading
즉시 로딩(Eager-loading)이란 지연로딩과 반대되는 개념으로, 필요할 때 데이터를 개별로 요청하는 게 아니라 로딩 시 필요한 데이터를 미리 다 갖고 오는 것을 의미한다. 앞서 언급한 N+1 쿼리 문제의 경우 특정 테이블과 외래키로 묶인 테이블의 정보에 대해서도 함께 쿼리를 날려 데이터를 미리 가져와 해당 문제를 해결할 수 있다.
장고에서 즉시 로딩을 구현하는 방법에는 select_related와 prefatch_related 두 가지가 있다. 이들을 사용하여 related된 정보를 추가로 함께 요청한다면 해당 데이터가 캐싱되어 추가 쿼리 요청 없이 related된 정보를 가져올 수 있다.
(* 외래키를 가진 클래스에서 가지지 않는 클래스를 참조할 때를 정참조, 반대의 경우를 역참조라 한다.)
select_related
가져올 객체가 역참조하는 single object(one-to-one or many-to-one)이거나, 또는 정참조 foreign key 일 때 사용한다.
select_related는 SQL의 join문을 통해 연관된 테이블의 관련된 데이터를 즉시 가져온다.
from django.db import models
class City(models.Model):
# ...
pass
class Person(models.Model):
# ...
hometown = models.ForeignKey(
City,
on_delete=models.SET_NULL,
blank=True,
null=True,
)
class Book(models.Model):
# ...
author = models.ForeignKey(Person, on_delete=models.CASCADE)
# select_related 사용 O
b = Book.objects.select_related('author__hometown').get(id=4) # DB hit
p = b.author # 캐시 사용
c = p.hometown # 캐시 사용
# select_related 사용 X
b = Book.objects.get(id=4) # DB hit
p = b.author # DB hit
c = p.hometown # DB hit
prefatch_related
구하려는 객체가 정참조 multiple objects(many-to-many or one-to-many)이거나, 또는 역참조 Foreign Key일때 사용한다. prefetch_related를 사용하면 추가적으로 하나의 쿼리문이 더 수행되면서 참조하고 있는 테이블의 정보를 전부 가져오게 된다. selected_related와 달리, prefetch_related는 SQL의 join문을 실행하지 않고, python에서 joining을 실행한다.
from django.db import models
class Topping(models.Model):
name = models.CharField(max_length=30)
class Pizza(models.Model):
name = models.CharField(max_length=50)
toppings = models.ManyToManyField(Topping)
def __str__(self):
return "%s (%s)" % (
self.name,
", ".join(topping.name for topping in self.toppings.all()),
)
# 쿼리셋에 있는 아이템을 호출할 때마다 Toppings 테이블에 쿼리를 날림
Pizza.objects.all()
# 한 번의 쿼리로 캐시에 토핑 데이터가 전부 저장됨
Pizza.objects.prefetch_related('toppings')
참고:
Making queries | Django documentation | Django (djangoproject.com)
Making queries | Django documentation | Django
Django The web framework for perfectionists with deadlines. Toggle theme (current theme: auto) Toggle theme (current theme: light) Toggle theme (current theme: dark) Toggle Light / Dark / Auto color theme Overview Download Documentation News Community Code
docs.djangoproject.com
Database access optimization | Django 문서 | Django (djangoproject.com)
Database access optimization | Django 문서 | Django
Django The web framework for perfectionists with deadlines. Toggle theme (current theme: auto) Toggle theme (current theme: light) Toggle theme (current theme: dark) Toggle Light / Dark / Auto color theme Overview Download Documentation News Community Code
docs.djangoproject.com
'Django Project' 카테고리의 다른 글
헤로쿠로 앱 배포하기(3) - 캐싱으로 db부하 줄이기 (0) | 2022.10.09 |
---|---|
헤로쿠로 앱 배포하기(2) (0) | 2022.10.09 |
헤로쿠로 앱 배포하기(1) (0) | 2022.10.06 |
파이썬 가상환경 설정(conda, venv) (0) | 2022.10.06 |
댓글