[Two Scoops of Django] 6장. 장고에서 모델 이용하기

guava·2021년 10월 2일
0

Two Scoops of Django

목록 보기
6/12
post-thumbnail

Two Scoops of Django 3.x를 읽고 정리한 글입니다.

1. 시작하기

모델이 너무 많으면 앱을 나눈다

각 앱이 가진 모델의 수가 다섯 개를 넘지 않아야 한다는게 우리의 의견이다.

모델 상속에 주의하자

장소는 세 가지 모델 상속 방법을 제공한다.

  1. 추상화 기초 클래스 (abstract base class): 파이썬 표준 라이브러리의 abc모듈이 아닌 장고 추상화 기초 클래스
  2. 멀티테이블 상속 (multi-table inheritance)
  3. 프락시 모델 (proxy model)

상속 스타일에 따른 장점과 단점

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

어떤 종류의 상속을 언제 이용하면 될까?

  1. 모델 사이에서 중복되는 내용이 적을 때에는 상속 없이 모든 모델에 필드를 추가한다.
  2. 대부분은 공통 필드 부분을 추상화 기초 모델로 이전하는 리팩터링이 가능하다.
  3. 프락시 모델은 종종 편리하게 이용되지만 다른 두 가지 모델 상속 방식과는 다르게 동작한다는점을 명심하자.
  4. 멀티테이블 상속은 상당한 부하를 일으키므로 피한다.

실제로 모델 상속해보기 : TimeStampedModel

데이터베이스를 설계한다고 생각해보자.
설계해보니까 모든 테이블에 생성시간(created), 수정시간(modified)이 필요하다.
어떻게 해야할까? 모든 모델에 created, modified를 추가하면 된다. 하지만 Django의 추상화 기초 클래스를 활용하면 더 간결해진다.
TimeStampedModel이라는 추상화 기초 클래스를 정의한다. 이 모델을 상속한 모델은 DB에서도 동일한 필드가 추가된다.

# core/models.py

from django.db import models

class TimeStampedModel(models.Model):
    """
    'created'와 'modified'필드를 자동으로 업데이트해 주는 추상화 기반 클래스 모델
    """
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True  # 추상화 기초 클래스로 선언하는 코드
    # 추상화 기초클래스로 선언되면 마이그레이션을 실행할 때 core_timestampmodel테이블이 생성되지 않는다.
# flavors/models.py
from django.db import models
from core.models import TimeStampedModel

class Flavor(TimeStampedModel):  
    """
    추상화 기초클래스인 TimeStampedModel를 상속하였다.
    TimeStampedModel의 'created', 'modified'필드가 Flavor모델에 포함된다.
    """
    title = models.CharField(max_length=200)

추상화 기초클래스인 TimeStampedModel은 마이그레이션 시 테이블이 생성되지 않고, Flavor테이블만 생성된다. 또한 Flavor모델은 TimeStampedModel이라는 추상화 기초 클래스를 상속하였으므로 created/modified가 포함되어 테이블이 생성된다.

2. 데이터베이스 마이그레이션

Migrations are Django’s way of propagating changes you make to your models (adding a field, deleting a model, etc.) into your database schema. They’re designed to be mostly automatic, but you’ll need to know when to make migrations, when to run them, and the common problems you might run into.
- django documentation

마이그레이션은 모델에 대한 변경 사항을 데이터베이스 스키마에 적용하는 것이다.
장고는 django.db.migrations라는 강력한 데이터베이스 마이그레이션(migration) 도구를 제공한다.

마이그레이션 생성 팁

  • 새로운 앱이나 모델이 생성되면 django.db.migrations를 실행하면 된다.
    python manage.py makemigrations
  • 마이그레이션 코드를 실행하기 전에 생성된 코드를 살펴본다. 다음 명령을 통해 SQL문도 확인 가능하다. python manage.py sqlmigrate
  • django.db.migrations 스타일로 이루어져 있지 않은 외부 앱에 대해 마이그레이션을 처리할 때는 MIGRATION_MODULES세팅을 이용한다.
  • 마이그레이션 개수는 많아도 된다. 너무 많아서 불편하다면 squshmigrations를 활용해보자.
  • 마이그레이션을 실행하기 전에 항상 데이터를 백업하자.

마이그레이션에 Python함수 및 Custom SQL 추가

django.db.migrations는 데이터와 상호 작용하는 외부 구성 요소에대한 복잡한 변경에 대한 처리가 불가능하다.

