728x90

명시적으로 api 작성하는것과 적절한 모델링을 통해 서버 개발하는것은 기본입니다. 그렇다면, 어떤 부분에서 성능을 개선할 수 있을까? 생각해봐야합니다.

 

추후 다른 주제로도 다루겠지만 이번 챕터에서는 쿼리에 대해 다뤄보겠습니다.

 

1. select_related와 prefetch_related(feat. N+1)

이건 많이 접했을것이라 생각듭니다. 먼저 select_related와 prefetch_related에 대해 알아보기전 django orm의 고질적인 문제인 N+1문제에 대해 알아봐야합니다.

from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=100)


class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)


from .models import Book


def book_list(request):
    books = Book.objects.all()
    for book in books:
        author_name = book.author.name  # 작가 정보를 가져오기 위해 추가 쿼리 발생
    return render(request, 'book_list.html', {'books': books})

위와 같은 기본적인 작과와 도서에 대한 간단한 모델의 각 도서 목록의 작간의 이름을 가져오는 방식을 볼때의 경우,

- Book.objects.all() 도서목록을 가져오는 쿼리 1회

- book.author.name을 통해 n개의 작가 이름을 가져오는 쿼리 n회

이러한 비효율적인 쿼리가 발생하게 됩니다.

 

이러한 문제를 해결하기 위해 select_related를 활용할 수 있습니다.

select_related란, Foreinkey 혹은 OneToOneField와 같은 관계 필드에 대해 사용하며 데이터베이스에서 필요한 연관 객체를 추가로 로드하게 되어 위와같은 N+1 쿼리 문제를 해결할 수 있게 됩니다.

books = Book.objects.select_related('author').all()
for book in books:
    author_name = book.author.name  # 추가 쿼리 없이 author를 미리 로드

- Book.objects.select_related('author').all() 1회의 쿼리에서 미리 작가의 정보까지 로드

이후, 반복문의 경우 미리 로드되어 있는 author의 정보로 인해 추가적인 쿼리가 발생하지 않게 됩니다.

 

그렇다면, prefetch_related는 무엇일까요? 이또한, N+1문제를 해결할 수 있으며 select_related와는 다르게 ManyToManyField 및 reverse ForeignKey/OneToOneField 관계와 같은 역참조 필드에 사용됩니다.

class Category(models.Model):
    name = models.CharField(max_length=100)
    books = models.ManyToManyField(Book)


from .models import Category

# prefetch_related를 사용하지 않은 경우
categories = Category.objects.all()
for category in categories:
    books = category.books.all()  # N+1 쿼리 발생

# prefetch_related를 사용한 경우
categories = Category.objects.prefetch_related('books').all()
for category in categories:
    books = category.books.all()  # 추가 쿼리 없이 books를 미리 로드

- Category.objects.all() 카테고리 목록을 가져오는 쿼리 1회의 쿼리에서 미리 도서 목록의 정보까지 로드

이후, 추가 쿼리에서는 미리 로드 되어있는 books로 인해 추가적인 쿼리가 발생하지 않게됩니다.

 

물론, 이 메서드들을 때에 맞지 않게 사용할시 오히려 역효과를 나을수 있겠지만 잘 사용만 한다면 쿼리 성능을 극대화 할 수 있는 가장 효율좋은 메서드 입니다.

 

2. bulk_create, bulk_update

예시를 들어봅시다. name과 price로 이뤄진 Product 모델이 있고 각각의 데이터 100개를 저장해야합니다.

from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)


from .models import Product

products_to_insert = [
        Product(name='Product 1', price=10.99),
        Product(name='Product 2', price=20.99),
        Product(name='Product 3', price=15.99),
        ...
        Product(name='Product 100', price=14.99),
    ]

for product in products_to_insert:
    product.save()

위와 같이 저장할시, 당연히 문제 없이 저장이 될것 입니다. 하지만, 100개의 쿼리가 발생하며 이말은 즉, 100개의 트랜잭션의 작업이 발생한다는 것입니다.

Product.objects.bulk_create(products_to_insert)

 bulk_create를 사용하면 저장할 Product 스키마에 맞는 데이터를 리스트에 담아 전달해주면 하나의 트랜잭션내에서 100개의 데이터를 생성해낼 수 있습니다.

물론, 성능상 이점도 있겠지만 하나의 트랜잭션 내에서 실행되므로 일관성이 유지된다는 장점 또한 존재합니다.

 

