하루만에 완성하는 Django+DRF 서비스(4)

June
None
Published in
8 min readMar 20, 2023
출처: https://www.django-rest-framework.org/

안녕하세요. 휴먼스케이프 june입니다.

이번 시간엔 쿼리성능 개선 방법에 대해 알아보도록 하겠습니다.

프로젝트를 진행한 코드는 깃허브에 올라가 있으니 참고하시기 바랍니다.

쿼리 확인

django+drf의 쿼리를 확인할 수 있는 방법은 여러가지가 있습니다.

그 중 가장 간단한 방식 2가지를 소개드리도록 하겠습니다.

Django Debug Toolbar

문서를 보고 debug toolbar를 설치합니다.

서버 내의 모든 view에 툴바가 생긴것을 확인할 수 있습니다.

SQL탭을 누르면 어떤 쿼리가 실행되었는지, 쿼리 explain등을 볼 수 있습니다.

Query logging

django의 logging 문서를 참고해 settings.py에 로깅 설정을 작성합니다.

LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}

api를 호출하면 console에 쿼리가 찍히는것을 확인할 수 있습니다.

(0.000) SELECT "django_session"."session_key", "django_session"."session_data", "django_session"."expire_date" FROM "django_session" WHERE ("django_session"."expire_date" > '2023-03-07 12:56:17.442700' AND "django_session"."session_key" = '2pa7rg8hnhtummtoem4pcr8nzb26mf1r') LIMIT 21; args=('2023-03-07 12:56:17.442700', '2pa7rg8hnhtummtoem4pcr8nzb26mf1r'); alias=default
(0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 1 LIMIT 21; args=(1,); alias=default
[07/Mar/2023 12:56:17] "GET /admin/jsi18n/ HTTP/1.1" 200 3343

쿼리 성능 개선

비효율적인 쿼리를 발생시키기 위해 item list view에 ItemDetailSerializer를 연결시켜보겠습니다.

class ItemViewSet(viewsets.ModelViewSet):
queryset = Item.objects.all()
serializer_class = ItemDetailSerializer
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]

# def get_serializer_class(self):
# if self.action == 'retrieve':
# return ItemDetailSerializer
# return ItemSerializer

item list를 조회하게 되면 아래와 같이 각 item당 review를 검색하는 쿼리가 실행됩니다.

이렇게 object하나 당 해당 object에 연결된 objects를 가져오기 위해 쿼리를 추가로 실행하는 문제를 n+1문제라 부릅니다.

n+1문제를 해결하는 방법은 2가지가 있습니다.

selected_related

내부적으로 join을 사용해 데이터를 한번에 가져옵니다.

따라서 join으로 가져올 수 있는 관계 (연결된 object가 하나일경우)에서 사용됩니다.

prefetch_related

가져오는 objects의 id list를 이용해 in 연산으로 연결된 objects를 가져옵니다.

select_related와 달리 쿼리를 2개로 나누고, python에서 join합니다.

이번 경우엔 prefetch_related를 통해 쿼리를 최적화합니다.

Index

index는 데이터 검색 속도를 향상시키기 위해 사용하는 데이터 구조입니다. 인덱스는 특정 column 값을 색인화하여 데이터베이스에서 빠르게 검색할 수 있도록 합니다.

일반적으로 데이터를 검색할 때, 전체 데이터를 하나씩 탐색하면서 조건에 맞는 데이터를 찾습니다. 이는 데이터의 양이 적을 때는 큰 문제가 되지 않지만, 데이터의 양이 많아질수록 검색 속도가 느려지는 문제가 발생할 수 있습니다.

이 때, 인덱스는 데이터베이스에서 데이터를 빠르게 검색할 수 있도록 도와줍니다.

예를 들어, 특정 테이블에서 ‘이름’ 필드를 인덱스로 설정하면, 이름으로 검색하는 작업에서 데이터베이스는 전체 데이터를 탐색하는 것이 아니라, 인덱스를 사용하여 더 빠르게 해당 데이터를 찾아낼 수 있습니다.

인덱스는 데이터베이스에서 자주 검색되는 필드에 대해서 생성하는 것이 좋습니다.

데이터베이스에서 데이터를 검색할 때 빠르게 찾을 수 있는 장점이 있지만, 데이터를 추가, 수정, 삭제할 때 추가적인 작업이 필요하며, 인덱스의 크기가 크면 디스크 공간도 많이 차지하게 됩니다.

따라서 인덱스를 사용할 때에는 신중하게 결정해야 하며, 자주 사용하는 필드에 대해서만 생성하는 것이 좋습니다.

Django에서는 모델 클래스의 필드에 db_index=True 속성을 설정하여 해당 필드를 인덱스로 사용할 수 있습니다.

item의 date range로 검색하는 필터 기능이 추가된다고 가정해봅시다.

우리는 date 검색을 빠르게 하기 위해 date 필드에 db_index=True 속성을 설정할 수 있습니다.

class Item(models.Model):
name = models.CharField(max_length=100)
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='items')
description = models.TextField(null=True, blank=True)
image = models.ImageField(upload_to='images/', null=True)
date = models.DateTimeField(auto_now_add=True, db_index=True)

def __str__(self):
return self.name

지금까지 쿼리 최적화 방법에 대해 알아봤습니다.

다음 시간엔 새로운 앱을 추가해 여러 앱을 운용하는 방법에 대해 알아보도록 하겠습니다.

--

--