이는 마이그레이션 실행을 지원하기 위해 파이썬 또는 SQL을 어떻게 작성해야 하는지 조사할 필요성이 생길 수 있다는 뜻이다. (저자는 RunPython을 추천하고 있다.)

프로덕션 환경에서 프로젝트가 어느정도 궤도에 오르면 RunPython 또는 RunSQL클래스를 사용해야 할 시점이 올 것이다.

3. RunPython의 문제점 극복하기

Custom Model Manager의 메소드에 접근하기

custom model manager에서도 레코드를 필터링, 제외, 생성 또는 수정이 가능하다. 그러나 이러한 작업이 기본 migrations명령에서는 제외되며, use_in_migrations = True 를 추가해서 재정의하면 된다. (예제)

Custom Model의 메소드에 접근하기

django.db.migrations의 모델 직렬화 방식 구조상 마이그레이션 중에는 호출이 불가능하다. (설명)

모델의 저장 및 삭제 메서드를 재정의하면 RunPython에서 호출할 때 호출되지 않으므로 치명적인 문제가 발생할 수 있다.

RunPython.noop을 사용

다음과 같이 RunPython을 활용해 add_cones작업을 수행하였고 추후에 역 마이그레이션이 동작할 수 있게 하려면 RunPython의 reverse_code 파라미터를 이용하라. (ex. migrations.RunPython(add_cones, reverse_code=migrations.RunPython.noop) - link

from django.db import migrations, models

def add_cones(apps, schema_editor):
    Scoop = apps.get_model('scoop', 'Scoop')
    Cone = apps.get_model('cone', 'Cone')

    for scoop in Scoop.objects.all():
        Cone.objects.create(
            scoop=scoop,
            style='sugar'
        )

class Migration(migrations.Migration):
    initial = True

    dependencies = [
        ('scoop', '0051_auto_20670724'),
    ]

    operations = [
        migrations.CreateModel(
            name='Cone',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 
                ('style', models.CharField(max_length=10), choices=[('sugar', 'Sugar'), ('waffle', , 'Waffle')]),
                ('scoop', models.OneToOneField(null=True, to='scoop.Scoop', on_delete=django.db.models.deletion.SET_NULL,)),
            ],
        ),
        # RunPython.noop does nothing but allows reverse migrations to occur
        migrations.RunPython(add_cones, reverse_code=migrations.RunPython.noop)
    ]

마이그레이션의 배포 및 관리 정리

  1. 마이그레이션을 실행하기 전에 항상 데이터를 백업하라.
  2. 배포하기전에 롤백이 가능한지 확인 하라
  3. 프로젝트에 수백만 개의 행이 있는 테이블이 있는 경우 프로덕션에서 마이그레이션을 실행하기 전에 스테이징 서버에서 해당 크기의 데이터에 대해 먼저 테스트를 수행하라.
  4. MySQL을 사용한다면?
    1. 스키마를 변경하기 전에 데이터베이스 백업은 필수다. MySQL은 스키마 변경에 대한 트랜잭션 지원이 부족하다.
    2. 데이터베이스에 변경을 가하기 전에 프로젝트를 읽기 전용으로 설정해라.
    3. 상당히 큰 테이블의 경우 주의하지 않으면 스키마 변경에 상당한 시간이 소요될 수 있다. (몇시간)

4. 장고 모델 디자인

좋은 장고 모델을 디자인 하는 방법을 위한 전략은 다음과 같다.

정규화하기

  • 장고 모델 디자인은 항상 정규화로부터 시작한다.
  • 이미 모델에 포함된 데이터가 중복되는 일이 없도록 한다.
  • 이 단계에서는 정규화에 집중한다. 미숙한 비정규화는 피하는것이 좋다.

캐시와 비정규화

비정규화를 최대한 피하고 캐시를 세팅하자. 상당부분 비정규화로 인한 문제를 해결해주기도 한다. (챕터26의 Finding and Reducing Bottlenecks에서 자세히 다룬다.)

반드시 필요한 경우에만 비정규화를 하자

  • 비정규화는 프로젝트를 복잡하게 만들고 데이터를 손실시킬 수 있는 위험을 야기한다.
  • 필요성이 생긴다면 되도록 캐시를 연구해보고, 챕터26의 Finding and Reducing Bottlenecks에서 연급된 방법으로도 한계에 이르렀을 때 비정규화에 대한 패턴과 개념을 생각해보자.

