← 블로그 홈

Django 6.0 주요 기능 리뷰: django-ninja 개발자가 알아야 할 것들

Django 6.0이 2025년 12월 3일에 공식 릴리스되었습니다. 이번 버전은 Content Security Policy 지원, Template Partials, Background Tasks 프레임워크 등 많은 새로운 기능을 포함하고 있습니다. 특히 django-ninja를 주로 사용하는 API 개발자...

Django 6.0이 2025년 12월 3일에 공식 릴리스되었습니다. 이번 버전은 Content Security Policy 지원, Template Partials, Background Tasks 프레임워크 등 많은 새로운 기능을 포함하고 있습니다. 특히 django-ninja를 주로 사용하는 API 개발자 관점에서 어떤 변화가 있는지, 어떤 부분을 주의해야 하는지 상세히 살펴보겠습니다.

📋 Python 호환성 및 업그레이드 필수 사항

Python 버전 요구사항

Django 6.0은 Python 3.12 이상을 요구합니다. Python 3.10과 3.11 지원이 완전히 중단되었습니다.

지원 버전:

  • Python 3.12 ✅
  • Python 3.13 ✅
  • Python 3.14 ✅

django-ninja 개발자 주의사항: Django-ninja는 Python 3.7+를 지원하지만, Django 6.0을 사용하려면 Python 3.12 이상으로 업그레이드해야 합니다. 대부분의 django-ninja 프로젝트는 호환성 문제 없이 Python 3.12로 마이그레이션할 수 있습니다.

# 업그레이드 전 확인 사항
# pyproject.toml or requirements.txt
Django>=6.0
django-ninja>=1.3.0  # Python 3.12 호환 버전

주요 의존성 버전 요구사항

Python 3.12 호환을 위해 다음 라이브러리들도 최신 버전으로 업데이트해야 합니다:

# requirements.txt
Django>=6.0
django-ninja>=1.3.0
psycopg>=3.1.12  # PostgreSQL 사용 시
psycopg2>=2.9.9  # 또는 psycopg2 사용 시
redis-py>=5.1.0  # Redis 사용 시
Pillow>=10.1.0  # 이미지 처리 시

업그레이드 체크리스트:

  1. Python 3.12+ 설치 및 가상환경 재생성
  2. 모든 의존성 라이브러리 버전 확인 및 업데이트
  3. 테스트 실행으로 호환성 검증
  4. CI/CD 파이프라인의 Python 버전 업데이트

🚀 Django 6.0의 가장 중요한 기능: Background Tasks

Background Tasks 프레임워크 도입

Django 6.0에서 가장 주목할 만한 기능은 내장 Background Tasks 프레임워크입니다. 이제 Celery나 Django-RQ 같은 외부 라이브러리 없이도 백그라운드 작업을 처리할 수 있습니다.

django-ninja 개발자에게 중요한 이유:

  • API 응답 시간을 개선할 수 있습니다
  • 이메일 발송, 이미지 처리 등 시간이 걸리는 작업을 비동기로 처리
  • 더 나은 사용자 경험 제공

기본 사용법

1. Task 정의하기

# tasks.py
from django.core.mail import send_mail
from django.tasks import task


@task
def send_welcome_email(user_email: str, username: str):
    """환영 이메일을 백그라운드에서 발송"""
    subject = f"{username}님, 환영합니다!"
    message = f"회원가입을 축하드립니다, {username}님!"
    
    return send_mail(
        subject=subject,
        message=message,
        from_email=None,
        recipient_list=[user_email],
    )


@task
def process_image(image_path: str, user_id: int):
    """이미지를 백그라운드에서 처리"""
    from PIL import Image
    
    # 이미지 리사이징, 썸네일 생성 등
    image = Image.open(image_path)
    image.thumbnail((800, 800))
    image.save(image_path)
    
    return {"status": "success", "user_id": user_id}

2. django-ninja API에서 Task 실행하기

# api.py
from ninja import NinjaAPI, Schema
from .tasks import send_welcome_email, process_image

api = NinjaAPI()


class UserCreateSchema(Schema):
    username: str
    email: str
    password: str


@api.post("/users")
def create_user(request, data: UserCreateSchema):
    # 사용자 생성 (빠른 동기 처리)
    user = User.objects.create_user(
        username=data.username,
        email=data.email,
        password=data.password
    )
    
    # 환영 이메일은 백그라운드에서 발송 (비동기)
    send_welcome_email.enqueue(
        user_email=user.email,
        username=user.username
    )
    
    # API는 즉시 응답 반환
    return {
        "id": user.id,
        "username": user.username,
        "email": user.email,
        "message": "사용자가 생성되었습니다. 환영 이메일을 발송 중입니다."
    }


@api.post("/images/upload")
def upload_image(request, file: UploadedFile):
    # 파일 저장 (빠른 동기 처리)
    file_path = save_uploaded_file(file)
    
    # 이미지 처리는 백그라운드에서 실행
    process_image.enqueue(
        image_path=file_path,
        user_id=request.user.id
    )
    
    return {
        "status": "uploaded",
        "message": "이미지가 업로드되었습니다. 처리 중입니다."
    }

