← 블로그 홈

Django Ninja ModelSchema와 PatchDict 완전 활용 가이드: 더 스마트한 API 개발

Django Ninja ModelSchema와 PatchDict 완전 활용 가이드

Django Ninja ModelSchema와 PatchDict 완전 활용 가이드

Django Ninja를 사용하면서 매번 모델 필드를 Schema로 다시 정의하는 것이 번거로웠던 경험이 있나요? 또는 PATCH 요청에서 선택적 필드 업데이트를 처리할 때 복잡한 로직을 작성해야 했던 적이 있나요?

Django Ninja의 ModelSchemaPatchDict는 이런 문제들을 우아하게 해결해주는 강력한 도구들입니다. 이 포스트에서는 이 두 기능을 실무에서 어떻게 효과적으로 활용할 수 있는지 상세한 예제와 함께 알아보겠습니다.

🎯 학습 목표

  • ModelSchema의 다양한 활용 방법과 최적화 기법
  • PatchDict를 활용한 우아한 PATCH 요청 처리
  • 실무에서 자주 마주치는 시나리오별 해결책
  • 보안과 성능을 고려한 스키마 설계 전략

📋 1. ModelSchema 기초: Django 모델에서 스키마 자동 생성

1.1 기본 사용법

Django Ninja의 ModelSchema는 Django 모델에서 자동으로 Pydantic 스키마를 생성해주는 혁신적인 기능입니다.

# models.py
from django.db import models
from django.contrib.auth.models import User

class Category(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    tags = models.CharField(max_length=200, blank=True)
    is_published = models.BooleanField(default=False)
    view_count = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    def __str__(self):
        return self.title
# schemas.py
from ninja import ModelSchema
from .models import Post, Category

# 기본 ModelSchema 사용
class PostSchema(ModelSchema):
    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'is_published', 'created_at']

# 생성되는 스키마는 다음과 동일합니다:
# class PostSchema(Schema):
#     id: int
#     title: str
#     content: str
#     is_published: bool
#     created_at: datetime

1.2 기존 방식과의 비교

# 기존 방식 (수동 스키마 정의)
from ninja import Schema
from datetime import datetime

class PostSchemaManual(Schema):
    id: int
    title: str
    content: str
    is_published: bool
    created_at: datetime
    
    # 필드가 많아질수록 반복 작업 증가
    # 모델 변경 시 스키마도 수동으로 업데이트 필요

# ModelSchema 방식 (자동 생성)
class PostSchemaAuto(ModelSchema):
    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'is_published', 'created_at']
    
    # 모델 변경이 자동으로 반영됨
    # 타입 안정성 보장
    # 코드 중복 제거

1.3 API 엔드포인트 활용

# api.py
from ninja import NinjaAPI
from django.shortcuts import get_object_or_404
from .models import Post
from .schemas import PostSchema

api = NinjaAPI()

@api.get("/posts", response=list[PostSchema])
def list_posts(request):
    """포스트 목록 조회"""
    return Post.objects.filter(is_published=True)

@api.get("/posts/{post_id}", response=PostSchema)
def get_post(request, post_id: int):
    """포스트 상세 조회"""
    return get_object_or_404(Post, id=post_id, is_published=True)

🔧 2. ModelSchema 고급 활용법

2.1 필드 선택과 제외 전략

2.1.1 보안을 고려한 필드 선택

# 안전하지 않은 방법 - 절대 사용 금지!
class UnsafeUserSchema(ModelSchema):
    class Meta:
        model = User
        fields = "__all__"  # 패스워드까지 노출됨!

# 안전한 방법 - 명시적 필드 선택
class SafeUserSchema(ModelSchema):
    class Meta:
        model = User
        fields = ['id', 'username', 'first_name', 'last_name', 'email', 'date_joined']

# 또는 제외 방식 사용
class SafeUserSchemaExclude(ModelSchema):
    class Meta:
        model = User
        exclude = ['password', 'user_permissions', 'groups', 'last_login']

2.1.2 역할별 스키마 설계

# 공개 API용 스키마 (최소 정보)
class PostPublicSchema(ModelSchema):
    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'created_at']

# 관리자용 스키마 (상세 정보)
class PostAdminSchema(ModelSchema):
    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'author', 'category', 
                 'is_published', 'view_count', 'created_at', 'updated_at']

