Django 6.0 주요 기능 리뷰: django-ninja 개발자가 알아야 할 것들
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 # 이미지 처리 시
업그레이드 체크리스트:
- Python 3.12+ 설치 및 가상환경 재생성
- 모든 의존성 라이브러리 버전 확인 및 업데이트
- 테스트 실행으로 호환성 검증
- 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 적용 가이드
단계별 적용:
- Report-Only 모드로 시작
SECURE_CSP_REPORT_ONLY = {...} - 위반 사항 모니터링
- 로그를 확인하고 어떤 리소스가 차단되는지 파악
- 정책 조정
- 필요한 도메인을 허용 목록에 추가
- 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)
성능 최적화 팁:
StringAgg로 관계형 데이터를 한 번에 조회select_related/prefetch_related대신annotate사용 고려values()로 필요한 필드만 선택- API 응답 크기 감소
⚡ Async 지원 개선: AsyncPaginator
AsyncPaginator와 AsyncPage
Django 6.0은 비동기 환경에서 페이지네이션을 지원하는 AsyncPaginator와 AsyncPage를 추가했습니다. 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배 빠름)
주의사항
- 데이터베이스 연결: 비동기 페이지네이션은 비동기 DB 드라이버가 필요합니다
# settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', # PostgreSQL의 경우 psycopg 3.x 사용 } } - ORM 제약사항: 모든 ORM 기능이 비동기로 지원되는 것은 아닙니다
select_related(),prefetch_related()는 비동기 지원- 일부 복잡한 집계는 동기로 처리될 수 있음
- 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"}
4. Multiple Cookie Headers 지원 (HTTP/2)
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 마이그레이션 경험이나 질문이 있으시면 댓글로 공유해주세요! 🚀