Settings 설정

# settings.py
TASKS = {
    "default": {
        "BACKEND": "django.core.tasks.backends.database.DatabaseBackend",
    },
    # 또는 Redis를 백엔드로 사용
    "redis": {
        "BACKEND": "django.core.tasks.backends.redis.RedisBackend",
        "LOCATION": "redis://localhost:6379/0",
    }
}

중요: Worker 프로세스 필요

Django는 task를 큐에 넣는 것만 담당하고, 실제 실행은 별도의 worker 프로세스가 필요합니다. 이는 Celery와 유사한 아키텍처입니다.

# Worker 실행 (향후 Django 명령어로 제공될 예정)
# 현재는 서드파티 패키지가 필요할 수 있음
python manage.py run_tasks_worker

django-ninja + Background Tasks 실전 예제

실시간 알림이 필요한 API

from ninja import NinjaAPI, Schema
from django.tasks import task
from typing import List

api = NinjaAPI()


@task
def send_notification_to_users(user_ids: List[int], message: str):
    """여러 사용자에게 알림 발송"""
    from .models import Notification
    
    notifications = [
        Notification(user_id=uid, message=message)
        for uid in user_ids
    ]
    Notification.objects.bulk_create(notifications)
    
    # 실제 푸시 알림도 발송
    for user_id in user_ids:
        send_push_notification(user_id, message)


@api.post("/announcements")
def create_announcement(request, message: str):
    """공지사항 생성 및 전체 사용자에게 알림"""
    
    # 공지사항 생성 (빠름)
    announcement = Announcement.objects.create(
        message=message,
        created_by=request.user
    )
    
    # 모든 사용자 ID 조회 (빠름)
    user_ids = list(User.objects.values_list('id', flat=True))
    
    # 알림 발송은 백그라운드에서 (느림, 비동기 처리)
    send_notification_to_users.enqueue(
        user_ids=user_ids,
        message=message
    )
    
    return {
        "id": announcement.id,
        "message": "공지사항이 생성되었습니다.",
        "notifying_users": len(user_ids)
    }

Background Tasks vs Celery

장점:

  • 추가 의존성 없음 (Django 내장)
  • 설정이 간단함
  • Django ORM과 완벽한 통합

단점:

  • 기능이 제한적 (Celery에 비해)
  • Worker 관리 도구가 아직 성숙하지 않음
  • 프로덕션 사용에는 신중한 접근 필요

django-ninja 개발자를 위한 추천:

  • 소규모 프로젝트: Background Tasks 사용
  • 대규모 프로젝트: 여전히 Celery 사용 권장
  • 마이그레이션 계획: 천천히 Background Tasks로 전환 고려

🔒 Content Security Policy (CSP) 내장 지원

CSP란 무엇인가?

Content Security Policy는 XSS(Cross-Site Scripting) 공격을 방어하는 보안 헤더입니다. 브라우저에게 어떤 소스의 콘텐츠(스크립트, 스타일, 이미지 등)를 로드할 수 있는지 명시적으로 지시합니다.

Django 6.0 이전에는 django-csp 같은 서드파티 패키지가 필요했지만, 이제는 Django에 내장되었습니다.

django-ninja API에서 CSP가 중요한 이유

API 응답에 HTML을 포함하는 경우:

  • Admin 인터페이스를 함께 제공하는 경우
  • API 문서 페이지 (Swagger UI)
  • 이메일 템플릿 미리보기 API
  • 파일 업로드 후 미리보기 기능

django-ninja의 자동 문서화 기능: django-ninja는 자동으로 Swagger UI를 제공하는데, 이 페이지에도 CSP를 적용할 수 있습니다.

기본 설정

# settings.py
from django.utils.csp import CSP

MIDDLEWARE = [
    # ... 다른 미들웨어
    "django.middleware.csp.ContentSecurityPolicyMiddleware",
]

# CSP 정책 설정
SECURE_CSP = {
    "default-src": [CSP.SELF],  # 기본적으로 자신의 도메인만 허용
    "script-src": [
        CSP.SELF,
        CSP.NONCE,  # 인라인 스크립트에 nonce 사용
    ],
    "style-src": [
        CSP.SELF,
        CSP.NONCE,
    ],
    "img-src": [
        CSP.SELF,
        "https:",  # 모든 HTTPS 이미지 허용
    ],
    "connect-src": [
        CSP.SELF,
        "https://api.example.com",  # API 도메인 허용
    ],
}

django-ninja API에서 CSP 적용하기

View별로 CSP 정책 커스터마이징:

# api.py
from ninja import NinjaAPI
from django.views.decorators.csp import csp_update, csp_replace

api = NinjaAPI()


# 기본 CSP 정책 사용
@api.get("/users")
def list_users(request):
    users = User.objects.all()
    return [{"id": u.id, "name": u.username} for u in users]


# 특정 API에서 CSP 정책 변경
@api.get("/dashboard")
@csp_update(script_src=["https://cdn.example.com"])
def dashboard(request):
    """외부 CDN의 스크립트가 필요한 경우"""
    return {"message": "Dashboard with external scripts"}