# 작성자용 스키마 (편집 가능 정보)
class PostAuthorSchema(ModelSchema):
    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'category', 'tags', 'is_published']

2.2 관계 필드 처리 및 중첩 스키마

# 관계 필드가 포함된 고급 스키마
class CategorySchema(ModelSchema):
    class Meta:
        model = Category
        fields = ['id', 'name', 'description']

class UserSimpleSchema(ModelSchema):
    class Meta:
        model = User
        fields = ['id', 'username', 'first_name', 'last_name']

class PostDetailSchema(ModelSchema):
    # 관계 필드 오버라이드
    author: UserSimpleSchema
    category: CategorySchema
    
    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'author', 'category', 
                 'tags', 'is_published', 'view_count', 'created_at']

# API에서 사용
@api.get("/posts/{post_id}/detail", response=PostDetailSchema)
def get_post_detail(request, post_id: int):
    """포스트 상세 정보 (관계 필드 포함)"""
    return get_object_or_404(
        Post.objects.select_related('author', 'category'), 
        id=post_id
    )

2.3 계산된 필드와 커스텀 필드

from ninja import Field

class PostWithStatsSchema(ModelSchema):
    # 계산된 필드 추가
    author_name: str = Field(..., description="작성자 이름")
    category_name: str = Field(..., description="카테고리 이름")
    is_recent: bool = Field(..., description="최근 작성 여부")
    read_time: int = Field(..., description="예상 읽기 시간(분)")
    
    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'view_count', 'created_at']
    
    @staticmethod
    def resolve_author_name(obj):
        return f"{obj.author.first_name} {obj.author.last_name}".strip() or obj.author.username
    
    @staticmethod
    def resolve_category_name(obj):
        return obj.category.name
    
    @staticmethod
    def resolve_is_recent(obj):
        from django.utils import timezone
        from datetime import timedelta
        return obj.created_at >= timezone.now() - timedelta(days=7)
    
    @staticmethod
    def resolve_read_time(obj):
        # 간단한 읽기 시간 계산 (분당 200단어 기준)
        word_count = len(obj.content.split())
        return max(1, word_count // 200)

# 사용 예시
@api.get("/posts/{post_id}/stats", response=PostWithStatsSchema)
def get_post_with_stats(request, post_id: int):
    """포스트 통계 정보 포함 조회"""
    return get_object_or_404(
        Post.objects.select_related('author', 'category'), 
        id=post_id
    )

🔄 3. PatchDict: PATCH 요청의 혁신

3.1 기존 PATCH 처리의 문제점

# 기존 방식의 문제점들
from ninja import Schema
from typing import Optional

class PostUpdateSchema(Schema):
    title: Optional[str] = None
    content: Optional[str] = None
    category_id: Optional[int] = None
    tags: Optional[str] = None
    is_published: Optional[bool] = None

@api.patch("/posts/{post_id}")
def update_post_old_way(request, post_id: int, payload: PostUpdateSchema):
    post = get_object_or_404(Post, id=post_id)
    
    # 문제 1: 명시적으로 제공되지 않은 필드도 None으로 설정될 수 있음
    # 문제 2: 실제로 업데이트된 필드를 구분하기 어려움
    # 문제 3: validation 로직이 복잡해짐
    
    update_data = payload.dict(exclude_unset=True)  # 해결책이지만 번거로움
    
    for field, value in update_data.items():
        if value is not None:  # 추가 검증 필요
            setattr(post, field, value)
    
    post.save()
    return {"message": "Updated successfully"}

3.2 PatchDict로 우아한 해결

from ninja import PatchDict

# 기본 스키마 (모든 필드가 required)
class PostUpdateSchema(Schema):
    title: str
    content: str
    category_id: int
    tags: str
    is_published: bool

@api.patch("/posts/{post_id}")
def update_post_with_patchdict(request, post_id: int, payload: PatchDict[PostUpdateSchema]):
    """PatchDict를 사용한 우아한 PATCH 처리"""
    post = get_object_or_404(Post, id=post_id)
    
    # payload는 dict 타입이며, 실제로 제공된 필드만 포함됨
    for field, value in payload.items():
        setattr(post, field, value)
    
    post.save()
    
    return {"message": f"Updated fields: {list(payload.keys())}"}

# 사용 예시:
# PATCH /posts/1 {"title": "새 제목"}
# → payload = {"title": "새 제목"}
# 
# PATCH /posts/1 {"title": "새 제목", "is_published": true}  
# → payload = {"title": "새 제목", "is_published": true}

3.3 PatchDict와 ModelSchema 조합

# ModelSchema와 PatchDict 조합으로 최강의 PATCH API
class PostPatchSchema(ModelSchema):
    class Meta:
        model = Post
        fields = ['title', 'content', 'category', 'tags', 'is_published']
        # 모든 필드가 required로 정의되지만 PatchDict가 optional로 만들어줌

@api.patch("/posts/{post_id}")
def update_post_advanced(request, post_id: int, payload: PatchDict[PostPatchSchema]):
    """ModelSchema + PatchDict 조합"""
    post = get_object_or_404(Post, id=post_id)
    
    # 관계 필드 특별 처리
    if 'category' in payload:
        category_id = payload.pop('category')
        post.category_id = category_id
    
    # 나머지 필드 일괄 업데이트
    for field, value in payload.items():
        setattr(post, field, value)
    
    post.save()
    
    # 업데이트된 포스트 반환
    return PostDetailSchema.from_orm(post)

3.4 복잡한 비즈니스 로직과 PatchDict

from django.db import transaction
from django.utils import timezone

class PostAdvancedPatchSchema(ModelSchema):
    class Meta:
        model = Post
        fields = ['title', 'content', 'category', 'tags', 'is_published']

@api.patch("/posts/{post_id}/advanced")
def update_post_with_business_logic(request, post_id: int, payload: PatchDict[PostAdvancedPatchSchema]):
    """비즈니스 로직이 포함된 고급 PATCH 처리"""
    
    with transaction.atomic():
        post = get_object_or_404(Post, id=post_id)
        
        # 권한 검증
        if post.author != request.user:
            return {"error": "Permission denied"}, 403
        
        updated_fields = []
        
        # 제목 업데이트 시 특별 로직
        if 'title' in payload:
            old_title = post.title
            new_title = payload['title']
            post.title = new_title
            updated_fields.append('title')
            
            # 로그 기록
            print(f"Title changed: '{old_title}' → '{new_title}'")
        
        # 발행 상태 변경 시 특별 로직
        if 'is_published' in payload:
            if payload['is_published'] and not post.is_published:
                # 발행 시점 기록
                post.published_at = timezone.now()
                updated_fields.append('published_at')
                
                # 알림 발송 등의 로직
                send_publication_notification(post)
            
            post.is_published = payload['is_published']
            updated_fields.append('is_published')
        
        # 카테고리 변경 시 특별 로직
        if 'category' in payload:
            old_category = post.category.name
            new_category = get_object_or_404(Category, id=payload['category'])
            post.category = new_category
            updated_fields.append('category')
            
            print(f"Category changed: '{old_category}' → '{new_category.name}'")
        
        # 나머지 필드 일괄 처리
        for field, value in payload.items():
            if field not in ['title', 'is_published', 'category']:
                setattr(post, field, value)
                updated_fields.append(field)
        
        post.updated_at = timezone.now()
        post.save()
        
        return {
            "message": "Post updated successfully",
            "updated_fields": updated_fields,
            "post": PostDetailSchema.from_orm(post)
        }

def send_publication_notification(post):
    """발행 알림 발송 (예시)"""
    # 실제로는 Celery 태스크나 이메일 발송 로직
    print(f"📢 새 포스트 발행: {post.title}")

🎨 4. 실무 시나리오별 활용 예제

4.1 사용자 프로필 관리 API

# 복잡한 사용자 프로필 모델
class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to='avatars/', blank=True)
    birth_date = models.DateField(null=True, blank=True)
    phone = models.CharField(max_length=20, blank=True)
    website = models.URLField(blank=True)
    location = models.CharField(max_length=100, blank=True)
    is_public = models.BooleanField(default=True)
    email_notifications = models.BooleanField(default=True)
    
    # 소셜 미디어 링크
    twitter_url = models.URLField(blank=True)
    linkedin_url = models.URLField(blank=True)
    github_url = models.URLField(blank=True)