언제 Null을 쓰고 언제 공백을 쓰는가?

  • 모델 필드를 정의할 때 null, blank를 지정할 수 있다. 기본은 둘다 False이다.
  • null은 DB와 연관되어 있으며 주어진 데이터베이스 컬럼이 null값을 가질것인지 아닌지를 정의한다.
  • blank는 유효성과 관련되어 있으며 fom.is_valid()가 호출될 때 유효성 검사에 이용된다.
  • null=True, blank=Flase는 DB레벨에서는 null이 될 수 있지만, application레벨에서는 required필드인것을 의미한다.
  • 일반적으로 CharField, TextField와 같은 문자열 기반 필드는 null=True를 정의해서는 안된다. "데이터 없음"에 대해 두 가지 값인 None과 빈문자열인 두 가지 상태를 갖게되기 때문이다. 일반적으로 빈 문자열을 "데이터 없음"으로 갖게 하는 것이 장고 컨벤션이다. → 문자열 관련 필드를 지정할 때 blank=True만 지정하면 된다. 빈값이 빈 문자열로 저장된다. (참고링크)
    class Person(models.Model):
        name1 = models.CharField(max_length=255) # 폼에서 빈값으로 제출하면 허용하지 않는다.
        name2 = models.CharField(max_length=255, blank=True)  # 폼에서 빈값으로 제출하면 DB에 빈 문자열로 저장된다.
        name3 = models.CharField(max_length=255, null=True, blank=True) # 폼에서 빈값으로 제출하면 DB에 null로 저장된다.

CharField, TextField, SlugField, EmailField, CommaSeparated-IntegerField, UUIDField필드

  • null=True → unique=True와 blank=True로 모두 설정했다면 괜찮다. 공백으로 여러 개체를 저장할 때 고유 제약조건 위반 방지를 위해 null=True가 필요하다.
  • blank=True → 위젯이 빈 값을 허용하기를 원한다면 설정한다. null=True일때 데이터베이스에서는 빈 값이 Null로 저장되고 null=False일 때 빈 문자열이 저장된다.

FileField, ImageField

  • null=True → 사용하지 않는걸 추천한다. 내부적으로는 CharField를 통해 이미지 경로 등을 저장하므로 CharField와 동일한 패턴을 따른다.
  • blank=True → 사용할 수 있다. CharField와 동일한 패턴을 따른다.

BooleanField

  • null=True → 사용한다.
  • blank=True → 기본값은 blank=True이다.

IntegerField, FlaotField, DecimalFiled, DurationField 등

  • null=True → 해당 값이 데이터베이스에 NULL로 들어가도 문제가 없다면 이용한다.
  • blank=True → 위젯에서 해당 값이 빈 값을 받아와도 문제가 없다면 이용한다. (null=True와 함께 이용한다.)

DateTimeField, DateField, TimeField 등

  • null=True → 해당 값이 데이터베이스에 NULL로 들어가도 문제가 없다면 이용한다.
  • blank=True → 위젯에서 빈 값을 받아와도 문제 없거나 auto_now, auto_now_add를 이용하고 있다면 이용한다. (null=True와 함께 이용한다.)

ForeignKey, OneToOneField, ManyToManyField, GenericIPAddressField

  • null=True → 해당 값이 데이터베이스에 NULL로 들어가도 문제가 없다면 이용한다.
  • blank=True → 위젯에서 해당 값이 빈 값을 받아와도 문제가 없다면 이용한다. (null=True와 함께 이용한다.)

ManyToManyField

  • null=True → NULL은 아무런 영향이 없다.
  • blank=True → 위젯에서 해당 값이 빈 값을 받아와도 문제가 없다면 이용한다.

JSONField

  • null=True → 써도 좋다.
  • blank=True → 써도 좋다.

Binary Field는 언제 사용하나?

  • raw binary data 또는 byte를 저장하는 필드이다.
  • filter, exclude, 기타 SQL 액션들이 적용되지 않는다.
  • 메시지팩 형식의 콘텐츠, 로우 센서 데이터, 압축 데이터(base64)등의 저장에 사용된다.
  • 크기가 방대해질 수 있고 이로인해 데이터베이스가 느려질 수 있으므로 이 병목 지점의 데이터를 파일 형태로 저장하고 FileField에 레퍼런스만 저장하는 방법도 있다.
  • 파일을 직접 저장하는것은 안티패턴이다.

