1. 개요
지난주에 Django를 활용한 블로그 만들기 프로젝트를 진행하였습니다. 다른 분들의 코드가 궁금하여, GitHub에 있는 코드를 리뷰했습니다. 사용해 본 적 없었던 annotate()
메서드를 사용한 코드가 있었습니다. 이 메서드는 카테고리와 태그별로 작성된 포스트의 숫자를 쿼리셋에 추가하기 위해 사용되었습니다.
처음 본 annotate()
메서드를 공부해 보았고, Django의 ORM에 있어서 몰라서는 안 되는 유용한 기능이란 사실을 알게 되었습니다.
본 포스팅에서는 annotate()
가 무엇인지 알아보고, 프로젝트에서 어떻게 사용되었는지 살펴보겠습니다. 이후 쿼리 최적화에 대한 내용을 다루고 포스팅을 마치겠습니다.
2. annotate()란?
영어 사전에서 annotate를 검색하면, 주석을 달다라는 의미로 나옵니다. Django의 annotate()
도 주석을 다는 것과 유사합니다. 정확히는 쿼리셋에 새로운 필드를 동적으로 추가하는 기능을 합니다. 이 기능을 이용하면 기존 모델의 필드에 집계나 계산된 데이터를 포함한 새로운 필드를 생성할 수 있습니다.
이 매서드는 자신의 필드뿐만 아니라 외래키 관계에 있는 객체의 데이터도 사용할 수 있습니다. 덕분에 집계나 계산된 데이터를 동적으로 생성하여 사용할 수 있습니다.
3. annotate() 사용법
두 가지 경우의 annotate()
활용법에 대해서 알아보겠습니다.
- 자신의 필드만을 이용한 활용법
- 외래키를 사용하여 다른 모델과 관련된 데이터를 집계하는 법
3.1 총 판매액 계산하기
먼저 자신의 필드를 대상으로 annotate()
를 사용하는 예시입니다.
자신의 필드끼리 연산을 하고, 그 결괏값을 추가적인 필드로 반환합니다.
이번에 사용하게 될 모델과 테이블 구조입니다.
class Product(models.Model):
name = models.CharField(max_length=100)
selling_price = models.IntegerField() # 판매 가격
quantity_sold = models.IntegerField() # 판매된 수량
def __str__(self):
return self.name
id | name | selling_price | quantity_sold |
1 | Product A | 1000 | 50 |
2 | Product B | 6000 | 250 |
3 | Product C | 15000 | 0 |
총 판매액은 (판매된 수량) X (판매 가격)
입니다. F
객체를 활용하여 계산해 보겠습니다.
F
객체는 데이터베이스에서 필드 간의 연산을 처리할 때 유용하게 사용됩니다.
F
객체에 대해서 자세히 알고 싶으신 분은 아래 포스트를 참고하시면 됩니다.
[Django] F객체, Q객체란? - ORM에서 F,Q 사용하기(짤막팁 2편)
from django.db.models import F
products_with_total_sales = Product.objects.annotate(total_sales=F('quantity_sold') * F('selling_price'))
F
객체를 통해 계산된 결과는 total_sales
라는 필드에 추가되어 쿼리셋으로 반환됩니다.
id | name | selling_price | quantity_sold | total_sales |
1 | Product A | 1000 | 50 | 50000 |
2 | Product B | 6000 | 250 | 1500000 |
3 | Product C | 15000 | 0 | 0 |
(참고 : print(over_sold_products)
를 통해 출력하면 <QuerySet [<Product: Laptop> , <Product: Smartphone> , <Product: Headphones>]
>출력됩니다.)
3.2 Category 내에 있는 Post 갯수 세기
이번에는 외래 키 관계에 있는 모델의 데이터를 집계하는 예시입니다.
이번에 사용하게 될 Category
모델의 코드와 데이터 테이블구조입니다.
class Category(SoftDeleteModel):
name = models.CharField(max_length=25, unique=True)
slug = models.SlugField(max_length=200, unique=True, allow_unicode=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("category_posts", kwargs={"slug": self.slug})
class Meta:
verbose_name_plural = "Categories"
id | name | slug |
1 | Technology | technology |
2 | Lifestyle | lifestyle |
3 | Health | health |
아래 코드는 이번 포스팅을 작성하게 된 view코드입니다.
Category
모델에 연결되어 있는 Post
모델의 수를 세는 코드입니다.
class CategoryListView(ListView):
model = Category
template_name = "blog/category_list.html"
context_object_name = "categories"
def get_queryset(self):
queryset = Category.objects.annotate(post_count=Count("post"))
return queryset
각각의 Category
에 해당하는 Post
의 숫자를 post_count
라는 필드에 넣고, 아래와 같은 쿼리셋을 반환합니다.
id | name | slug | post_count |
1 | Technology | technology | 2 |
2 | Lifestyle | lifestyle | 1 |
3 | Health | health | 2 |
(참고 : print(queryset)
를 통해 출력되는 값은 <QuerySet [<Category: Technology>, <Category: Lifestyle>, <Category: Health>]>
입니다.)
4. annotate() 성능 최적화
annotate()
는 아래와 같은 두 가지 장점이 있습니다.
- 간편한 처리 : 여러 모델 간의 관계를 기반으로 다양한 집계 함수(
Count
,Sum
,Avg
,...)를 사용할 수 있습니다. - 동적 처리 : 기존 모델에 없는 필드를 동적으로 추가할 수 있습니다.
위와 같은 장점이 존재하지만, 복잡하거나 대규모 데이터를 처리할 때 성능 저하의 가능성이 있습니다. 따라서, 쿼리 성능을 최적화하는 것이 필요합니다.
이번에 다룰 방법은 필요한 메서드를 사용하여 필요한 데이터만 가져오는 것입니다.
4.1 only() 메서드 사용
only()
매서드는 쿼리셋에서 지정한 필드만을 미리 반환하고, 나머지 필드는 필요할 때 Lazy Loading하는 방식입니다. 이를 통해 성능을 최적화할 수 있습니다.
only()
매서드를 사용한 예시입니다. 3.1에서 사용한 코드에 only()
메서드를 추가해보겠습니다.
products = Product.objects.annotate(
total_sales=F('quantity_sold') * F('selling_price')
).only('name', 'total_sales')
위 코드로 반환된 쿼리셋에서는 name
과 total_sales
필드만을 반환하여 쿼리 성능이 최적화됩니다.
name | selling_price | cost_price | quantity_sold |
Product A | 100 | (지연 로드) | (지연 로드) |
Product B | 200 | (지연 로드) | (지연 로드) |
Product C | 300 | (지연 로드) | (지연 로드) |
*주의 : 필요한 필드가 많다면 전체 데이터를 한 번에 반환하는 것이 최적화에 도움이 됩니다. 먼저 반환되지 않은 필드에 접근할 때마다 추가 쿼리가 발생하기 때문입니다.
4.2 values() 메서드 사용
values()
메서드는 지정한 필드의 정보만을 딕셔너리 형태로 반환합니다. only()
메서드와는 달리 딕셔너리 형태로 필드 값을 반환하기 때문에 나머지 모델의 필드에 접근할 수 없습니다.
3.2에서 사용한 코드에 values()
메서드를 추가해 보겠습니다.
category = Category.objects.annotate(
post_count=Count('post')
).values('name', 'post_count')
values()
매서드가 지정한 name
과 post_count
필드만 딕셔너리 형태로 반환합니다.
[
{'name': 'Technology', 'post_count': 2},
{'name': 'Lifestyle', 'post_count': 1},
{'name': 'Health', 'post_count': 2}
]
4.3 only() 와 values()의 차이점
only() | values() | |
목적 | 특정 필드만 미리 로드하고 나머지는 지연 | 특정 필드만 딕셔너리 형태로 반환 |
반환 형태 | 모델 인스턴스 | 딕셔너리 |
접근 방식 | 전체 모델에 접근 가능 | 지정된 필드에만 접근 가능 |
성능 최적화 방식 | 지정되지 않은 필드에 접근할 때 추가 쿼리 | 모델 인스턴스 로드를 피하고 필드 값만 반환 |
사용 시기 | 모델 객체의 다른 메서드나 필드를 사용할 때 | 단순한 필드 값 조회가 필요할 때 |
추가 쿼리 발생 | 지정되지 않은 필드에 접근 시 추가 쿼리 발생 | 추가 쿼리 없음, 필요한 필드만 반환 |
5. 마치며
오늘은 annotate()
가 무엇인지 알아보고, 사용 방법과 최적화하는 방법도 같이 알아보았습니다.
annotate()
는 model에 추가적인 필드를 생성하지 않아도, 원하는 필드를 구성할 수 있는 유용한 기능입니다.
데이터가 적을 때는 문제가 없지만, 데이터의 양이 많아지면 점점 데이터를 관리하는 방법이 중요한 것 같습니다. 이 글이 annotate()
를 정리하는데 도움이 되었으면 좋겠습니다.
'Django' 카테고리의 다른 글
[Django] select_related(), prefetch_related()란?(N+1문제 해결법) (1) | 2024.09.19 |
---|---|
[Django] F객체, Q객체란? - ORM에서 F,Q 사용하기(짤막팁 2편) (0) | 2024.09.08 |
[Django] Signals 사용법 - 프로필 자동 생성, 좋아요 숫자 갱신(signal 표로 정리) (2) | 2024.09.05 |