# 다양한 용도별 스키마
class UserProfilePublicSchema(ModelSchema):
    """공개 프로필 스키마"""
    username: str
    full_name: str
    
    class Meta:
        model = UserProfile
        fields = ['bio', 'location', 'website', 'twitter_url', 'linkedin_url', 'github_url']
    
    @staticmethod
    def resolve_username(obj):
        return obj.user.username
    
    @staticmethod 
    def resolve_full_name(obj):
        return f"{obj.user.first_name} {obj.user.last_name}".strip()

class UserProfilePrivateSchema(ModelSchema):
    """개인 프로필 스키마 (본인만 접근)"""
    username: str
    email: str
    
    class Meta:
        model = UserProfile
        fields = ['bio', 'birth_date', 'phone', 'website', 'location', 
                 'is_public', 'email_notifications', 'twitter_url', 'linkedin_url', 'github_url']

class UserProfilePatchSchema(ModelSchema):
    """프로필 업데이트용 스키마"""
    class Meta:
        model = UserProfile
        fields = ['bio', 'phone', 'website', 'location', 'is_public', 
                 'email_notifications', 'twitter_url', 'linkedin_url', 'github_url']

# API 엔드포인트
@api.get("/users/{user_id}/profile", response=UserProfilePublicSchema)
def get_user_profile(request, user_id: int):
    """사용자 프로필 조회 (공개)"""
    profile = get_object_or_404(UserProfile, user_id=user_id, is_public=True)
    return profile