데이터베이스 필드에 파일 저장 시 문제점

  1. read/write속도는 데이터베이스보다 파일 시스템이 빠르다.
  2. 데이터베이스 백업 속도가 느려진다.
  3. 파일 자체에 접근하기 위해서 앱(장고)레이어와 데이터베이스 레이어 둘 다를 거쳐야 한다.

범용 관계 피하기

  • 우리는 범용 관계(generic relations)이용과 models.field.GenericForeignKey이용에 부정적이다.
  • 프로젝트에서 이 두개가 빈번하게 이용되고 있다면 머지 않아 큰 문제가 발생할거라는 증거이다.
  • 범용 관계란 한 테이블로부터 다른 테이블을 서로 제약 조건이 없는 외부 키(GenericForeignKey)로 바인딩하는 것이다.
  • 이는 외부 키 제약조건이 존재하지 않는 NoSQL DB를 이용하는 것과 비슷하다.

다음 문제가 야기될 수 있다.

  1. 모델 간의 인덱싱이 존재하지 않으면 쿼리 속도에 손해를 가져오게 된다.
  2. 다른 테이블에 존재하지 않는 레코드를 참조할 수 있는 데이터 충돌의 위험성이 존재한다.

다음과 같이 고려하자.

  • 범용 관계와 GenericForeignKey 이용은 피한다.
  • 범용 관계가 필요하다면 모델 디자인을 바꾸거나 Database 필드를 통해 해결할 수 있는지 확인하자.
  • 불가피하게 이용해야만 한다면 서드파티 앱을 사용해보자.

Choices와 Sub-Choices모델 상수

모델 필드에 선택 항목을 추가할 수 있다. 튜플을 이용해 구조를 정의한다. (튜플 기반 접근)

# orders/models.py
from django.db import models
class IceCreamOrder(models.Model):
    FLAVOR_CHOCOLATE = 'ch'
    FLAVOR_VANILLA = 'vn'
    FLAVOR_STRAWBERRY = 'st'
    FLAVOR_CHUNKY_MUNKY = 'cm'
    FLAVOR_CHOICES = (
        (FLAVOR_CHOCOLATE, 'Chocolate'),
        (FLAVOR_VANILLA, 'Vanilla'),
        (FLAVOR_STRAWBERRY, 'Strawberry'),
        (FLAVOR_CHUNKY_MUNKY, 'Chunky Munky')
    )
    flavor = models.CharField(
        max_length=2,
        choices=FLAVOR_CHOICES
    )

다음과 같이 활용이 가능하다.

>>> from orders.models import IceCreamOrder
>>>IceCreamOrder.objects.filter(flavor=IceCreamOrder.FLAVOR_CHOCOLATE)
[<icecreamorder: 35>, <icecreamorder: 42>, <icecreamorder: 49>]

Choices에 열거형 타입(Enumeration Types) 사용하기

Django에서 choices에 대해 열거형 타입을 사용하기를 권장한다. 3.0부터 사용이 가능하다.

from django.db import models
class IceCreamOrder(models.Model):
    class Flavors(models.TextChoices):
        CHOCOLATE = 'ch', 'Chocolate'
        VANILLA = 'vn', 'Vanilla'
        STRAWBERRY = 'st', 'Strawberry'
        CHUNKY_MUNKY = 'cm', 'Chunky Munky'

    flavor = models.CharField(
        max_length=2,
        choices=Flavors.choices
    )

다음과 같이 활용이 가능하다.

>>> from orders.models import IceCreamOrder
>>> IceCreamOrder.objects.filter(flavor=IceCreamOrder.Flavors.CHOCOLATE)
[<icecreamorder: 35>, <icecreamorder: 42>, <icecreamorder: 49>]

열거형을 사용할경우 단점

  • 열거 유형에는 Named groups의 사용이 불가능하다. 선택 항목에 카테고리를 포함하고 싶다면 튜플 기반 접근 방식을 사용해야 한다.
  • str 및 int이외에 다른 type을 원하면 직접 정의해야 한다.

열거형을 주로 사용하고 위의 단점에 부딪히면 튜플 기반 방법으로 전환하라.

5. Model Manager

모델에 질의를 하게 되면 장고의 ORM을 통한다. 이 때 모델 매니저(Model Manager)라는 데이터베이스와 연동되는 인터페이스를 호출하게 된다.

다음과 같이 커스텀 모델 매니저를 정의할 수 있다.

from django.db import models
from django.utils import timezone

class PublishedManager(models.Manager):
    def published(self, **kwargs):
        return self.filter(pub_date__lte=timezone.now(), **kwargs)