# CSP 완전히 교체
@api.get("/widget")
@csp_replace(default_src=["*"])
def widget_endpoint(request):
    """특수한 경우에만 사용 (보안 주의)"""
    return {"message": "Widget with custom CSP"}

Swagger UI와 CSP 설정

django-ninja의 자동 문서화 페이지는 인라인 스크립트를 사용합니다. CSP를 활성화하면 nonce를 사용해야 합니다.

# settings.py
SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [
        CSP.SELF,
        CSP.NONCE,  # Swagger UI를 위해 필요
        "https://cdn.jsdelivr.net",  # Swagger UI CDN
    ],
    "style-src": [
        CSP.SELF,
        CSP.NONCE,
        "https://cdn.jsdelivr.net",
    ],
    "img-src": [
        CSP.SELF,
        "data:",  # Swagger UI 로고
    ],
}

Report-Only 모드로 테스트하기

프로덕션에 바로 적용하기 전에 Report-Only 모드로 테스트하는 것이 좋습니다.

# settings.py
# 정책을 위반해도 차단하지 않고 보고만 함
SECURE_CSP_REPORT_ONLY = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
    "report-uri": "/csp-report/",  # 위반 사항 리포트 URL
}


# api.py
@api.post("/csp-report")
def csp_violation_report(request):
    """CSP 위반 사항을 수집"""
    import json
    
    report = json.loads(request.body)
    logger.warning(f"CSP Violation: {report}")
    
    return {"status": "reported"}

django-ninja 프로젝트에 CSP 적용 가이드

단계별 적용:

  1. Report-Only 모드로 시작
    SECURE_CSP_REPORT_ONLY = {...}
    
  2. 위반 사항 모니터링
    • 로그를 확인하고 어떤 리소스가 차단되는지 파악
  3. 정책 조정
    • 필요한 도메인을 허용 목록에 추가
  4. Enforce 모드로 전환
    SECURE_CSP = {...}
    

주의사항:

  • 외부 API 호출이 많은 경우 connect-src 설정 주의
  • CDN 사용 시 해당 도메인 명시적 허용 필요
  • 개발 환경과 프로덕션 환경 설정 분리 권장

💾 Models 및 ORM 개선사항 (django-ninja 개발자 필독)

StringAgg가 모든 데이터베이스에서 사용 가능

이전에는 PostgreSQL에서만 사용 가능했던 StringAgg가 이제 모든 데이터베이스(SQLite, MySQL, Oracle 등)에서 사용할 수 있습니다.

django-ninja API에서 활용 예제:

# models.py
from django.db import models

class Tag(models.Model):
    name = models.CharField(max_length=50)

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    tags = models.ManyToManyField(Tag)


# api.py
from ninja import NinjaAPI, Schema
from django.db.models import StringAgg

api = NinjaAPI()


@api.get("/posts")
def list_posts(request):
    """게시글 목록과 태그를 한 번의 쿼리로 조회"""
    posts = Post.objects.annotate(
        tag_names=StringAgg('tags__name', delimiter=', ')
    ).values('id', 'title', 'tag_names')
    
    return list(posts)
    # 결과: [
    #   {"id": 1, "title": "Django 6.0", "tag_names": "Django, Python, Backend"},
    #   {"id": 2, "title": "FastAPI", "tag_names": "Python, API"}
    # ]

성능 이점:

  • N+1 쿼리 문제 해결
  • API 응답 시간 단축
  • 메모리 사용량 감소

AnyValue Aggregate 추가

랜덤한 값이 필요할 때 유용:

from django.db.models import AnyValue

@api.get("/categories/sample")
def category_samples(request):
    """각 카테고리에서 무작위 상품 하나씩 가져오기"""
    samples = Product.objects.values('category').annotate(
        sample_name=AnyValue('name'),
        sample_price=AnyValue('price'),
        sample_image=AnyValue('image_url')
    )
    
    return list(samples)

Aggregate에 order_by 지원

이제 집계 함수에서 정렬 순서를 지정할 수 있습니다.

from django.db.models import StringAgg, Count

@api.get("/users/{user_id}/activity")
def user_activity(request, user_id: int):
    """사용자의 최근 활동을 시간순으로 집계"""
    user = User.objects.annotate(
        recent_posts=StringAgg(
            'posts__title',
            delimiter=' | ',
            order_by='-posts__created_at'  # 최신순 정렬
        ),
        post_count=Count('posts')
    ).get(id=user_id)
    
    return {
        "user_id": user.id,
        "username": user.username,
        "post_count": user.post_count,
        "recent_posts": user.recent_posts
    }

GeneratedField 자동 갱신

GeneratedField와 F() 표현식으로 할당된 필드가 자동으로 새로고침됩니다 (PostgreSQL, SQLite, Oracle).

# models.py
from django.db import models
from django.db.models import F, GeneratedField

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    discount_rate = models.DecimalField(max_digits=5, decimal_places=2, default=0)
    
    # 자동 계산되는 필드
    final_price = GeneratedField(
        expression=F('price') * (1 - F('discount_rate') / 100),
        output_field=models.DecimalField(max_digits=10, decimal_places=2),
        db_persist=True
    )


