“섣부른 최적화는 만악의 근원이다”
- 컴퓨터 과학의 거장 ‘도널드 크누스’
그래도 하고 싶다면 아래 두 단계를 따를 것
- 하지마라.
- 아직 하지마라. 완전히 명백하게 이해하고 해법을 찾기전까지는 하지 마라.
확실하게 이해하고 사용하지 않은 단순 성능상의 이익을 위한 최적화는 결국 더욱 큰 문제로 되돌아옴
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를 사용하는 관계에서도 동작)
- 내부적으로 두번의 쿼리를 사용해서 동작 첫번째 쿼리는 원래 객체를 조회하며 두번째 쿼리는 연관된 객체를 가져오는 방식(쿼리문 두개~)
- 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 |