Django는 처음엔 배우기 쉬웠는데 배우면 배울수록 알아야할게 많아진다...
기초만 보고 단순한 웹 사이트를 만드는데는 장고만큼 쉬운게 없을 것 같다.. 관리자 페이지도 있고, 기본적인 유저 모델이나 인증 시스템이 만들어져 있기 때문에.
그런데 객체지향을 제대로 이해해야지만 장고 프레임워크도 이해할 수 있을 것 같다.
객체지향 모델의 동작 방식이 이해가 되면 객체의 메소드들을 오버라이딩해서 커스터마이징 해야하고.. 인증 방식도 커스터마이징 해야하고.. 할게 많음
그러다 django_rest_framework를 만나면 새로운 프레임워크를 처음부터 배우는것같은 느낌이 든다..
rest_framework에서 serializer를 처음 만났을때 엄청난 거부감이 들면서 왜 써야하지 싶었는데.. 사실 아직도 정확히는 모르겠다.. 커스텀도 힘들고, 알아야할게 너무 많다!
이제 rest_framework의 도움을 받아서 api를 만들기 시작했다고 하자. 그럼 지금까지 사용했던 ORM은 잊어야 한다. 최적화가 되지않은 ORM을 남발하다보면 장고가 이렇게 느린가?? 생각이 들게된다. 데이터 10000개 가져오는데 하루종일 걸린다고 장고한테 정이 떨어질수도있었다.. 근데 장고 문제가 아니라 ORM에 따라 SQL이 어떻게 작성되고 동작하는지를 이해하지 않고 ORM을 사용한 내 잘못이다!@!@
위 문제를 해결하기위해서 '쿼리 최적화'라는 성능에 아주 큰 영향을 미치는 높은 산을 마주했다.. 이걸 넘을 차례다
그래도 이미 산을 넘어간 사람들이 정리를 많이 해주셔서 큰 도움이 되었다.
Category, Post 모델
게시판에 Category와 Post모델이 있다.
# models.py
class Category(models.Model):
title = models.CharField(max_length=128, unique=True, null=True, blank=False)
is_anonymous = models.BooleanField(default=False)
created_date = models.DateTimeField(default=timezone.now)
top_fixed = models.BooleanField(default=False)
only_superuser = models.BooleanField(default=False)
creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name="category")
class Meta:
verbose_name = '게시판 종류'
verbose_name_plural = '게시판 종류 모음'
ordering = ['-created_date', ]
def __str__(self):
return self.title
class Post(models.Model):
title = models.CharField(max_length=128, null=True, blank=False)
content = models.TextField(default='')
thumbnail = models.ImageField(upload_to='post_thumbnail/', null=True, blank=True)
hits = models.PositiveIntegerField(default=0)
created_date = models.DateTimeField(default=timezone.now)
modified_date = models.DateTimeField(auto_now=True)
top_fixed = models.BooleanField(default=False)
category = models.ForeignKey(Category, null=True, on_delete=models.CASCADE, related_name="post")
creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name="post")
class Meta:
verbose_name = '게시글'
verbose_name_plural = '게시글 모음'
ordering = ['-created_date', ]
def __str__(self):
return self.title
Post들을 Serialize하는 코드이다.
SerializerMethodField를 적극 활용했었다..(prefatch_related X)
// serializer.py
// PostListSerializer 부분만 추출
from rest_framework import serializers
from rest_framework.fields import SerializerMethodField
...
class PostListSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField(read_only=True)
creator = serializers.SerializerMethodField(read_only=True)
favorite_count = serializers.SerializerMethodField(read_only=True)
only_superuser = serializers.BooleanField(source="category.only_superuser", read_only=True)
class Meta:
model = Post
fields = (
'id', 'category', 'title', 'content', 'thumbnail',
'hits', 'created_date', 'modified_date', 'top_fixed',
'creator', 'favorite_count', 'only_superuser',
)
def get_creator(self, obj):
try:
category = obj.category
creator = obj.creator
is_anonymous = category.is_anonymous
if is_anonymous:
return "익명"
else:
return creator.profile.nickname
except:
return ''
def get_thumbnail(self, obj):
try:
return obj.thumbnail.url
except:
return ''
def get_favorite_count(self, obj):
try:
return obj.favorite_user.count()
except:
return 0
최적화 전
여기서 creator, favorite_count, only_superuser를 가져오는데 엄청난 중복 쿼리가 발생한다
get_creator 함수에서 obj.category를 할 때마다, obj.creator를 할 때마다 쿼리문이 실행된다.
post를 가져올때 관련된 creator의 nickname을 한번에 가져온다면 쿼리가 중복될 필요없지 않을까?
사실 얘는 문제가 없다..
위의 시리얼라이저에서 쿼리문이 실행되기 때문.
# 기존 API
# api_post.py
class CategoryManageApi(ApiAuthMixin, APIView):
def get(self, request, *args, **kwargs):
"""
cate_id에 맞는 게시판의 글을 모두 보여준다.
"""
pk = kwargs['cate_id']
if not pk:
return Response({
"message": "Select a board type"
}, status=status.HTTP_400_BAD_REQUEST)
category = get_object_or_404(Category, pk=pk)
postlist = Post.objects.filter(category=category)
serializer = PostListSerializer(postlist, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
아래는 django_debug_tool로 확인한 충격적인 중복쿼리들이다.
데이터는 1000개를 임의로 생성했다.
충격적인 쿼리 중복.. 장고가 느린게 아니라 내가 몰랐던 것일 뿐
이제 쿼리를 최적화 해보자..
쿼리 최적화 코드
아직 많이 부족한 코드입니다
from django.db.models import Q, F
from django.db.models.aggregates import Count
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
...
class CategoryManageApi(ApiAuthMixin, APIView):
def get(self, request, *args, **kwargs):
"""
cate_id에 맞는 게시판의 글을 모두 보여준다.
"""
pk = kwargs['cate_id']
if not pk:
return Response({
"message": "Select a board type"
}, status=status.HTTP_400_BAD_REQUEST)
category = get_object_or_404(Category, pk=pk)
postlist = Post.objects.select_related(
'creator'
).annotate(
nickname=F('creator__profile__nickname'),
favorite_count=Count('favorite_user'),
).filter(
Q(category__pk=pk)
)
data = []
data.append(
{
'category': category.title,
'only_superuser': category.only_superuser,
}
)
for post in postlist:
try:
imageurl = post.thumbnail.url
except:
imageurl = ''
context = {
'id': post.id,
'title': post.title,
'content': post.content,
'thumbnail': imageurl,
'hits': post.hits,
'favorite_count': post.favorite_count,
'created_date': post.created_date,
'modified_date': post.modified_date,
'creator': post.nickname,
}
data.append(context)
return Response(data, status=status.HTTP_200_OK)
serializer를 과감히 포기했다.
serializer에서 쿼리를 새로 짤 수도 있다고 알고 있지만 어떻게 해야할지 잘 모르겠다..
추가.
serializer도 결국 queryset에서 데이터를 추출해주는 도구일 뿐이다.
serializer를 만들때 최적화가 완료된 쿼리를 넘겨준다면 동일하게 사용할 수 있다.
결론. serialzer를 버릴 필요가 없다.
postlist = Post.objects.select_related(
'creator'
).annotate(
nickname=F('creator__profile__nickname'),
favorite_count=Count('favorite_user'),
).filter(
Q(category__pk=pk)
)
이 부분을 살펴보자
먼저 select_related로 'creator' 를 설정했다. 이는 Post 객체를 선택할 때 객체와 외래키로 연결된 creator에 있는 정보들도 같이 가져오겠다는 뜻이다.
annotate는 쿼리를 가져오면서 필드명을 임의로 지정해준다고 생각하면 편하다.
nickname=F('creator__profile__nickname') <- 코드를 통해서 post.nickname 에 접근할 수 있게된다.
post.nickname은 (post와 연결된 creator에 연결된 profile)의 필드인 nickname을 의미한다.
favorite_count도 nickname과 마찬가지로 post.favorite_count로 참조가 가능하다.
**여기서 django의 aggregate를 사용했는데 연결된 외래키 또는 many-to-many키의 갯수를 반환해주는 Count를 사용했다. Count는 쿼리를 따로 호출하지 않고 Post객체들을 가져올 때 한번에 계산해서 가져온다! (시간 절약)
마지막으로 filter의 Q는 SQL의 where문과 같다고 생각하면 된다. 코드 Q(category__pk=pk) 의 뜻은 Post를 가져오는데 post객체와 외래키로 연결된 category의 pk값이 pk(멤버변수)와 같다면 가져온다 라는 뜻이다.
그럼 위 코드를 간략히 풀어서 설명하자면 post객체와 외래키로 연결된 category의 pk값이 pk와 같다면 다 가져오는데, 가져올 때 post와 연결된 creator의 모든 정보를 가져오고, nickname과 favorite_count를 annotate와 F를 사용해서 db를 조회할 때 한번에 가져오게 한다.
위 설명은 아래에 나오는 SQL에 그대로 적용된다. 참조.
결과
쿼리 시간이 70배 가까이 빨라졌다. 337ms -> 4.65ms
cpu 시간은 20배 이상 차이가 난다. 83714ms -> 339ms
위 SQL문은 방금 설명한 쿼리가 SQL로 변환된 것이다. django ORM과 함께라면 SQL 공부도 이해가 너무 잘될것같다.
모든 중복 쿼리가 사라졌다. 너무 깔끔하고 기분 좋다. 이제 지금까지 사용했던 모든 django쿼리문을 수정하러 떠나야겠다.
'Back-End > Django' 카테고리의 다른 글
[Django] Django Api 인증, 권한 설정 (0) | 2021.10.27 |
---|---|
[Django] PROJECT 홈페이지 (유저 모델, 쿼리 최적화) (2) | 2021.10.24 |
[Security] XSS(Cross Site Scripting) 취약점 Django (0) | 2021.09.02 |
[Django] Google 소셜 로그인 (OAuth2.0) (13) | 2021.09.02 |
[Django] 회원정보 모델 설계 UserModel design #1 (0) | 2021.05.17 |