# api.py
@api.put("/products/{product_id}/discount")
def update_discount(request, product_id: int, discount_rate: float):
    """할인율 업데이트 - final_price는 자동 계산됨"""
    product = Product.objects.get(id=product_id)
    product.discount_rate = discount_rate
    product.save()
    
    # Django 6.0에서는 final_price가 자동으로 갱신됨
    return {
        "id": product.id,
        "price": float(product.price),
        "discount_rate": float(product.discount_rate),
        "final_price": float(product.final_price)  # 자동 갱신된 값
    }

Model.NotUpdated 예외 추가

강제 업데이트가 실패했을 때 더 명확한 예외가 발생합니다.

from django.db import models

@api.put("/products/{product_id}")
def update_product(request, product_id: int, data: ProductSchema):
    """동시성 제어가 필요한 업데이트"""
    try:
        product = Product.objects.get(id=product_id)
        product.name = data.name
        product.price = data.price
        
        # force_update=True: 해당 레코드가 존재하지 않으면 예외 발생
        product.save(force_update=True)
        
        return {"status": "updated", "id": product.id}
        
    except Product.NotUpdated:
        # 다른 요청에서 이미 삭제되었거나 수정된 경우
        return {"error": "Product was modified or deleted by another request"}, 409

JSONField의 음수 인덱싱 (SQLite)

SQLite에서도 이제 Python 스타일의 음수 인덱싱을 사용할 수 있습니다.

from django.db import models

class UserActivity(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    actions = models.JSONField()  # 배열: ["login", "view", "purchase"]


@api.get("/users/{user_id}/last-action")
def get_last_action(request, user_id: int):
    """사용자의 마지막 행동 조회"""
    activity = UserActivity.objects.filter(
        user_id=user_id,
        actions__-1='purchase'  # 마지막 요소가 'purchase'인 경우
    ).first()
    
    if activity:
        return {
            "user_id": user_id,
            "last_action": activity.actions[-1],
            "all_actions": activity.actions
        }
    return {"message": "No purchase activity found"}

django-ninja Schema와 함께 사용하기

최적화된 API 응답:

from ninja import Schema
from typing import Optional
from decimal import Decimal

class ProductOut(Schema):
    id: int
    name: str
    price: Decimal
    discount_rate: Decimal
    final_price: Decimal
    tags: str  # StringAgg 결과


@api.get("/products", response=list[ProductOut])
def list_products(request):
    """최적화된 상품 목록 API"""
    products = Product.objects.annotate(
        tags=StringAgg('tags__name', delimiter=', ')
    ).values(
        'id', 'name', 'price', 'discount_rate', 'final_price', 'tags'
    )
    
    return list(products)

성능 최적화 팁:

  1. StringAgg로 관계형 데이터를 한 번에 조회
  2. select_related/prefetch_related 대신 annotate 사용 고려
  3. values()로 필요한 필드만 선택
  4. API 응답 크기 감소

⚡ Async 지원 개선: AsyncPaginator

AsyncPaginator와 AsyncPage

Django 6.0은 비동기 환경에서 페이지네이션을 지원하는 AsyncPaginatorAsyncPage를 추가했습니다. django-ninja의 async endpoint와 완벽하게 통합됩니다.

기존 동기 방식:

from ninja import NinjaAPI
from django.core.paginator import Paginator

api = NinjaAPI()

@api.get("/products")
def list_products(request, page: int = 1):
    products = Product.objects.all()
    paginator = Paginator(products, 20)
    page_obj = paginator.get_page(page)
    
    return {
        "count": paginator.count,
        "results": [
            {"id": p.id, "name": p.name}
            for p in page_obj
        ]
    }

새로운 비동기 방식 (Django 6.0):

from ninja import NinjaAPI
from django.core.paginator import AsyncPaginator
from typing import List

api = NinjaAPI()

@api.get("/products")
async def list_products_async(request, page: int = 1):
    """완전한 비동기 페이지네이션"""
    products = Product.objects.all()
    
    # AsyncPaginator 사용
    paginator = AsyncPaginator(products, per_page=20)
    
    # 비동기로 페이지 조회
    page_obj = await paginator.get_page(page)
    
    return {
        "count": await paginator.acount(),  # 비동기 카운트
        "total_pages": paginator.num_pages,
        "current_page": page_obj.number,
        "has_next": await page_obj.has_next(),
        "has_previous": await page_obj.has_previous(),
        "results": [
            {"id": p.id, "name": p.name}
            async for p in page_obj  # 비동기 이터레이션
        ]
    }

django-ninja와 AsyncPaginator 통합 예제

재사용 가능한 Pagination Schema:

# schemas.py
from ninja import Schema
from typing import Generic, TypeVar, List

T = TypeVar('T')

class PaginatedResponse(Schema, Generic[T]):
    count: int
    total_pages: int
    current_page: int
    has_next: bool
    has_previous: bool
    results: List[T]


class ProductSchema(Schema):
    id: int
    name: str
    price: float


# api.py
from django.core.paginator import AsyncPaginator

@api.get("/products", response=PaginatedResponse[ProductSchema])
async def list_products(request, page: int = 1, page_size: int = 20):
    """타입 안전한 비동기 페이지네이션"""
    products = Product.objects.all().order_by('-created_at')
    
    paginator = AsyncPaginator(products, per_page=page_size)
    page_obj = await paginator.get_page(page)
    
    return {
        "count": await paginator.acount(),
        "total_pages": paginator.num_pages,
        "current_page": page_obj.number,
        "has_next": await page_obj.has_next(),
        "has_previous": await page_obj.has_previous(),
        "results": [
            {
                "id": p.id,
                "name": p.name,
                "price": float(p.price)
            }
            async for p in page_obj
        ]
    }

복잡한 쿼리와 비동기 페이지네이션

from django.db.models import Q, Count
from django.core.paginator import AsyncPaginator

@api.get("/search", response=PaginatedResponse[ProductSchema])
async def search_products(
    request,
    query: str,
    category: str = None,
    min_price: float = None,
    max_price: float = None,
    page: int = 1
):
    """복잡한 검색 쿼리에 비동기 페이지네이션 적용"""
    
    # 필터 조건 구성
    filters = Q(name__icontains=query) | Q(description__icontains=query)
    
    if category:
        filters &= Q(category__name=category)
    if min_price:
        filters &= Q(price__gte=min_price)
    if max_price:
        filters &= Q(price__lte=max_price)
    
    # 쿼리셋 생성
    products = Product.objects.filter(filters).annotate(
        review_count=Count('reviews')
    ).order_by('-review_count', '-created_at')
    
    # 비동기 페이지네이션
    paginator = AsyncPaginator(products, per_page=20)
    page_obj = await paginator.get_page(page)
    
    return {
        "count": await paginator.acount(),
        "total_pages": paginator.num_pages,
        "current_page": page_obj.number,
        "has_next": await page_obj.has_next(),
        "has_previous": await page_obj.has_previous(),
        "results": [
            {
                "id": p.id,
                "name": p.name,
                "price": float(p.price),
                "review_count": p.review_count
            }
            async for p in page_obj
        ]
    }

성능 비교 및 사용 시기

AsyncPaginator를 사용해야 하는 경우:

  • 대량의 데이터를 처리하는 API
  • 높은 동시 요청을 처리하는 서비스
  • I/O bound 작업이 많은 경우 (외부 API 호출, 파일 읽기 등)

동기 Paginator를 사용해도 되는 경우:

  • 간단한 CRUD API
  • 트래픽이 낮은 서비스
  • CPU bound 작업이 많은 경우

실제 벤치마크 예시:

# 1000개의 동시 요청 처리 시간
# 동기 Paginator: ~5초
# AsyncPaginator: ~1.5초 (약 3배 빠름)

주의사항

  1. 데이터베이스 연결: 비동기 페이지네이션은 비동기 DB 드라이버가 필요합니다
    # settings.py
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.postgresql',
            # PostgreSQL의 경우 psycopg 3.x 사용
        }
    }
    
  2. ORM 제약사항: 모든 ORM 기능이 비동기로 지원되는 것은 아닙니다
    • select_related(), prefetch_related()는 비동기 지원
    • 일부 복잡한 집계는 동기로 처리될 수 있음
  3. django-ninja 설정: ASGI 서버 사용 필요
    uvicorn myproject.asgi:application --reload
    

