장고 쿼리셋의 특징 중 Lazy Loading, Caching, Eager Loading에 대해 알아보자. 🤔
Lazy Loading
먼저, ORM은 게으르다. 😴
from prod.models import Product
product = Product.objects.fitler(name='제품A')
print(product)
위와 같은 코드를 작성한 후 실행해보면, 실제 DB hits는 몇 번 일어날까? 정답은 1번이다.
filter 할 때는 DB hits가 일어나지 않고 마지막 'print(product)'에서 DB hits가 일어난다. 장고 ORM은 QuerySet이 evaluated 될 때까지, 실제로 DB에 접근해 쿼리를 실행하지 않는다.
그렇다면 QuerySet이 evaluated 되는 시점은 언제일까?
Iteration / Slicing
# Iteration
for p in Product.objects.all():
print(p)
# Sclicing
products = Product.objects.all()[::3]
repr() / len() / list() / bool()
# repr
products = repr(Product.objects.all())
# len
products = len(Product.objects.all())
# list
products = list(Product.objects.all())
# bool
if Product.objects.filter(name='제품A'):
print('exist!')
위와 같이 쿼리셋에 담겨있는 데이터를 이용하는 경우 실제 SQL문이 호출된다.
또 다른 특징으로는 장고의 ORM은 정말 필요한 만큼만 호출한다. 예를 들어,
from prod.models import Product
product = Product.objects.all()
prod1 = product[0]
'''
prod1 = product[0]에서 수행되는 SQL문
SELECT "product"."id",
"product"."code",
"product"."name"
FROM "product"
LIMIT 1
'''
위와 같은 상황에서, prod1 = product[0] 구문이 실행될 때, 실제 수행되는 SQL문을 보면, 마지막에 LIMIT 1이라는 옵션이 걸려있는 것을 확인할 수 있다.
이렇게 장고 ORM은 정말 필요한 시점에 필요한 만큼만 호출하는 특징을 가지고 있는데, 가볍게 생각하면 정말 똑똑한 ORM인 것 같다.😎 하지만, 주의해야 될 점을 간과하면 여러 문제가 생길 수 있다.
위의 특징들 때문에, 여러 개의 쿼리셋이 한 번에 합쳐 실행되면 느리게 동작할 수 있고, 필요한 만큼만 값을 가져오기 때문에 이미 알고 있는 값도 다시 한번 호출할 수 있다.(N+1 문제)
Caching
각 쿼리셋에는 DB 접근을 최소화하기 위한 캐시가 포함되어 있다. 쿼리셋이 처음 evaluated 될 때, DB 쿼리가 발생하고 장고는 쿼리의 결과를 캐시에 저장하고 요청 결과를 반환한다. 다음 evaluated시에는 캐시에 저장된 결과를 재사용한다.
아래의 예시를 보자.
from prod.models import Product
for prod in Product.objects.all(): # DB hit!
print(prod.name)
for prod in Product.objects.all(): # DB hit!
print(prod.name)
다음과 같이 사용하면 DB hits를 줄일 수 있다.
from prod.models import Product
products = Product.objects.all()
for prod in products: # DB hit!
print(prod.name)
for prod in products: # 캐시 사용
print(prod.name)
첫 번째, for문에서 evaluation이 이루어질 때, 캐시에 쿼리의 결과를 저장하기 때문에 두 번째 for문에서는 캐싱된 데이터를 사용한다. 쿼리셋이 항상 캐시 되는 것은 아니다. slice나 index를 통하여 쿼리셋을 제한하면 캐시가 되지 않는다.
from prod.models import Product
products = Product.objects.all()
print(products[0]) # DB hit!
print(products[0]) # DB hit!
위와 같이 특정 인덱스의 데이터를 반복적으로 쿼리 하면 매번 DB hits가 일어난다. 하지만 아래의 경우처럼 이미 evaluated가 된 경우에는 캐시를 사용할 수 있다.
from prod.models import Product
products = list(Product.objects.all()) # DB hits!
print(products[0]) # 캐시 사용
print(products[0]) # 캐시 사용
쿼리셋의 캐싱을 이용할 수 있도록, 비즈니스 로직을 구성하는 것이 성능 측면에서 중요하다.
Eager Loading
위에서 설명했듯이, 장고 ORM은 기본적으로 Lazy Loading 전략을 택하기 때문에 N+1문제가 발생할 수 있다.
아래의 예시를 보자.
from board.models import Board
from board.models import Writer
def get_writer_name_list():
boards = list(Board.objects.all()) # 1번 조회
writer_name_list = [board.writer.name for board in boards] # N번 조회
return writer_name_list
get_writer_name_list()
게시글과 작성자는 ForeignKey로 연결되어 있고, 게시글 작성자의 이름을 조회하는 함수를 만든다고 해보자. 게시글의 모든 객체를 조회하기 위한 쿼리가 1번 수행되고, 작성자는 게시글과 ForeignKey로 연결되어 있기 때문에, 작성자 객체를 조회하는 쿼리문이 게시글의 개수만큼 N번 수행된다. (N+1 문제😨)
ORM은 필요할 때마다 쿼리문을 수행하기 때문에, 게시글 늘어날수록 게시글의 개수만큼 쿼리문이 수행될 것이고, 서버의 속도는 느려질 것이다.
N+1문제는 Eager Loading으로 해결할 수 있고, Eager Loading에 대해서는 아래의 포스팅에서 자세히 알아보자.
https://intrepidgeeks.com/tutorial/til46-n-1-problem-and-orm-optimization
https://leffept.tistory.com/311
https://rimi0108.github.io/django/understand-queryset/
'🐍Python | Django' 카테고리의 다른 글
[Django] ORM Eager Loading(select related & prefetch_related) (0) | 2022.05.15 |
---|---|
[Django] QuerySet method (0) | 2022.05.12 |
[Python] generator (0) | 2022.05.09 |
[Python] iterable? iterator? (0) | 2022.05.08 |
[Python] GIL (0) | 2022.05.07 |