몰입공간
[Django] 비즈니스 로직에 대한 고찰 (Where to put business logics) 본문
[Django] 비즈니스 로직에 대한 고찰 (Where to put business logics)
sahayana 2023. 4. 25. 01:19
#1. 들어가며..
대부분 Django를 사용하는 프로덕트팀이라면 여러번 고민했을 컨벤션에 대한 내용입니다.
Django에서 비즈니스 로직을 정리하는 대표적인 구조는 다음과 같이 여러개 존재하며,
대부분 공통적으로 Fat Model을 지양하고, 종종 안티패턴으로도 해석되는 장고의 액티브 레코드 특성을 최대한 분리시켜 놓으려는 노력이 다분하다고 생각합니다.
- Proxy Model
- Custom QuerySet and Manager
- Behavior Mixin, Stateless helper
- Service Layer
어느 구조가 가장 완벽하다고 말할 수 없고, 명확하게 이 구조를 쓰라고 말하기도 쉽지 않습니다.
다만, 개인적인 공부와 프로젝트 그리고 실제 실무에서 각기 다르게 앱 구성을 했던 경험을 통해
한번쯤은 느낀점을 정리하고 싶었던 내용입니다.
#2. Proxy Model
Django의 공식 문서에서도 잘 나와있는 구조입니다.
오리지널 모델에 데이터 구조, 즉 필드만 정의하고, 오리지널 모델을 상속받은 새로운 모델에 proxy 속성을 정의하여 비즈니스 로직을 작성하는 방법입니다.
from django.db import models
# Concrete Model
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
# Behavior Model
class MyPerson(Person):
class Meta:
ordering = ["last_name"]
proxy = True
@property
def full_name(self):
return f"{first_name} {last_name}"
def do_something_person(self):
pass
오리지널 모델은 데이터 구조의 역할을 수행하고, 오리지널 모델을 상속받은 프록시 모델 MyPerson에 관련 비즈니스 로직을 정의합니다.
모델 필드를 변경할 수 없지만, 기본적으로 데이터의 상태와 행위를 완벽하게 분리한 구조이며, 프록시 모델 역시 네이밍 컨벤션을 통해 어플리케이션의 규모에 따라 추가 생성하여 행위를 적절하게 세분화시킬 수 있는 장점이 있습니다.
사실 실제로 사용한 적이 없는 방법이지만, 로직을 적용함에 있어서 각기 다른 프록시 모델을 통해 로직을 사용하는 점이 번거로우며,
장고의 Meta 프레임워크를 적용했을 때, 오리지널 모델이 아닌 적용한 프록시 모델을 통해 적용된다는 점을 주의해야 할 것 같습니다.
#3. Custom QuerySet and Manager
모델과 관련된 도메인 로직들을 모델 안에서 캡슐화시키는 장고의 철학이 잘 드러나는 전략입니다.
장고에서 기본적으로 제공하는 모델 관련 쿼리셋, 매니저 API외에 특정 모델과 관련된 추가 메서드를 제공하고 싶을 때 사용합니다.
class VIPUserQuerySet(models.QuerySet):
def filter_latest_results(self):
"""가장 최근 달의 레코드를 조회합니다."""
today = datetime.date.today()
return self.filter(
created_at__year=today.year, created_at__month=today.month
).order_by("-pk")
def filter_specific_results(self, year: int, month: int):
"""특정 년/월의 레코드를 조회합니다."""
return self.filter(created_at__year=year, created_at__month=month)
class VIPUserManager(models.Manager):
def get_queryset(self):
return VIPUserQuerySet(self.model, using=self._db)
def filter_latest_results(self):
return self.get_queryset().filter_latest_results()
def filter_specific_results(self, year: int, month: int):
return self.get_queryset().filter(
created_at__year=year, created_at__month=month
)
# Model
class VIPUser(models.Model):
"""
objects = VIPUserManager()
"""
DRF를 사용하여 API를 제공하는 실제 실무에서 썼던 방식입니다.
특정 모델에만 종속되어 명확한 역할을 가지지만, 규모가 커짐에 따라 모델 규모 역시 뚱뚱해집니다.
또한 커스텀 쿼리셋 및 매니저를 할당하는 것으로 장고의 기본 매니저 패턴을 무시함에 따라 쿼리를 체인할 수 없거나 기존의 쿼리셋으로 부터 예상하지 못한 결과를 얻을 수 있습니다.
특히, 이전에 서술한 오리지널 모델을 상속한 프록시 모델을 작성하는 경우 혹은 Abstract 모델을 정의하는 경우 적용되는 범위가 다를 수 있으니 주의하여야 합니다.
보통 DRY를 원칙을 지키는 로직은 쿼리셋을 작성하고, 이후 매니저에서 커스텀 쿼리셋을 받아 재정의합니다.
실무 얘기를 하니 생각난 것이 있는데, 아래에서도 얘기하겠지만 보통 쿼리나 기능에 직접적인 역할을 하지 않지만 부가적으로 필요한 기능들을 helper 함수로 따로 작성하는 경우가 많습니다.
이 경우 특정 모델에 종속된 helper함수 관리 비용을 줄이기 위해 팀 내에서 논의되었던 방식이 있습니다.
장고 ORM의 경우 objects 인터페이스를 통한 QuerySet API를 제공하는 일종의 퍼사드 패턴으로 되어 있습니다.
특정 모델에만 사용되는 helper함수도 역시 아래와 같은 패턴을 통해 관리하면 역할이 보다 명확해지고, 유지 비용이
감소할 수 있습니다.
좋은 아이디어였다고 생각했는데, 막상 적용되지 않았던 것은 꽤나 아쉬웠습니다.
MyModel.helpers.do_something()
#4. Behavior Mixin, Stateless helper
장고 베스트셀러인 장고 두 숟갈에서 잠시 소개되었던 구조입니다.
파이썬의 강력한 믹스인 플러그인을 모델에 적용하여 하나의 모델에 다양한 기능을 가진 여러 믹스인을 조합하는 방식입니다.
from .behaviors import Permalinkable, Publishable
# Model
class BlogPost(Permalinkable, Publishable, models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
# Mixin Plugin
class Permalinkable(models.Model):
slug = models.SlugField()
class Meta:
abstract = True
def pre_save(self, instance, add):
from django.utils.text import slugify
if not instance.slug:
instance.slug = slugify(self.slug_source)
class Publishable(models.Model):
publish_date = models.DateTimeField(null=True)
class Meta:
abstract = True
def publish_on(self, date=None):
from django.utils import timezone
if not date:
date = timezone.now()
self.publish_date = date
self.save()
@property
def is_published(self):
from django.utils import timezone
return self.publish_date < timezone.now()
기존의 전통적인 모델을 정의하는 방식과 사뭇 다릅니다.
레코드의 행위를 명확하게 구분지어 별도로 플러그인 형태로 정의 후, 오리지널 모델에 믹스인 상속으로 박아버리는
구조입니다.
비즈니스 로직을 파악하기 좋아보이지만, 개인적으로 모델 필드가 산재되어 있어, 규모가 큰 모델의 경우 모델 구조를 파악하기 꽤나 까다로워 보이네요.
필드를 바꿀 수 있다는 것은 장점이지만, 어쨌든 모든 믹스인이 소수의 모델에 최종적으로 의존하게 되는 모습도 좋아보이지는 않습니다.
#5. Service Layer
스택오버플로우나 Django 개발자들에게 아마 가장 많이 언급되는 구조이지 않을까 싶습니다.
저 역시 실제로 장고를 심화로 익히는 과정에서, 가장 마음에 들었던 구조입니다.
본래 자바 스프링에서 사용하는 구조로 View와 Model사이의 요청-응답 인터페이스 역할을 하는 비즈니스 로직 레이어를 별도로 두는 구조입니다.
# Layer structure
feed/services/post
feed/services/comment
feed/services/like
from feed.models import Comment, Post
def create_comment(user_id: int, post_id: int, content: str):
return Comment.objects.create(author_id=user_id, post_id=post_id, content=content)
def delete_comment(user_id: int, comment_id: int):
comment = Comment.objects.filter(id=comment_id, author_id=user_id).get()
comment.delete()
def get_single_comment(comment_id: int):
return Comment.objects.select_related("author", "post").filter(id=comment_id).get()
def get_list_comment(post_id: int, offset: int, limit: int):
return (
Comment.objects.select_related("author", "post")
.filter(post_id=post_id)
.order_by('-likes')[offset : offset + limit]
)
기존 모델이 가진 큰 의존성을 와해시키고, 도메인에 맞게 세분화가 가능합니다.
이를 통해 어플리케이션이 가진 전체적인 비즈니스를 모델 -> 레이어 -> 뷰 를 거쳐 거시적으로 이해하는데 좀 더 편리하다고 생각합니다.
개인적으로 테스트를 할 때도 Unit -> integration -> Client 로 테스트 하는 일련의 과정이 굉장히 매끄럽다고 생각했습니다.
#6. 마치며
다시 한번 말하지만, 어느 패턴이 무조건 좋다라는 것은 없습니다.
또한 어떤 프레임워크를 사용하느냐, 프로젝트 목적이 무엇이냐에 따라 팀 내에서 사용하는 컨벤션이 달라질 수 있습니다.
전체적인 어플리케이션을 이해하는 데 있어서, 비즈니스 로직 뿐만 아니라 시리얼라이징 로직, 최종적으로 VIEW단에서 적용되는양상에 따라 전체적으로 맞는 방법을 찾아가는 수 밖에 없습니다.
한가지 재미있는 사실은 액티브 레코트 패턴은 장고의 기본적인 철학임에도, 위에서 소개한 전략들의 양상은 그 철학을 와해시키는방향으로 나아간다는 것입니다.
결국 기능의 구현보다 유지보수에 들어가는 비용이 더 크다는 것을 어쩌면 반증하는 내용일지도 모르겠네요.
#7. 인용
- 장고 공식문서
- Django model behavior (https://blog.kevinastone.com/django-model-behaviors)
'Programming > Django' 카테고리의 다른 글
[Django] EC2 + Nginx + Gunicorn 장고 서버 배포하기 (Deploying server with AWS) (0) | 2023.08.14 |
---|---|
[Django] 동시성 프로그래밍으로 성능 개선하기 ft. Async view (0) | 2023.04.27 |
[Django] 미들웨어 (What has things to do with middleware in Django) (0) | 2022.05.26 |
[Django] 장고 모델 디자인 주의할 점 (Django model design principle) (0) | 2022.05.23 |
[Django] F() 객체와 annotation (Combined usage with F() and annotation in Django ORM) (0) | 2022.05.18 |