⚠️ Breaking Changes (django-ninja 개발자 주의사항)

DEFAULT_AUTO_FIELD가 BigAutoField로 변경

Django 6.0부터 DEFAULT_AUTO_FIELD의 기본값이 AutoField에서 BigAutoField로 변경되었습니다.

영향받는 경우:

  • 새로운 모델을 생성하는 경우
  • 기존 프로젝트에서 명시적으로 설정하지 않은 경우

django-ninja API에서의 영향:

# 기존 코드 (Django 5.x까지)
class Product(models.Model):
    # id는 자동으로 AutoField (최대 2,147,483,647)
    name = models.CharField(max_length=200)


# Django 6.0 이후
class Product(models.Model):
    # id는 자동으로 BigAutoField (최대 9,223,372,036,854,775,807)
    name = models.CharField(max_length=200)

API 응답 타입 변경:

# schemas.py
from ninja import Schema

class ProductSchema(Schema):
    id: int  # 여전히 int로 사용 가능 (BigInt는 Python int로 표현됨)
    name: str


# API에는 영향 없음
@api.get("/products/{product_id}", response=ProductSchema)
def get_product(request, product_id: int):
    product = Product.objects.get(id=product_id)
    return product

마이그레이션 필요 여부:

# settings.py
# 기존 동작을 유지하려면 (권장하지 않음)
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

# 또는 앱별로 설정
# apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    default_auto_field = 'django.db.models.AutoField'

권장사항:

  • 새 프로젝트는 BigAutoField 사용 (기본값)
  • 기존 프로젝트는 settings.py에 명시적으로 설정
  • 마이그레이션 시 데이터베이스 스키마 변경 필요할 수 있음

Email API 변경사항

Python의 modern email API로 전환되었습니다. django-ninja에서 이메일 발송하는 경우 영향을 받을 수 있습니다.