bulk_update도 어렵지 않게 bulk_create와 동일한 방식으로 사용가능합니다.

products_to_update = [
        Product(name='Product 1', price=10.99),
        Product(name='Product 2', price=20.99),
        Product(name='Product 3', price=15.99),
        ...
        Product(name='Product 100', price=14.99),
    ]

Product.objects.bulk_update(products_to_update, ['price'])

bulk_create와 마찬가지로 스키마에 맞는 데이터를 리스트에 담아 전달 후, 업데이트할 데이터를 2번째 인자로 리스트에 담아 전달해주면 bulk_update가 완료됩니다.

 

추가로 insert_or_update 기능또한 존재합니다. 기본적으로 bulk_create 메서드를 이용하여 사용하는데 사용방식은 아래와 같습니다.

from django.db import models


class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
	quantity = models.PositiveIntegerField()
    date = models.DateField()
    
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["name", "date"],
                name="unique name and date",
            )
        ]


from .models import Product


product_data = [
        Product(name='Product 1', price=10.99, quantity=10, date='2021-01-03'),
        Product(name='Product 2', price=20.99, quantity=6, date='2022-01-03'),
        Product(name='Product 3', price=15.99, quantity=3, date='2023-01-03'),
        ...
        Product(name='Product 100', price=14.99, quantity=2, date='2023-05-03'),
    ]

Product.objects.bulk_create(
    product_data,
    update_conflicts=True,
    unique_fields=["name", "date"],
    update_fields=["quantity", "price"],
)

bulk_create 메서드에서 update_conflicts=True로 설정뒤 고유값으로 설정한 필드와 업데이트할 필드를 입력해줍니다.

주의사항의 경우, 기본적으로 unique하게 선언되어 있지 않던 컬럼의 경우, model단에서, constraint로 선언해주어야합니다.

 

위의경우, 기존에 name=Product 1, date:2021-01-03이였던 데이터가 있었다면 price와 quantity가 업데이트 되며, 이외의 데이터의 경우, 새롭게 생성되게 됩니다.

 

물론, bulk_create, bulk_update방식이 위와같이 가장 이상적으로 담겨진다면 문제가 없겠지만 어쩔수 없이 로직상 불가능한 경우엔 사용이 어려운 부분이 있어 상황에 맞게 사용해야함은 당연합니다.

 

개인적으로, bulk 기능을 잘이용하기위한 팁은 순서를 보장하는 자료구조와 dataclass를 사용하여 미리 데이터 셋을 정의하고 bulk 기능을 사용하는것이 유용하니 참고 해보시길 바랍니다.

 

3. cached_property

class Travel(BaseAdminModel):
    user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="여행 리스트 방장")
    members = models.ManyToManyField(User, through="Member", related_name="travels", verbose_name="여행 멤버들")
    title = models.CharField(max_length=255, verbose_name="여행 제목")
    color = models.CharField(max_length=255, verbose_name="여행 색상")
    start_date = models.DateField(verbose_name="여행 시작 날짜")
    end_date = models.DateField(verbose_name="여행 끝나는 날짜")
    description = models.CharField(max_length=13, null=True, verbose_name="여행 메모")
    currency = models.CharField(max_length=15, choices=CurrencyType.CHOICES, default=CurrencyType.USD)

    def __str__(self):
        return self.title

    @property
    def total_amount(self):
        return sum(self.billings.all().values_list('total_amount', flat=True))

위의 코드의 경우, 쿼리를 할때마다 total_amount 연산을 수행하게 됩니다.

 

django에서 제공하는 decorator인 cached_property까 존재하는데 처음 호출되었을때 property 함수 결과값을 캐싱해 둔 뒤 그 이후에는 캐싱된 결과를 리턴합니다. 즉, 쿼리를 할때마다 total_amount로직을 수행하는 것이 아닌 해당 메서드를 호출할 때 캐싱된 값을 리턴하게 되어 불필요한 연산을 줄이게 됩니다.

from django.utils.functional import cached_property 


@cached_property 
def total_amount(self):
    return sum(self.billings.all().values_list('total_amount', flat=True))

캐싱된 데이터는 모델 인스턴스가 살아있는 동안만 캐싱되며, 모델인스턴스의 생명이 끝나게 되면 함께 초기화 되게 됩니다. 캐싱된 데이터이기 때문에 모델인스턴스에서 최초 1회가 아닌 연산에 변화가 생기게 되더라도 변화가 적용되지 않기 때문에 이부분을 주의하여 사용한다면, 성능 개선에 굉장한 도움이 될것입니다.

 

 

