본문 바로가기
Django Project

Django 쿼리셋 특징 - Lazy Loading

by nijex 2022. 11. 7.

장고는 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_relatedSQL의 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

 

댓글