deprecated된 기능:

  • SafeMIMEText, SafeMIMEMultipart 클래스
  • BadHeaderError 예외 (ValueError로 대체)
  • Positional arguments (키워드 인수 필수)

기존 코드:

from django.core.mail import send_mail

@api.post("/contact")
def send_contact_email(request, data: ContactSchema):
    # 기존 방식 (여전히 작동하지만 deprecated warning)
    send_mail(
        "Contact Request",  # subject
        "Message body",      # message
        "from@example.com",  # from_email
        ["to@example.com"],  # recipient_list
        False                # fail_silently (positional - deprecated)
    )
    return {"status": "sent"}

수정된 코드 (Django 6.0 권장):

from django.core.mail import send_mail

@api.post("/contact")
def send_contact_email(request, data: ContactSchema):
    # 키워드 인수 사용 (필수)
    send_mail(
        subject="Contact Request",
        message="Message body",
        from_email="from@example.com",
        recipient_list=["to@example.com"],
        fail_silently=False  # 키워드로 전달
    )
    return {"status": "sent"}

EmailMessage 사용 시:

from django.core.mail import EmailMessage

@api.post("/newsletter")
def send_newsletter(request, data: NewsletterSchema):
    # 모든 인수를 키워드로 전달
    email = EmailMessage(
        subject=data.subject,
        body=data.body,
        from_email="newsletter@example.com",
        to=data.recipients,
        # bcc, cc 등도 키워드로 전달
        bcc=["admin@example.com"]
    )
    email.send()
    return {"status": "sent", "recipients": len(data.recipients)}

MariaDB 10.5 지원 중단

MariaDB 사용자는 10.6 이상으로 업그레이드해야 합니다.

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'mydb',
        'USER': 'myuser',
        'PASSWORD': 'mypassword',
        'HOST': 'localhost',
        'PORT': '3306',
        # MariaDB 10.6+ 필요
    }
}

확인 방법:

mysql --version
# 또는
MariaDB [(none)]> SELECT VERSION();

ORM 표현식 변경 (커스텀 표현식 사용 시)

커스텀 Lookup이나 Expression을 만든 경우:

# 기존 코드 (Django 5.x)
def as_sql(self, compiler, connection):
    lhs, lhs_params = self.process_lhs(compiler, connection)
    rhs, rhs_params = self.process_rhs(compiler, connection)
    params = lhs_params + rhs_params  # list 또는 tuple
    return sql, params


# Django 6.0 (tuple 반환 필수)
def as_sql(self, compiler, connection) -> tuple[str, tuple]:
    lhs, lhs_params = self.process_lhs(compiler, connection)
    rhs, rhs_params = self.process_rhs(compiler, connection)
    params = (*lhs_params, *rhs_params)  # tuple로 unpacking
    return sql, params

대부분의 django-ninja 개발자는 영향받지 않음 (커스텀 ORM 확장을 만들지 않는 한).

🔄 Django 6.0 마이그레이션 체크리스트

단계별 업그레이드 가이드

1단계: 환경 준비

# Python 3.12 설치 확인
python --version  # Python 3.12 이상이어야 함

# 가상환경 재생성
python -m venv venv
source venv/bin/activate  # Linux/Mac
# venv\Scripts\activate  # Windows

# Django 6.0 설치
pip install --upgrade Django
pip install --upgrade django-ninja

# 의존성 업데이트
pip install --upgrade -r requirements.txt

2단계: Settings 검토

# settings.py 체크리스트

# 1. DEFAULT_AUTO_FIELD 확인
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'  # 명시적으로 설정

# 2. Python 버전 체크
import sys
assert sys.version_info >= (3, 12), "Python 3.12 이상 필요"

# 3. 데이터베이스 버전 확인
# PostgreSQL: 12+
# MySQL: 8.0+
# MariaDB: 10.6+
# SQLite: 3.31.0+

3단계: 코드 수정

# deprecated 경고 확인
python -Wd manage.py check
python -Wd manage.py test

# 이메일 코드 검색 및 수정
grep -r "send_mail\|EmailMessage" . --include="*.py"
# Positional arguments를 keyword arguments로 변경

4단계: 마이그레이션 실행

# 마이그레이션 파일 생성
python manage.py makemigrations

# 마이그레이션 검토
python manage.py showmigrations

# 마이그레이션 실행 (백업 후!)
python manage.py migrate

5단계: 테스트

# 전체 테스트 실행
python manage.py test

# django-ninja API 테스트
python manage.py test myapp.tests.test_api

# 로컬 서버 실행 및 수동 테스트
python manage.py runserver

django-ninja 프로젝트 특화 체크리스트

API 엔드포인트 검증:

# tests.py
from django.test import TestCase
from ninja.testing import TestClient
from myapp.api import api

class APITestCase(TestCase):
    def setUp(self):
        self.client = TestClient(api)
    
    def test_pagination(self):
        """페이지네이션 동작 확인"""
        response = self.client.get("/products?page=1")
        self.assertEqual(response.status_code, 200)
        self.assertIn("results", response.json())
    
    def test_email_sending(self):
        """이메일 발송 API 테스트"""
        response = self.client.post(
            "/contact",
            json={"email": "test@example.com", "message": "Hello"}
        )
        self.assertEqual(response.status_code, 200)