orm에서 제공하는 몇몇 메서드들만 활용하더라도, 성능 개선을 할수 있음에 많은 도움을 받을 수 있었습니다. 기본적인 orm 메서드 활용 뿐만 아니라 다른 자원을 좀 더 공격적으로 활용해 서버 개선을 할수 있으니 다음 챕터에서는 다른 자원을 활용한 서버 성능 개선에 대해 알아보도록 하겠습니다.

728x90
728x90

6. 장고에서 모델 이용하기

- 모델은 장고 프로젝트에서 토대가 되는 부분이다. 성급하게 모델을 작성하게 되면 어느 순간 프로젝트가 내생각과 다르게 흘러가는 경험을 하게 될것이다.

- 그렇기 때문에 모델은 신중히 미래를 고려하여 설계해야만 한다.

 

6-1. 앱을 분리하자

- 모델이 너무 많으면 앱을 나눠야 한다. 하나의 앱에 모델이 20개 이상있으면 너무 많은 일을 한다는것을 반증하는 것이다. 책에서는 모델 5개가 넘지 않길 권장한다.

6-2. 모델 상속에 주의하자

모델의 상속 스타일 장점  단점
상속을 이용하지 않는 경우: 모델들 사이에 공통 필드가 존재할 경우, 두 모델에 전부 해당 필드를 만들어 준다. 테이블에 어떤 식으로 매필되는지 상관 없이 장고 모델을 한 눈에 이해하기 쉽다. 모델들 사이에 서로 중복되는 테이블이 많을 경우 이를 지속적으로 관리하는 데 어려움이 따른다.
추상화 기초 클래스: 오직 상속받아 생성된 모델들의 테이블만 생성된다. 추상화된 클래스에 공통적인 부분을 추려 놓음으로써 한 번만 타이핑을 하면 된다. 
추가테이블이 생성되지 않고 여러 테이블에 걸쳐 조인을 함으로써 발생하는 성능 저하도 없다.
부모 클래스를 독립적으로 이용할 수 없다.
멀티테이블 상속: 부모와 자식 모델에 대해서도 모두 테이블이 생성된다. onetoonefield는 부모와 자식간 적용된다. 각 모델에 대해 매칭되는 테이블이 생성된다. 따라서 부모 또는 자식 모델 어디로든지 쿼리를 할 수 있다. 부모 객체로부터 자식 객체를 호출 하는 것이 가능하다: parent.child 자식 테이블에 대한 각 쿼리에 대해 부모 테이블로의 조인이 필요하므로 이에 따른 상당한 부하가 발생한다.
프록시 모델: 원래 모델에 대해서만 테이블이 생성된다.  각기 다른 파이썬 작용을 하는 모델들의 별칭을 가질 수 있다.  모델의 필드를 변경할 수 없다.

6-3. 장고 모델 디자인

- 정규화 하기 -> 데이터베이스 자체의 정규화에 대한 지식습득이 먼저다! 이 원칙에 맞춰 디자인을 해야만 한다.

- 캐시와 비정규화

    -적절한 위치에서 캐시를 세팅하는 것은 모델을 비정규화 할 때 발생하는 문제점들을 상당부분 해결해준다.

- 반드시 꼭 필요할때만 비정규화를 하도록한다.

-언제 널을 쓰고 언제 공백을 쓸것인가?

필드 타입  null=True blank=true
charfield, textfield, slugfield, emailfield, uuidfield, filefield, imagefield 이용하지 않는다. 이용한다.
booleanfield 이용하지 않는다. 이용하지 않는다.
integerfield, floatfield, decimalfield 해당값이 null로 들어가도 문제가 없다면 이용한다. 해당값이 빈값을 받아도 문제가 없다면 이용한다.
datetimefield, datefield null값으로 설정 가능하다면 이용한다. null=true와 같이 이용한다.
foreignkey, manytomanyfield, onetoonefield null값으로 설정 가능하다면 이용한다. 이용한다.

 

-정리

이외에도 모델에 관한 부분은 수많이 있으며 또한 two scoops of django에도 굉장히 자세히 다루고 있다. 제필요성에 의해서 필요한 부분만 적어둔거기 때문에 따로 웹서핑이든 도서를 통해 계속해서 학습을 해야하는 파트라고 생각됩니다!

728x90

+ Recent posts