class FlavorReview(models.Model):
    review = models.CharField(max_length=255)
    pub_date = models.DateTimeField()

    # add our custom model manager
    objects = PublishedManager()

다음과 같이 사용이 가능하다

>>> from reviews.models import FlavorReview
>>> FlavorReview.objects.count()
35
>>> FlavorReview.objects.published().count()
31

그러나 기존 커스텀 매니저를 교체하는것이 아닌 두번째 모델 매니저를 추가하는것이 더 합리적일 수 있다.

from django.db import models
from django.utils import timezone

class PublishedManager(models.Manager):
    def get_queryset(self):
        return super(UseredManager, self).get_queryset().filter(pub_date__lte=timezone.now())

class FlavorReview(models.Model):
    review = models.CharField(max_length=255)
    pub_date = models.DateTimeField()

    # add our custom model manager
    objects = models.Manager()
    published = PublishedManager() 

다음과 같이 사용이 가능하다

>>> from reviews.models import FlavorReview
>>> FlavorReview.objects.filter().count()
35
>>> FlavorReview.published.filter().count()
31

어떻게 사용해야 좋을까?

표면적으로는 기존 model manager를 교체하는게 현명하게 보이지만 이 방법을 사용할 때에는 신중해야 한다.

  1. 모델 상속을 사용할 때 추상화 기초 클래스의 자식은 부모 모델 관리자를 받지만 접합 기반 클래스의 자식들은 그렇지 못한다.
  2. 모델 클래스에 적용되는 첫 번째 매니저는 장고가 기본값으로 하는 매니저다. 즉 QuerySet에서 예측할 수 없는 결과로 나타날 수 있다.

Model Manager의 작동 순서를 항상 염두해야 한다.
⇒ 새 이름을 가진 사용자 정의 모델 관리자 위에 objects=models.Manager()를 설정하라.

참고 사이트 : link

7. 거대 모델(Fat Models) 이해하기

  • 데이터 관련 코드를 뷰나 템플릿에 넣기 보다는 모델 메서드, 클래스 메서드, 프로퍼티 심지어는 매니저 메서드 안에 넣어 캡슐화 하는 개념이다.
  • 다음과 같이 뷰나 여타의 작업에서 동일한 로직을 이용 가능하다.
    • Review.create_review(cls, user, rating, title, description)와 같이 리뷰를 생성하는 클래스 메서드를 정의한다. (HTML이나 REST에서 사용)
    • Review.product_average와 같이 리뷰된 프로젝트의 평균 점수를 반환하는 프로퍼티를 정의한다.
    • Review.found_useful(self, user, yes)와 같이 해당 리뷰가 유용했는지 아닌지 사용자가 기록할 수 있는 메서드를 정의한다. (HTML이나 REST에서 사용)
  • 그러나 너무 많은 로직을 모델 안에 넣으면 god object문제를 야기할 수 있다.
  • 메서드들과 클래스 메서드, 프로퍼티들은 유지한 채 그것들이 지닌 로직을 모델 행동(model behaviors)나 상태 없는 헬퍼 함수(stateless helper function)로 이전하기도 한다.

모델 행동(믹스인)

모델 행동은 믹스인을 통한 캡슐화와 구성화의 개념으로 이루어진다. 모델은 추상화 모델로부터 로직들을 상속 받는다.

Section10.2의 Using Mixins With CBV.에서 자세히 살펴본다.

상태 없는 헬퍼 함수

  • 모델로부터 로직을 떼어나 유틸리티 함수로 넣어 독립적으로 구성한다.
  • 단점은 해당 함수들이 자신의 상태를 가지지 않음으로 함수에 더 많은 인자를 필요로 하게 된다.
  • Chapter 31: What Abount Those Random Utilities?에서 다룬다.

9. 요약

  • 모델은 장고 프로젝트의 기초이기에 신중하게 설계 한다.
  • 정규화를 시작하고 충분히 다른 옵션을 고려한 후에 비정규화한다.
  • raw query로 복잡한 쿼리를 단순화하거나 적절한 캐싱으로 성능 문제를 해결하라
  • 모델 상속을 사용하기로 결정했다면 접합 모델(concrete model)이 아니라 추상화 기초 클래스로부터 상속하라.
  • null=True, blank=True옵션을 이용할 때에는 문제에 주의하라
  • django-model-utils와 django-extensions이 유용할 수 있다.
  • 거대 모델은 로직을 쉽게 캡슐화할 수 있지만 god objects를 유발할 수 있다.

0개의 댓글