성능 테스트:

import time

def test_async_performance(self):
    """AsyncPaginator 성능 확인"""
    start = time.time()
    response = self.client.get("/products?page=1&page_size=100")
    duration = time.time() - start
    
    self.assertLess(duration, 1.0)  # 1초 이내 응답

프로덕션 배포 전 체크리스트

  • Python 3.12+ 설치 확인
  • 모든 의존성 버전 업데이트
  • 데이터베이스 백업 완료
  • 로컬 환경에서 전체 테스트 통과
  • 스테이징 환경에서 검증
  • Email 코드 키워드 인수로 변경
  • CSP 설정 검토 (필요한 경우)
  • CI/CD 파이프라인 업데이트
  • 롤백 계획 준비

📄 Template Partials (템플릿 사용 시)

Django 6.0은 Template Partials를 도입하여 템플릿 재사용성을 크게 개선했습니다. django-ninja는 주로 JSON API를 제공하지만, 관리자 페이지나 이메일 템플릿을 사용하는 경우 유용합니다.

Template Partials란?

템플릿 내에서 재사용 가능한 작은 컴포넌트를 정의하고 사용할 수 있는 기능입니다.

기존 방식 (별도 파일 필요):

<!-- templates/user_card.html -->
<div class="user-card">
    <h3>{{ user.username }}</h3>
    <p>{{ user.email }}</p>
</div>

<!-- templates/user_list.html -->
{% raw %}
{% for user in users %}
    {% include "user_card.html" %}
{% endfor %}
{% endraw %}

새로운 방식 (같은 파일 내에서):

{% raw %}
<!-- templates/user_list.html -->
{% partialdef user_card %}
    <div class="user-card">
        <h3>{{ user.username }}</h3>
        <p>{{ user.email }}</p>
    </div>
{% endpartialdef %}

<!-- 사용 -->
{% for user in users %}
    {% partial user_card user=user %}
{% endfor %}
{% endraw %}

django-ninja에서 이메일 템플릿에 활용

이메일 발송 API와 Template Partials:

# api.py
from ninja import NinjaAPI
from django.template.loader import get_template
from django.core.mail import EmailMessage

api = NinjaAPI()


@api.post("/orders/{order_id}/confirm")
def send_order_confirmation(request, order_id: int):
    """주문 확인 이메일 발송"""
    order = Order.objects.get(id=order_id)
    
    # Partial을 포함한 템플릿 렌더링
    template = get_template('emails/order_confirmation.html')
    html_content = template.render({
        'order': order,
        'user': order.user,
        'items': order.items.all()
    })
    
    email = EmailMessage(
        subject=f"주문 확인 #{order.id}",
        body=html_content,
        from_email="orders@example.com",
        to=[order.user.email]
    )
    email.content_subtype = "html"
    email.send()
    
    return {"status": "email_sent", "order_id": order.id}

이메일 템플릿:

{% raw %}
<!-- templates/emails/order_confirmation.html -->
{% partialdef order_item %}
    <tr>
        <td>{{ item.product.name }}</td>
        <td>{{ item.quantity }}</td>
        <td>${{ item.price }}</td>
    </tr>
{% endpartialdef %}

<!DOCTYPE html>
<html>
<head>
    <title>주문 확인</title>
</head>
<body>
    <h1>주문이 확인되었습니다</h1>
    <p>안녕하세요, {{ user.username }}님</p>
    
    <table>
        <thead>
            <tr>
                <th>상품</th>
                <th>수량</th>
                <th>가격</th>
            </tr>
        </thead>
        <tbody>
            {% for item in items %}
                {% partial order_item item=item %}
            {% endfor %}
        </tbody>
    </table>
    
    <p>총 금액: ${{ order.total_amount }}</p>
</body>
</html>
{% endraw %}

관리자 커스터마이징에 활용

{% raw %}
<!-- templates/admin/custom_dashboard.html -->
{% extends "admin/base.html" %}

{% partialdef stat_card %}
    <div class="stat-card">
        <h3>{{ title }}</h3>
        <p class="stat-value">{{ value }}</p>
        <p class="stat-change">{{ change }}</p>
    </div>
{% endpartialdef %}

{% block content %}
    <div class="dashboard">
        {% partial stat_card title="총 사용자" value=user_count change="+12%" %}
        {% partial stat_card title="총 주문" value=order_count change="+8%" %}
        {% partial stat_card title="매출" value=revenue change="+15%" %}
    </div>
{% endblock %}
{% endraw %}

django-ninja 개발자를 위한 정리:

  • JSON API가 주요 사용 사례라면 Template Partials는 선택사항
  • 이메일 템플릿이나 관리 페이지를 많이 사용한다면 유용
  • HTMX나 Alpine.js와 함께 사용하면 더 강력

🛠️ 기타 유용한 개선사항

1. forloop.length 변수 추가

템플릿에서 반복문의 총 개수를 쉽게 확인할 수 있습니다.

