Django

Django ORM 최적화

zhelddustmq 2024. 8. 30. 13:23

“섣부른 최적화는 만악의 근원이다”

  • 컴퓨터 과학의 거장 ‘도널드 크누스’

그래도 하고 싶다면 아래 두 단계를 따를 것

  1. 하지마라.
  2. 아직 하지마라. 완전히 명백하게 이해하고 해법을 찾기전까지는 하지 마라.

확실하게 이해하고 사용하지 않은 단순 성능상의 이익을 위한 최적화는 결국 더욱 큰 문제로 되돌아옴


 

SELECT "articles_comment"."id", "articles_comment"."article_id", "articles_comment"."content", "articles_comment"."created_at", "articles_comment"."updated_at" 
FROM "articles_comment"

SELECT "articles_article"."id", "articles_article"."title", "articles_article"."content", "articles_article"."created_at", "articles_article"."updated_at" 
FROM "articles_article" 
WHERE "articles_article"."id" = 16 
LIMIT 21

SELECT "articles_article"."id", "articles_article"."title", "articles_article"."content", "articles_article"."created_at", "articles_article"."updated_at" 
FROM "articles_article" 
WHERE "articles_article"."id" = 31 
LIMIT 21

SELECT "articles_article"."id", "articles_article"."title", "articles_article"."content", "articles_article"."created_at", "articles_article"."updated_at" 
FROM "articles_article" 
WHERE "articles_article"."id" = 38 
LIMIT 21

...

→ 한 번의 쿼리(1)가 일어난 다음, 비슷한 쿼리(2)가 엄청 많이 일어나는 것을 확인할 수 있음.

 

지연로딩(Lazy Loading)

  • 지연 로딩 (Lazy Loading) : ORM을 작성하면 작성하자마자 SQL로 변환되어 쿼리되는것이 아닌 최대한 쿼리를 미루다가 해당 데이터가 실제로 사용될 때 쿼리를 진행
  •  
  • 지연로딩은 아래와 같은 장점이 있으며 많은 ORM에서 구현하고 있는 방식
    • 불필요한 데이터베이스 쿼리를 방지하여 필요한 데이터만 쿼리하여 성능을 보장
    • 모든 관련된 데이터를 한 번에 로드하지 않고 필요한 경우에만 쿼리하므로 메모리 사용을 줄일 수 있음.
    • 데이터베이스의 부담을 줄일 수 있음.
comments = Comment.objects.all()
for comment in comments:
	print(f"{comment.id}의 글제목")
	print(f"{comment.article.title}")
  • 위 코드에서의 실제 쿼리 발생 지점 (진짜 쓸 때만 불러온다고 생각하면 됨)
    • comments = Comment.objects.all() → 쿼리하지 않음 (예약만 해둠)
    • for comment in comments: → comments 조회 쿼리 발생
    • print(f"{comment.id}의 글제목") → 쿼리하지 않음 (이미 데이터 가지고 왔음)
    • print(f"{comment.article.title}") → 해당 comment의 article id 조회 쿼리 발생(N번)

※N+1 Problem

  • 위와 같이 관계형 데이터베이스에서 지연로딩을 사용할 경우 관련된 객체를 조회하기 위해 N개의 추가 쿼리가 발생하고 실행 되는 문제. 당연히 데이터베이스에 많은 부하가 걸리고 응답시간이 느려지는 등의 성능 문제를 야기함.
    → 아니, 그냥 처음 가져올때 뒤에 필요한 데이터도 한 번에 가져오면 되는거 아님?

 

즉시로딩(Eager Loading)

 

  • 데이터를 로드할 때 필요하다고 판단되는 연관된 데이터 객체들을 한 번에 가져오는 것. 이를 통해 지연로딩에서 발생하는 N+1 문제를 해결할 수 있음. 너무 많은 데이터를 가져오면 오히려 성능 문제를 야기할 수 있
  • Django에서의 Eager Loading
    • select_related
      • one-to-many 또는 one-to-one 관계에서 사용
      • SQL의 JOIN을 이용해서 관련된 객체들을 한 번에 로드하는 방식(쿼리문 하나)
    • prefetch_related
      • many-to-many 또는 역참조 관계에서 주로 사용
      • (select_related를 사용하는 관계에서도 동작)
      • 내부적으로 두번의 쿼리를 사용해서 동작 첫번째 쿼리는 원래 객체를 조회하며 두번째 쿼리는 연관된 객체를 가져오는 방식(쿼리문 두개~)
# 간단하게 표현하면

ModelClass.objects.filter(조건절)
	.select_related('정방향 참조') # JOIN
	.prefetch_related('역방향 참조') # Additional Query

물론 장고가 더 효율적인걸 판단해서 가져다 쓰지만 일단 저런식으로 알고있자~

 

02. 내 로직 편하게 살펴보기

Django-silk

https://github.com/jazzband/django-silk

 

GitHub - jazzband/django-silk: Silky smooth profiling for Django

Silky smooth profiling for Django. Contribute to jazzband/django-silk development by creating an account on GitHub.

github.com

 

  • 실시간으로 내 요청에 대한 다양한 정보를 볼 수 있는 검사 도구
  • 대시보드를 제공하여 개발자로 하여금 편하게 로직을 분석할 수 있게 도와줌
  • 특히 ORM을 통해 Django 내부적으로 사용하는 쿼리를 바로 확인할 수 있
  • Silk 외에도 다양한 도구가 존재
pip install django-silk

settings.py 설정

MIDDLEWARE = [
    ...
    'silk.middleware.SilkyMiddleware',
    ...
]

INSTALLED_APPS = (
    ...
    "silk",
)

urls.py 설정

urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))]

**migrate 도 해야함

 

Silk 사용해보기

만약 이전의 코드로 작성한다면? 👀

comments = Comment.objects.all()
for comment in comments:
  print(comment.article.title)

'Django' 카테고리의 다른 글

외부 API 연동하기  (1) 2024.09.01
Redis  (1) 2024.08.30
Django ORM (심화)  (0) 2024.08.30
JSON Web Token, JWT  (0) 2024.08.30
Serializer 활용하기  (0) 2024.08.29