[ 글의 목적: django에서 코드 반복을 줄이고 재사용성 증대, 생산성과 유지보수 이득을 가져가기 위한 설계 중 하나인 mixin, manager 활용법 제대로 알기 ]
django model을 사용하면서 "공통된 queryset 호출", "특정 filter 조건에 model 대상 특정 비즈니스 로직의 반복" 을 경험하게 된다. 그럴때마다 admin 로직 또는 api 로직에서 매번 반복하면 코드의 재사용성이 저하된다. 코드 재사용성과 유지보수 향상에 많은 도움이 되는 manager & mixin 활용에 대해 알아보자.
우선 등장하게 될 예제는 https://github.com/Nuung/django-all-about 레포 기준으로 진행한다.
Django model은 모두 default manager이 있고 object라는 attribute에 binding 되어 있다. 즉, objects = models.Manager()
이 모든 모델에 기본적으로 선언이 되어 있다는 것이다. 그래서 queryset handling할때 Model.objects.filter ...
와 같이 접근해야 한다. 물론 objects
이름을 apple
이런식으로 바꿔 Model.apple...
과 같이 접근할 수 있다. 이런식으로 어쎄신 코딩이 가능하다.
하지만 objects
라는 default manager를 계속 사용하는게 협업에 있어서 유리하다. 핵심은 default manager가 이미 존재하고, 이미 objects
라는 Attribute로 존재한다는 것이다.
status
또는 active
라는 choice
값을 가지는 model이 많을 것이다. User
를 예시로 살펴보자.class User(models.Model):
...
active = models.BooleanField(
default=True
verbose_name="활성화 여부",
help_text="유저의 현재 활성화 상태입니다.",
)
django admin을 생각한다면, verbose_name
과 help_text
를 작성하는 센스,, 우리는 user model 대상으로 User.object.filter(active=True)
와 같은 queryset을 구성할 수 있다.
이후 특정 비즈니스 로직에서, 예를 들면 active 유저 대상으로 (1) 광고 메일 집행, (2) 특정 batch 돌릴때 user model filtering을 위해, (3) 다른 비즈니스 로직에서 user 를 filtering할 때 등 다양한 로직이 있을 수 있다. 이럴때 마다 User.object.filter(active=True)
를 쓰고 또 and를 위해 Q
import 하고, 게다가 최적화를 위한 prefetch 등을 고려하다보면 한 쿼리셋을 만드는 코드가 엄청 heavy 해질 수 있다.
이때 Custom Manager를 만들면 간단해진다. 일단 default manager는 models.Manager
를 통해 상속받고 오버라이딩할 수 있다.
# 1번 방법
class ActiveManager(models.Manager):
def actives(self):
return self.get_queryset().filter(active=True)
class User(models.Model):
...
objects = ActiveManager()
# 2번 방법
class ActiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(active=True)
class User(models.Model):
...
objects = models.Manager()
actives = ActiveManager()
위 1번 방법 으로는 User.objects.actives()
로, 2번 방법 으로는 User.actives.all()
로 사용할 수 있다. 2번 방법 이 다른 모델에서도 재사용가능한 manager를 만드는데 유리하다고 볼 수 있다. 하지만 해당 방법에서 주의할 점이 꼭 있다.
get_queryset
method 는 무조건 query_set
을 return 해야한다. 그리고 Manager를 2개 사용하기때문에 어떤 것을 default
로 사용할지 정하는게 굉장히 중요하다!
그리고 ActiveManager는 active
를 사용하는 어떤 모델에서든 사용이 가능해진다. 다른 예제를 하나만 더 살펴보자!
class PublishedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(published_date__lte=timezone.now()).order_by('-published_date')
class Post(models.Model):
...
published_date = models.DateTimeField(blank=True, null=True)
objects = models.Manager()
published = PublishedManager()
def publish(self):
self.published_date = timezone.now()
self.save()
위 예시에서는 new_post = Post(...)
을 하고 new_post.save()
또는 new_post.publish()
를 통해 바로 create 할 수 있다. 전자의 경우 published_date
는 비어있게 되어 있다.
이 점을 활용해서 manager를 통해 Post.objects.all()
와 Post.published.all()
로 2가지 all queryset을 활용할 수 있다. 쉽고 간편하게 날짜를, 속성 및 상태를 필터링한 queryset를 가져올 수 있다.
django.db.models.base.py
의 ModelBase
에 new_class.add_to_class("_meta", Options(meta, app_label))
로 model안의 meta가 정의되는 것을 볼 수 있다. 그리고 _default_manager
property가 정의되어 있는 것을 확인할 수 있다. 이 역시 어디서 가져오느냐! meta
에서 가져온다!@property
def _default_manager(cls):
return cls._meta.default_manager
Options
이라는 class를 사용하는데 django.db.models.options.py
에 정의되어 있다. 바로 여기서! 아래 method를 찾을 수 있다! @cached_property
def default_manager(self):
default_manager_name = self.default_manager_name
if not default_manager_name and not self.local_managers:
# Get the first parent's default_manager_name if there's one.
for parent in self.model.mro()[1:]:
if hasattr(parent, "_meta"):
default_manager_name = parent._meta.default_manager_name
break
if default_manager_name:
try:
return self.managers_map[default_manager_name]
except KeyError:
raise ValueError(
"%s has no manager named %r"
% (
self.object_name,
default_manager_name,
)
)
if self.managers:
return self.managers[0]
[1순위] 는 직접 model의 class Meta
하위에 default_manager_name
을 정의했을 때!
[2순위] 는 부모, 다중 상속의 경우 가장 나중(가장 왼쪽)의 default_manager_name
를 따르는 것!
[3순위] 는 1, 2순위 모두 없고, model 내에 정의된 managers 들의 arrays 중 첫 번째, 즉 코드상 가장 먼저, 가장 위에 manager를 정의한게 default가 된다.
python에서 다중상속을 지원하고, 그런 점을 활용해 "기능적 확장"을 목표로 mixin pattern이라는 형태를 사용한다. java에 비교하자면 interface + abstractc class 느낌인데, 오히려 abstractc에 가깝다. 이는 사실 모든 OOP에 나타나는 현상이긴 한데, python이 순수 객체 지향에다가 손쉽게 다중상속이 되니 두드러지게 나타나는 것 뿐이다.
django를 사용하다보면 model이 가지는 method가 필요하기 마련이다. 그러나 한 모델에 해당 메소드를 계속해서 추가해가다 보니 model이 너무 뚱뚱해진다. 개인적으로 django가 model에 너무 의존적이고 결집도가 높다는 의견, 그렇기 때문에 불편하다는 의견이 이 현상에서 나오지 않나 싶다.
그리고 model간 사용하는 비슷한 성격의 method가 눈에 보인다. 모두 흩어져있으니 영향 범위 파악도 어렵고 유지보수도 어렵다. 이런 부분을 Mixin
으로 빼내는 것이다. Model
은 그 자체 모델정의로 살려두면서 다양한 기능 추가를 Mixin 상속을 통해 꽤할수 있다.
from django.db import models
from django.utils.text import slugify
class TimestampMixin(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class SlugMixin(models.Model):
slug = models.SlugField(unique=True)
class Meta:
abstract = True
def save(self, *args, **kwargs):
if hasattr(self, "title") and not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
class Post(TimestampMixin, SlugMixin):
title = models.CharField(max_length=100)
body = models.TextField()
def __str__(self):
return self.title
Post
라는 model을 위해 abstract
인 model class를 2개나 상속 받았다. 그리고 TimestampMixin
과 SlugMixin
는 어느 모델에서나 상속받을 수 있다. TimestampMixin
에서 사용하는 필드는 이력보관과 업데이트 여부를 위해 통계가 아니고서야 대부분의 table에 포함되는 field이니, 생각보다 굉장히 많이, 자주, 흔히 사용되는 형태다. 아예 models.Model
를 상속받는 자체 BaseModel
을 만들고 시작하는 경우도 많다.class PostStatus(models.TextChoices):
DEFAULT = "DE", _("Default")
PUBLIC = "PU", _("Public")
PRIVATE = "PR", _("Private")
class PublicPostMixin:
def set_public(self):
self.status = PostStatus.PUBLIC.value
self.save()
class PrivatePostMixin:
def set_private(self):
self.status = PostStatus.PRIVATE.value
self.save()
class Post(TimestampMixin, SlugMixin, PublicPostMixin, PrivatePostMixin):
title = models.CharField(max_length=100)
status = models.CharField(
max_length=2,
choices=PostStatus.choices,
default=PostStatus.DEFAULT,
)
body = models.TextField()
def __str__(self):
return self.title
위 모델을 그대로 사용하면서, PostStatus
, PublicPostMixin
, PrivatePostMixin
3개 추가해서 model을 update 해보자.
우린 PublicPostMixin
으로 status가 PostStatus.PUBLIC
인 친구들 대상으로만 또 따로 class & method를 분리해서 model 자체에 대한 응집도를 낮출 수 있다. public을 대상으로한 다양한 queryset
호출 또는 특별한 로직은 model에서가 아니라 여기 mixin에서만 수정하면 model을 사용하는 모든 곳에서 호출이 가능하다.
>>> t = Post(title="first post test model")
>>> t
<Post: first post test model>
>>> t.save()
>>> Post.objects.get(pk=1)
<Post: first post test model>
>>> t.status
'DE'
>>> t.set_public()
>>> t.status
'PU'
class PublicPostMixin:
def pre(self):
if self.status is not PostStatus.PUBLIC:
return
def set_publics(self):
self.status = PostStatus.PUBLIC.value
self.save()
def find_post_expose(self):
self.pre()
...request...
pre
라는 method를 통해 set
이외 method는 PostStatus.PUBLIC
가 아니면 바로 return하게 하고, find_post_expose
와 같이 아예 NetworkIO를, crawling을 하는 method도 추가할 수 있다. 그럼 특정 instance 기반으로 다양한 호출이 가능해 진다. 여기에 celery 와 같은 async worker를 활요하면 퍼포먼스 향상도 가능하다!
게다가 "@static
or @classmethod
" 를 추가한다면 더 재미있고 확장가능해 진다!
@property
에 대해 배경 지식이 있어야 이해가능하다.class Post(TimestampMixin, SlugMixin, PublicPostMixin, PrivatePostMixin):
...
@property
def is_old(self):
if datetime.datetime.now().day - self.created_at.day > 1:
return True
return False
...
@property
이다. 위와 같이 model에 instance의 특정 필터, 조건값을 property로 만들어 매번 체크해야 했던 사항을 아래와 같이 심플하게 축약 가능하다.>>> t = Post.objects.get(pk=1)
>>> t.is_old
False
유용한 django mixin library, django-model-utils 를 통해 model을 위한 다양한 boilerplate
를 맛볼 수 있다. djanog가 추구하는 "디자인 철학"에 굉장히 알맞지 않나라는 생각이 든다.
해당 라이브러리의 from model_utils.managers import QueryManager
를 통해 아래와 같이 빠르게 model의 property로써 queryset을 구성하는 method를 만들 수 있다.
from django.db import models
from model_utils.managers import QueryManager
class Post(models.Model):
...
published = models.BooleanField()
pub_date = models.DateField()
...
objects = models.Manager()
public = QueryManager(published=True).order_by('-pub_date')
django의 model의 이런 점을 잘 활용하면 Django의 View, DRF의 RestAPI, 심지어 admin에서까지 재사용가능한 형태를 만들 수 있다. model 자체가 중심이 되어 일관성을 가지게 되는 것이다. 이게 단점이라고 지적하는 의견도 많다.
확실한점은 Django의 Model의 활용도에 따라 Django Project의 "간결성", "재사용성", "유지보수성", "개발의 속도" 가 차이가 난다는 점이다. Django에 대해 다룬다면, 한 번, Django model에 초점을 제대로 맞춰서 전체 project를 구성하는 것도 좋은 경험이 되리라 생각한다.
Manager vs Query Sets in Django
이라는 흥미로운 주제로 장고의 핵심만 디자인적 모습을 체크하고 있다. 꼭 읽어보길 추천한다.