{% raw %}
{% for item in items %}
    <p>{{ forloop.counter }} / {{ forloop.length }}</p>
{% endfor %}
{% endraw %}

2. Management Commands 개선

자동 import 기능:

python manage.py shell
# 이제 자동으로 import됨:
>>> settings  # django.conf.settings
>>> User  # django.contrib.auth.models.User (설치된 경우)

startproject/startapp 개선:

  • 존재하지 않는 디렉토리를 자동으로 생성

3. Admin 개선사항

Font Awesome 6.7.2 사용:

  • 더 다양한 아이콘 사용 가능
  • Admin 인터페이스 시각적 개선

메시지 레벨 아이콘 구분:

from django.contrib import messages

@api.post("/admin/action")
def admin_action(request):
    messages.debug(request, "디버그 메시지")  # 새로운 아이콘
    messages.info(request, "정보 메시지")    # 새로운 아이콘
    messages.success(request, "성공!")      # 기존과 다른 아이콘
    return {"status": "ok"}

ASGI 환경에서 HTTP/2의 multiple Cookie 헤더를 올바르게 처리합니다.

# django-ninja API with HTTP/2
@api.get("/profile")
async def get_profile(request):
    # HTTP/2에서 여러 Cookie 헤더가 자동으로 병합됨
    session_id = request.COOKIES.get('sessionid')
    csrf_token = request.COOKIES.get('csrftoken')
    
    return {
        "authenticated": bool(session_id),
        "csrf_protected": bool(csrf_token)
    }

📊 django-ninja 개발자를 위한 우선순위 정리

즉시 도입 가능한 기능

1. StringAgg (높은 우선순위)

  • N+1 쿼리 해결
  • API 응답 최적화
  • 모든 데이터베이스에서 사용 가능
# 즉시 적용 추천
products = Product.objects.annotate(
    tags=StringAgg('tags__name', delimiter=', ')
)

2. Model.NotUpdated 예외 (중간 우선순위)

  • 더 나은 에러 핸들링
  • 동시성 제어 개선
try:
    product.save(force_update=True)
except Product.NotUpdated:
    return {"error": "Conflict"}, 409

3. AsyncPaginator (선택적)

  • 높은 트래픽 API에 효과적
  • ASGI 환경 필요
  • 점진적 도입 가능

신중하게 도입할 기능

1. Background Tasks (실험적)

  • 프로덕션 사용 전 충분한 테스트 필요
  • Worker 관리 도구 미성숙
  • 소규모 프로젝트에서 먼저 시도

2. CSP (보안 강화 필요 시)

  • API 전용 서비스는 우선순위 낮음
  • Admin이나 문서 페이지 사용 시 고려
  • Report-Only 모드로 먼저 테스트

3. Template Partials (템플릿 사용 시만)

  • JSON API 중심이면 불필요
  • 이메일 템플릿 많으면 유용

🎯 결론: Django 6.0으로 업그레이드해야 할까?

django-ninja 개발자 관점 요약

✅ 업그레이드를 권장하는 경우:

  • 새 프로젝트 시작
  • Python 3.12+를 이미 사용 중
  • 데이터베이스 쿼리 최적화가 필요한 경우
  • 백그라운드 작업 처리가 필요한 경우
  • 보안 강화가 중요한 경우 (CSP)

⏸️ 업그레이드를 미룰 수 있는 경우:

  • 안정적으로 운영 중인 프로덕션 서비스
  • Python 3.11 이하를 사용 중이며 업그레이드가 어려운 경우
  • 레거시 의존성이 많은 경우
  • 팀의 테스트 리소스가 부족한 경우

⚠️ 주의사항:

  • Django 5.2는 2026년 4월까지 지원
  • 충분한 테스트 없이 프로덕션에 바로 적용하지 말 것
  • 스테이징 환경에서 먼저 검증
  • 데이터베이스 백업 필수

실전 로드맵

1단계: 학습 및 실험 (1-2주)

  • 로컬 환경에 Django 6.0 설치
  • 새로운 기능 테스트
  • 기존 코드와의 호환성 확인

2단계: 개발 환경 적용 (2-3주)

  • 개발 서버에 배포
  • 팀원들과 함께 테스트
  • 이슈 발견 및 해결

3단계: 스테이징 검증 (1-2주)

  • 프로덕션과 동일한 환경에서 테스트
  • 성능 벤치마크
  • 모니터링 설정

4단계: 프로덕션 배포 (계획에 따라)

  • 롤백 계획 준비
  • 점진적 배포 (Blue-Green, Canary 등)
  • 모니터링 강화

추천 학습 리소스

공식 문서:

django-ninja 관련:

Django 6.0은 많은 개선사항을 포함하고 있지만, django-ninja 개발자에게는 StringAgg, AsyncPaginator, Background Tasks가 가장 실용적인 기능입니다. 충분한 테스트와 계획을 통해 안전하게 마이그레이션하시기 바랍니다.


이 글이 도움이 되셨나요? Django 6.0 마이그레이션 경험이나 질문이 있으시면 댓글로 공유해주세요! 🚀

이 글을 공유해보세요!