"모든 개발의 첫 걸음은 아름다운 View 함수로 시작되지만, 결국에는 카오스가 된다" - 어느 지친 Django 개발자
Django 프로젝트를 개발하다 보면, View 함수가 점점 커지고 복잡해지는 현상을 경험해 보셨을 겁니다. 처음에는 10줄이었던 함수가 어느새 100줄이 되어있고, 비즈니스 로직, 데이터베이스 로직, 응답 처리 로직이 모두 한 군데 뒤섞여 있습니다. 이런 코드는 테스트하기 어렵고, 디버깅은 더 어렵습니다. 그리고 새로운 개발자가 이 코드를 마주했을 때? 그들의 눈에서 희망의 빛이 사라지는 것이 보일 겁니다.😭😭
(참고로 저또한 그랬습니다...)
이 글에서는 Django 애플리케이션에서 비즈니스 로직을 효과적으로 분리하는 방법과 그 이점에 대해 알아보겠습니다. 코드의 관심사를 명확히 분리하여, 더 깔끔하고, 테스트하기 쉽고, 유지보수하기 쉬운 애플리케이션을 만드는 여정을 함께 시작해봅시다.
아래 예시 코드는 모드 아래 GitHub 주소에서 볼 수 있어요!!ㅎㅎ
배경지식
비즈니스 로직 분리를 논하기 전에, Django의 MVT(Model-View-Template) 패턴을 잠시 상기해보겠습니다:
Model: 데이터 구조와 데이터베이스 상호작용을 담당
View: HTTP 요청을 처리하고 응답을 반환
Template: 사용자에게 보여지는 HTML 렌더링을 담당
그런데 이 구조에서는 "비즈니스 로직"의 위치가 명확하지 않습니다. 많은 Django 개발자들이 View에 모든 로직을 넣는 실수를 하게 됩니다. 이는 마치 주방에서 모든 요리 도구를 한 서랍에 넣는 것과 같습니다. 언젠가는 그 서랍을 열 때마다 아비규환이 펼쳐질 것입니다.
그렇다면 대체 비즈니스 로직이 뭘까요?
비즈니스 로직이란 무엇인가?
비즈니스 로직은 애플리케이션의 핵심 규칙과 계산을 담당하는 코드입니다. 이는 DB와 직접적인 관련이 없는 순수한 작업들을 포함합니다:
- 포인트 계산 알고리즘
- 할인율 계산
- 조건에 따른 값 검증 및 변환
- 복잡한 정렬 및 필터링 로직
- 외부 API 호출 및 데이터 처리
- 비즈니스 규칙에 따른 데이터 검증
💡 팁: 만약 그 코드가 데이터베이스 쿼리를 포함하지 않으면서, 애플리케이션의 핵심 규칙을 구현한다면, 그것은 아마도 비즈니스 로직입니다!
비즈니스 로직 분리의 장점
비즈니스 로직을 분리하는 것은 단순히 코드를 깔끔하게 정리하는 것 이상의 가치가 있습니다:
테스트 용이성: 독립적인 비즈니스 로직은 단위 테스트가 훨씬 쉽습니다. DB나 HTTP 요청 없이도 로직을 테스트할 수 있습니다.
재사용성: 분리된 비즈니스 로직은 다른 View나 심지어 다른 애플리케이션에서도 쉽게 재사용할 수 있습니다.
유지보수성: 코드의 역할이 명확히 분리되어 있어서 변경이 필요할 때 영향 범위를 예측하기 쉽습니다.
가독성: 각 코드 블록의 책임이 명확해져 새로운 개발자도 코드를 쉽게 이해할 수 있습니다.
Django 비즈니스 로직구성 4가지 방법
실무에서 Django를 직접 다루기 전까진 View에 비즈니스 로직을 넣는게 전부라고만 생각했습니다.
하지만 실제 실무에서 Django를 다루다보면 MTV 패턴 안에서 다양하게 비즈니스 로직을 처리할 수 있고 각 서버의 성격에 따라서 처리하는 방법이 다름을 알 수 있었습니다.
이 장에선 제가 실무에서 적용했던 방법들을 정리하려고 합니다.
아래는 비즈니스 로직을 처리하는 대표적인 방법 4가지입니다.
Fat Models | 모델 내부에서 비즈니스 로직을 처리합니다. |
View + Form + Serializer | 일반적으로 생각하는 방식으로 모든 비즈니스 로직을 View에서 처리합니다. |
Services Layer | 비즈니스 로직을 처리할 service.py를 생성하여 처리합니다. |
QuerySets + Manager | ORM을 통해서 비즈니스 로직을 처리합니다. |
Fat Models란?
Fat model이란 말 그대로 모델에 비즈니스 로직을 넣어 크게 만든다는 뜻입니다.
MVC의 기본 설계 패턴은 Fat Model, Thin Controller이다. Model에 기능을 모아두고, Controller은 중개역할만 하는거다.
이 경우, 코드의 중복이 줄어들고, 객체지향적으로 코드를 설계하기 편하다는 장점이 있다.
코드를 보면서 이해해봅시다.
# products/views.py (기본 골격)
from drf_spectacular.utils import extend_schema, OpenApiParameter
from rest_framework import viewsets
from rest_framework.serializers import Serializer
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST
from rest_framework.exceptions import ValidationError
from api_server.api.versioned.v1.products.serializers import ProductSerializer
from products.models import Product
class ProductsViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = [AllowAny, ]
serializer_class = ProductSerializer
queryset = Product.objects.all()
@extend_schema(
parameters=[
OpenApiParameter(name='discount_code', description='할인 코드', required=True, type=str)
]
)
def fatmodel(self, request, *args, **kwargs):
discount_code = request.query_params.get('discount_code')
try:
# 모델 메서드 호출
discounted_products = Product.get_discounted_products(discount_code)
return Response(discounted_products, status=HTTP_200_OK)
except ValidationError as e:
return Response({"error": str(e)}, status=HTTP_400_BAD_REQUEST)
# products/models.py (Fat Model 접근법)
# products/models.py (Fat Model 패턴)
from django.db import models
from django.core.exceptions import ValidationError
from decimal import Decimal
class DiscountCode(models.Model):
code = models.CharField(max_length=20, unique=True)
percentage = models.DecimalField(max_digits=5, decimal_places=2)
is_active = models.BooleanField(default=True)
expires_at = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.code
class Product(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
def __str__(self):
return self.name
def apply_discount(self, discount_code_str):
"""제품에 할인 적용 (Fat Model 패턴)"""
try:
discount = DiscountCode.objects.get(code=discount_code_str, is_active=True)
# 할인 코드 유효성 검증
if discount.expires_at and timezone.now() > discount.expires_at:
raise ValidationError("할인 코드가 만료되었습니다.")
# 할인 계산
discount_amount = self.price * (discount.percentage / Decimal('100'))
discounted_price = self.price - discount_amount
return {
'product': self,
'original_price': self.price,
'discount_code': discount_code_str,
'discount_percentage': discount.percentage,
'discounted_price': discounted_price
}
except DiscountCode.DoesNotExist:
raise ValidationError("유효하지 않은 할인 코드입니다.")
@classmethod
def get_discounted_products(cls, discount_code_str):
"""모든 제품에 할인 적용 (Fat Model 패턴)"""
try:
# 할인 코드 유효성 검증
discount = DiscountCode.objects.get(code=discount_code_str, is_active=True)
if discount.expires_at and timezone.now() > discount.expires_at:
raise ValidationError("할인 코드가 만료되었습니다.")
# 모든 제품에 할인 적용
products = cls.objects.all()
result = []
for product in products:
discount_amount = product.price * (discount.percentage / Decimal('100'))
discounted_price = product.price - discount_amount
result.append({
'id': product.id,
'name': product.name,
'original_price': product.price,
'discounted_price': discounted_price
})
return result
except DiscountCode.DoesNotExist:
raise ValidationError("유효하지 않은 할인 코드입니다.")
Fat Model 패턴의 장점:
- 로직이 모델과 가까이 있어 응집성이 높습니다
- 뷰가 간단해집니다
- Django의 철학과 일치합니다
Fat Model 패턴의 단점:
- 모델이 너무 커지고 복잡해질 수 있습니다
- 여러 모델에 걸친 로직 처리가 어려울 수 있습니다
- 테스트가 복잡해질 수 있습니다
Serializer 패턴 (DRF 활용)
같은 로직을 한번 Serializer에서 처리해볼게요!
@extend_schema(
parameters=[
OpenApiParameter(name='discount_code', description='할인 코드', required=True, type=str)
]
)
def serializer_pattern(self, request, *args, **kwargs):
"""Serializer 패턴을 사용한 할인 로직"""
discount_code = request.query_params.get('discount_code')
if not discount_code:
return Response(
{"error": "할인 코드를 제공해주세요."},
status=HTTP_400_BAD_REQUEST
)
from rest_framework import serializers
try:
products = self.get_queryset()
serializer = DiscountedProductSerializer(
products,
many=True,
context={'discount_code': discount_code}
)
return Response(serializer.data, status=HTTP_200_OK)
except serializers.ValidationError as e:
return Response({"error": str(e)}, status=HTTP_400_BAD_REQUEST)
class DiscountedProductSerializer(serializers.ModelSerializer):
original_price = serializers.DecimalField(max_digits=10, decimal_places=2, read_only=True)
discounted_price = serializers.DecimalField(max_digits=10, decimal_places=2, read_only=True)
class Meta:
model = Product
fields = ['id', 'name', 'original_price', 'discounted_price']
def to_representation(self, instance):
"""직렬화 중 할인 적용"""
data = super().to_representation(instance)
# context에서 할인 코드 가져오기
discount_code_str = self.context.get('discount_code')
if not discount_code_str:
return data
try:
# 할인 코드 유효성 검증
discount = DiscountCode.objects.get(code=discount_code_str, is_active=True)
from django.utils import timezone
if discount.expires_at and timezone.now() > discount.expires_at:
raise serializers.ValidationError("할인 코드가 만료되었습니다.")
# 할인 계산
original_price = instance.price
discount_amount = original_price * (discount.percentage / Decimal('100'))
discounted_price = original_price - discount_amount
# 결과 업데이트
data['original_price'] = str(original_price)
data['discounted_price'] = str(discounted_price)
return data
except DiscountCode.DoesNotExist:
raise serializers.ValidationError("유효하지 않은 할인 코드입니다.")
Serializer 패턴의 장점:
- DRF와 잘 통합됩니다
- 입력 유효성 검증과 출력 형식을 함께 처리할 수 있습니다
- API 응답을 직접 제어할 수 있습니다
Serializer 패턴의 단점:
- DRF에 의존적입니다
- 복잡한 비즈니스 로직은 Serializer를 과도하게 복잡하게 만들 수 있습니다
- 모델과 Serializer 간에 책임 경계가 모호해질 수 있습니다
Services Layer 패턴
같은 로직을 한번 Services Layer를 생성해서 처리해볼게요!
# products/services.py (Services Layer 패턴)
from django.utils import timezone
from django.core.exceptions import ValidationError
from decimal import Decimal
from products.models import Product, DiscountCode
class DiscountService:
@staticmethod
def validate_discount_code(discount_code_str):
"""할인 코드 유효성 검증"""
try:
discount = DiscountCode.objects.get(code=discount_code_str, is_active=True)
if discount.expires_at and timezone.now() > discount.expires_at:
raise ValidationError("할인 코드가 만료되었습니다.")
return discount
except DiscountCode.DoesNotExist:
raise ValidationError("유효하지 않은 할인 코드입니다.")
@staticmethod
def calculate_discount(product, discount_percentage):
"""제품 할인 계산"""
discount_amount = product.price * (discount_percentage / Decimal('100'))
return product.price - discount_amount
@staticmethod
def get_discounted_products(discount_code_str):
"""모든 제품에 할인 적용"""
# 할인 코드 유효성 검증
discount = DiscountService.validate_discount_code(discount_code_str)
# 모든 제품에 할인 적용
products = Product.objects.all()
result = []
for product in products:
discounted_price = DiscountService.calculate_discount(
product, discount.percentage
)
result.append({
'id': product.id,
'name': product.name,
'original_price': product.price,
'discounted_price': discounted_price
})
return result
@extend_schema(
parameters=[
OpenApiParameter(name='discount_code', description='할인 코드', required=True, type=str)
]
)
def service_pattern(self, request, *args, **kwargs):
"""Service Layer 패턴을 사용한 할인 로직"""
discount_code = request.query_params.get('discount_code')
if not discount_code:
return Response(
{"error": "할인 코드를 제공해주세요."},
status=HTTP_400_BAD_REQUEST
)
try:
# 서비스 레이어 호출
discounted_products = DiscountService.get_discounted_products(discount_code)
return Response(discounted_products, status=HTTP_200_OK)
except ValidationError as e:
return Response({"error": str(e)}, status=HTTP_400_BAD_REQUEST)
Services Layer 패턴의 장점:
- 비즈니스 로직이 명확하게 분리됩니다
- 여러 모델에 걸친 복잡한 비즈니스 로직을 처리하기 쉽습니다
- 단위 테스트가 용이합니다
- 재사용성이 높습니다
Services Layer 패턴의 단점:
- 추가적인 코드 레이어가 생깁니다
- 어디까지 서비스 레이어에 두어야 할지 경계 설정이 어려울 수 있습니다
QuerySet + Manager 패턴
Django의 QuerySet과 Manager를 활용하여 비즈니스 로직을 구현하는 방법입니다.
def queryset_pattern(self, request, *args, **kwargs):
"""QuerySet + Manager 패턴을 사용한 할인 로직"""
discount_code = request.query_params.get('discount_code')
if not discount_code:
return Response(
{"error": "할인 코드를 제공해주세요."},
status=HTTP_400_BAD_REQUEST
)
try:
# Manager 메서드 호출
products_with_discount = Product.objects.with_discount(discount_code)
# 직접 응답 형식 구성
result = [{
'id': product.id,
'name': product.name,
'original_price': product.price,
'discounted_price': product.discounted_price
} for product in products_with_discount]
return Response(result, status=HTTP_200_OK)
except ValidationError as e:
return Response({"error": str(e)}, status=HTTP_400_BAD_REQUEST)
class DiscountCode(models.Model):
code = models.CharField(max_length=20, unique=True)
percentage = models.DecimalField(max_digits=5, decimal_places=2)
is_active = models.BooleanField(default=True)
expires_at = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.code
class ProductQuerySet(models.QuerySet):
def with_discount(self, discount_code_str):
"""할인이 적용된 제품 쿼리셋 반환"""
try:
# 할인 코드 유효성 검증
discount = DiscountCode.objects.get(code=discount_code_str, is_active=True)
if discount.expires_at and timezone.now() > discount.expires_at:
raise ValidationError("할인 코드가 만료되었습니다.")
# 제품 목록을 가져온 후 각 제품에 할인 적용
products = list(self)
result = []
for product in products:
discount_amount = product.price * (discount.percentage / Decimal('100'))
discounted_price = product.price - discount_amount
# 원본 제품 객체에 속성 추가
product.discounted_price = discounted_price
product.discount_percentage = discount.percentage
result.append(product)
return result
except DiscountCode.DoesNotExist:
raise ValidationError("유효하지 않은 할인 코드입니다.")
class ProductManager(models.Manager):
def get_queryset(self):
return ProductQuerySet(self.model, using=self._db)
def with_discount(self, discount_code_str):
"""할인이 적용된 제품 목록 반환"""
return self.get_queryset().with_discount(discount_code_str)
class Product(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
# 기본 매니저 교체
objects = ProductManager()
def __str__(self):
return self.name
QuerySet + Manager 패턴의 장점:
- Django의 QuerySet 시스템과 자연스럽게 통합됩니다
- 쿼리 최적화가 용이합니다
- 비즈니스 로직이 데이터 접근과 함께 캡슐화됩니다
QuerySet + Manager 패턴의 단점:
- 복잡한 비즈니스 로직에는 적합하지 않을 수 있습니다
- 데이터베이스 쿼리와 관련 없는 로직은 어색하게 느껴질 수 있습니다
🚨 주의사항 & 한계점
각 패턴을 사용할 때 주의해야 할 점들:
Fat Model 패턴
- 모델이 너무 비대해지면 유지보수가 어려워집니다
Serializer 패턴
- API 중심 애플리케이션에만 적합합니다
Services Layer 패턴
- 과도한 추상화로 코드 이해가 어려워질 수 있습니다
- 서비스 간 의존성 관리가 복잡해질 수 있습니다
QuerySet + Manager 패턴
- 데이터베이스 액세스와 관련 없는 로직에는 부적합합니다
- QuerySet 체인이 복잡해지면 이해하기 어려울 수 있습니다
마무리
이 글에서는 Django에서 비즈니스 로직을 구현하는 4가지 주요 패턴을 살펴보았습니다. 각 패턴은 각자의 장단점이 있습니다. 그렇다면 어떤 패턴을 활용해야할까요?
프로젝트의 특성과 요구사항에 따라 적절한 패턴을 선택해야 합니다.
어떤 패턴을 선택해야 할까?
소규모 프로젝트: Fat Model이나 Serializer 패턴이 간단하고 빠르게 구현할 수 있습니다.
중규모 프로젝트: QuerySet + Manager 패턴이 데이터 액세스와 비즈니스 로직을 적절히 분리하면서 Django의 패턴을 따릅니다.
대규모 프로젝트: Services Layer 패턴이 확장성과 유지보수성을 제공합니다.
가장 중요한 것은 프로젝트 전체에서 일관된 패턴을 사용하는 것입니다. 여러 패턴을 혼합하면 코드 베이스가 혼란스러워지고 유지보수가 어려워집니다.
'프로그래밍 > Django' 카테고리의 다른 글
Django Multi-Tenancy: SaaS 서비스에서 기업별 데이터 분리하기 (0) | 2025.04.15 |
---|---|
Django ORM 완벽 가이드: 기본부터 심화까지 Django-ORM CheatSheet!! (0) | 2025.04.14 |
🚀 Django REST Framework (DRF) - Serializer가 Response를 만들기까지 Serializer Core에 대하여 (1) | 2025.02.27 |
🐜 Locust: 부하 테스트의 끝판왕! (0) | 2025.02.27 |
ElastiCache(Redis) Redis Insight EC2를 활용하여 접근하기 (0) | 2025.02.26 |