@api.get("/me/profile", response=UserProfilePrivateSchema)
def get_my_profile(request):
    """내 프로필 조회 (비공개 정보 포함)"""
    profile, created = UserProfile.objects.get_or_create(user=request.user)
    return profile

@api.patch("/me/profile")
def update_my_profile(request, payload: PatchDict[UserProfilePatchSchema]):
    """내 프로필 업데이트"""
    profile, created = UserProfile.objects.get_or_create(user=request.user)
    
    # 유효성 검증
    if 'phone' in payload and payload['phone']:
        # 전화번호 형식 검증 (예시)
        import re
        phone_pattern = r'^[\d\-\+\(\)\s]+$'
        if not re.match(phone_pattern, payload['phone']):
            return {"error": "Invalid phone number format"}, 400
    
    # 프로필 업데이트
    for field, value in payload.items():
        setattr(profile, field, value)
    
    profile.save()
    
    return {
        "message": "Profile updated successfully",
        "updated_fields": list(payload.keys()),
        "profile": UserProfilePrivateSchema.from_orm(profile)
    }

4.2 전자상거래 상품 관리 API

# 상품 모델
class Product(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    sale_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
    stock_quantity = models.IntegerField(default=0)
    is_active = models.BooleanField(default=True)
    category = models.ForeignKey('Category', on_delete=models.CASCADE)
    brand = models.CharField(max_length=100, blank=True)
    weight = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)
    dimensions = models.CharField(max_length=100, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

# 스키마 정의
class ProductListSchema(ModelSchema):
    """상품 목록용 스키마"""
    effective_price: float
    is_on_sale: bool
    category_name: str
    
    class Meta:
        model = Product
        fields = ['id', 'name', 'price', 'sale_price', 'stock_quantity', 'is_active']
    
    @staticmethod
    def resolve_effective_price(obj):
        return float(obj.sale_price if obj.sale_price else obj.price)
    
    @staticmethod
    def resolve_is_on_sale(obj):
        return obj.sale_price is not None and obj.sale_price < obj.price
    
    @staticmethod
    def resolve_category_name(obj):
        return obj.category.name

class ProductDetailSchema(ModelSchema):
    """상품 상세 스키마"""
    effective_price: float
    is_on_sale: bool
    discount_percentage: int
    category_name: str
    is_in_stock: bool
    
    class Meta:
        model = Product
        fields = ['id', 'name', 'description', 'price', 'sale_price', 
                 'stock_quantity', 'brand', 'weight', 'dimensions', 'created_at']
    
    @staticmethod
    def resolve_effective_price(obj):
        return float(obj.sale_price if obj.sale_price else obj.price)
    
    @staticmethod
    def resolve_is_on_sale(obj):
        return obj.sale_price is not None and obj.sale_price < obj.price
    
    @staticmethod
    def resolve_discount_percentage(obj):
        if obj.sale_price and obj.price:
            return int((1 - float(obj.sale_price) / float(obj.price)) * 100)
        return 0
    
    @staticmethod
    def resolve_category_name(obj):
        return obj.category.name
    
    @staticmethod
    def resolve_is_in_stock(obj):
        return obj.stock_quantity > 0

class ProductUpdateSchema(ModelSchema):
    """상품 업데이트용 스키마"""
    class Meta:
        model = Product
        fields = ['name', 'description', 'price', 'sale_price', 'stock_quantity', 
                 'is_active', 'category', 'brand', 'weight', 'dimensions']

# API 엔드포인트
@api.get("/products", response=list[ProductListSchema])
def list_products(request, category_id: int = None, is_active: bool = True):
    """상품 목록 조회"""
    queryset = Product.objects.select_related('category').filter(is_active=is_active)
    
    if category_id:
        queryset = queryset.filter(category_id=category_id)
    
    return queryset

@api.patch("/products/{product_id}")
def update_product(request, product_id: int, payload: PatchDict[ProductUpdateSchema]):
    """상품 정보 업데이트"""
    
    with transaction.atomic():
        product = get_object_or_404(Product, id=product_id)
        
        # 재고 변경 시 특별 로직
        if 'stock_quantity' in payload:
            old_stock = product.stock_quantity
            new_stock = payload['stock_quantity']
            
            # 재고 알림 로직
            if old_stock <= 0 and new_stock > 0:
                # 재입고 알림
                notify_restock(product)
            elif new_stock <= 5 and old_stock > 5:
                # 재고 부족 알림
                notify_low_stock(product)
        
        # 가격 변경 시 특별 로직
        if 'price' in payload or 'sale_price' in payload:
            # 가격 변경 히스토리 기록
            record_price_change(product, payload)
        
        # 카테고리 변경 처리
        if 'category' in payload:
            category = get_object_or_404(Category, id=payload.pop('category'))
            product.category = category
        
        # 나머지 필드 업데이트
        for field, value in payload.items():
            setattr(product, field, value)
        
        product.save()
        
        return {
            "message": "Product updated successfully",
            "product": ProductDetailSchema.from_orm(product)
        }

def notify_restock(product):
    """재입고 알림"""
    print(f"🔄 재입고: {product.name}")

def notify_low_stock(product):
    """재고 부족 알림"""
    print(f"⚠️ 재고 부족: {product.name} (남은 수량: {product.stock_quantity})")

def record_price_change(product, changes):
    """가격 변경 히스토리 기록"""
    print(f"💰 가격 변경: {product.name} - {changes}")

🛡️ 5. 보안과 성능 최적화

5.1 민감한 정보 보호

# 민감한 정보가 포함된 모델
class PaymentMethod(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    card_type = models.CharField(max_length=20)
    card_last_four = models.CharField(max_length=4)
    card_number_encrypted = models.TextField()  # 암호화된 카드번호
    expiry_month = models.IntegerField()
    expiry_year = models.IntegerField()
    cardholder_name = models.CharField(max_length=100)
    billing_address = models.TextField()
    is_default = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

# 안전한 스키마 (민감한 정보 제외)
class PaymentMethodSafeSchema(ModelSchema):
    class Meta:
        model = PaymentMethod
        fields = ['id', 'card_type', 'card_last_four', 'expiry_month', 'expiry_year', 'is_default']
        # card_number_encrypted, billing_address 등 민감한 정보는 절대 노출하지 않음

# 업데이트용 스키마 (카드번호는 수정 불가)
class PaymentMethodUpdateSchema(ModelSchema):
    class Meta:
        model = PaymentMethod
        fields = ['expiry_month', 'expiry_year', 'cardholder_name', 'is_default']

@api.patch("/payment-methods/{payment_id}")
def update_payment_method(request, payment_id: int, payload: PatchDict[PaymentMethodUpdateSchema]):
    """결제 수단 업데이트 (보안 고려)"""
    
    payment_method = get_object_or_404(PaymentMethod, id=payment_id, user=request.user)
    
    # 만료일 검증
    if 'expiry_month' in payload or 'expiry_year' in payload:
        from datetime import date
        current_date = date.today()
        
        expiry_month = payload.get('expiry_month', payment_method.expiry_month)
        expiry_year = payload.get('expiry_year', payment_method.expiry_year)
        
        if expiry_year < current_date.year or (expiry_year == current_date.year and expiry_month < current_date.month):
            return {"error": "Card expiry date cannot be in the past"}, 400
    
    # 기본 결제 수단 변경 시 다른 카드들의 기본 설정 해제
    if payload.get('is_default'):
        PaymentMethod.objects.filter(user=request.user).update(is_default=False)
    
    for field, value in payload.items():
        setattr(payment_method, field, value)
    
    payment_method.save()
    
    return PaymentMethodSafeSchema.from_orm(payment_method)

5.2 성능 최적화 전략

# 대용량 데이터 처리를 위한 최적화
class ArticleSchema(ModelSchema):
    author_name: str
    category_name: str
    comment_count: int
    
    class Meta:
        model = Post
        fields = ['id', 'title', 'created_at', 'view_count']
    
    @staticmethod
    def resolve_author_name(obj):
        # select_related로 이미 로드된 데이터 활용
        return obj.author.username
    
    @staticmethod
    def resolve_category_name(obj):
        # select_related로 이미 로드된 데이터 활용
        return obj.category.name
    
    @staticmethod
    def resolve_comment_count(obj):
        # prefetch_related나 annotation 활용
        return getattr(obj, 'comment_count', 0)

@api.get("/articles", response=list[ArticleSchema])
def list_articles_optimized(request, page: int = 1, per_page: int = 20):
    """최적화된 게시글 목록 조회"""
    from django.db.models import Count
    
    offset = (page - 1) * per_page
    
    # 성능 최적화: select_related, prefetch_related, annotation 활용
    queryset = Post.objects.select_related('author', 'category').annotate(
        comment_count=Count('comments')
    ).filter(is_published=True).order_by('-created_at')[offset:offset + per_page]
    
    return list(queryset)

# 캐싱을 활용한 최적화
from django.core.cache import cache

@api.get("/articles/{article_id}", response=ArticleSchema)
def get_article_cached(request, article_id: int):
    """캐싱을 활용한 게시글 조회"""
    
    cache_key = f"article:{article_id}"
    cached_article = cache.get(cache_key)
    
    if cached_article is None:
        article = get_object_or_404(
            Post.objects.select_related('author', 'category').annotate(
                comment_count=Count('comments')
            ),
            id=article_id,
            is_published=True
        )
        
        # 5분간 캐싱
        cache.set(cache_key, article, 300)
        cached_article = article
    
    return cached_article

📋 결론 및 Best Practices

🎯 핵심 요약

  1. ModelSchema의 활용:
    • 모델과 스키마 간의 동기화 자동화
    • 명시적 필드 선택으로 보안 강화
    • 역할별 스키마 설계로 유연성 확보
  2. PatchDict의 강력함:
    • 우아한 PATCH 요청 처리
    • 실제 변경된 필드만 식별
    • 복잡한 비즈니스 로직과의 완벽한 조화

🛡️ 보안 Best Practices

  • 절대 fields = "__all__" 사용 금지
  • 민감한 정보는 항상 명시적으로 제외
  • 역할 기반 스키마 설계
  • 입력 데이터 검증 강화

⚡ 성능 Best Practices

  • select_related/prefetch_related 적극 활용
  • annotation으로 계산 필드 최적화
  • 적절한 캐싱 전략 수립
  • 페이지네이션 구현

🔧 개발 Best Practices

  • 용도별 스키마 분리 (Public, Private, Update)
  • PatchDict로 PATCH API 단순화
  • 계산된 필드로 클라이언트 편의성 향상
  • 명확한 에러 메시지 제공

Django Ninja의 ModelSchema와 PatchDict는 단순한 편의 기능을 넘어서, 안전하고 효율적인 API 개발을 가능하게 해주는 강력한 도구입니다. 이 기능들을 적절히 활용하면 개발 생산성을 크게 향상시키면서도 높은 품질의 API를 구축할 수 있습니다.

앞으로 Django Ninja 프로젝트에서 이 기능들을 적극적으로 활용해보시기 바랍니다! 🚀


🏷️ 태그

Django Django-Ninja ModelSchema PatchDict API RESTful Pydantic Schema 성능최적화 보안

이 글을 공유해보세요!