Django Ninja 협업 가이드: 코드 가시성을 위한 실전 패턴
Django Ninja는 FastAPI의 장점을 Django에 결합한 현대적인 API 프레임워크입니다. 팀 프로젝트에서 Django Ninja를 활용할 때 코드의 가시성과 유지보수성을 높이는 실전 패턴들을 소개합니다.
Django Ninja는 FastAPI의 장점을 Django에 결합한 현대적인 API 프레임워크입니다. 팀 프로젝트에서 Django Ninja를 활용할 때 코드의 가시성과 유지보수성을 높이는 실전 패턴들을 소개합니다.
🎯 협업에서 중요한 코드 가시성
왜 코드 가시성이 중요한가?
팀 협업 시 발생하는 문제들
- API 스펙 불일치로 인한 프론트엔드-백엔드 소통 오류
- 복잡한 비즈니스 로직의 이해 부족
- 일관성 없는 코드 스타일로 인한 혼란
- 문서화 부족으로 인한 온보딩 어려움
Django Ninja가 제공하는 해결책
- 자동 문서 생성 (OpenAPI/Swagger)
- 타입 힌트 기반 검증
- 직관적인 코드 구조
- FastAPI와 유사한 선언적 문법
📝 1. Schema 설계로 명확한 데이터 구조 정의
입력/출력 Schema 분리
# schemas.py
from ninja import Schema
from typing import Optional
from datetime import datetime
class UserCreateSchema(Schema):
"""사용자 생성 요청 스키마"""
username: str
email: str
password: str
full_name: Optional[str] = None
class Config:
schema_extra = {
"example": {
"username": "john_doe",
"email": "john@example.com",
"password": "secure_password123",
"full_name": "John Doe"
}
}
class UserResponseSchema(Schema):
"""사용자 응답 스키마"""
id: int
username: str
email: str
full_name: Optional[str]
created_at: datetime
is_active: bool
class UserUpdateSchema(Schema):
"""사용자 수정 요청 스키마"""
email: Optional[str] = None
full_name: Optional[str] = None
is_active: Optional[bool] = None
중첩 Schema로 복잡한 데이터 표현
# schemas/product.py
from ninja import Schema
from typing import List, Optional
from decimal import Decimal
class CategorySchema(Schema):
id: int
name: str
slug: str
class ProductImageSchema(Schema):
id: int
url: str
alt_text: str
is_primary: bool
class ProductDetailSchema(Schema):
"""상품 상세 정보 스키마"""
id: int
name: str
description: str
price: Decimal
category: CategorySchema
images: List[ProductImageSchema]
tags: List[str]
stock_quantity: int
is_available: bool
created_at: datetime
@staticmethod
def resolve_tags(obj):
"""태그 리스트 변환 로직"""
return [tag.name for tag in obj.tags.all()]
🏗️ 2. Router 기반 모듈화 구조
도메인별 Router 분리
# api/users.py
from ninja import Router
from typing import List
from django.shortcuts import get_object_or_404
from django.contrib.auth.models import User
from .schemas import UserCreateSchema, UserResponseSchema, UserUpdateSchema
from .auth import AuthBearer
router = Router(tags=["Users"])
@router.get("/", response=List[UserResponseSchema])
def list_users(request):
"""
사용자 목록 조회
관리자만 접근 가능한 엔드포인트입니다.
페이지네이션은 추후 추가 예정입니다.
"""
users = User.objects.select_related().all()[:50]
return users
@router.post("/", response=UserResponseSchema)
def create_user(request, user_data: UserCreateSchema):
"""
새 사용자 생성
- 이메일 중복 검사 수행
- 패스워드 해싱 자동 처리
- 기본 권한 그룹 할당
"""
user = User.objects.create_user(
username=user_data.username,
email=user_data.email,
password=user_data.password,
first_name=user_data.full_name
)
return user
@router.get("/{user_id}", response=UserResponseSchema)
def get_user(request, user_id: int):
"""사용자 상세 정보 조회"""
user = get_object_or_404(User, id=user_id)
return user
@router.patch("/{user_id}", response=UserResponseSchema, auth=AuthBearer())
def update_user(request, user_id: int, user_data: UserUpdateSchema):
"""
사용자 정보 수정
인증된 사용자만 자신의 정보를 수정할 수 있습니다.
"""
user = get_object_or_404(User, id=user_id)
# 권한 검사
if request.auth.id != user_id and not request.auth.is_staff:
return {"error": "Permission denied"}, 403
# 부분 업데이트
for attr, value in user_data.dict(exclude_unset=True).items():
setattr(user, attr, value)
user.save()
return user
메인 API 라우터 구성
# api/__init__.py
from ninja import NinjaAPI
from .users import router as users_router
from .products import router as products_router
from .orders import router as orders_router
api = NinjaAPI(
title="E-Commerce API",
version="1.0.0",
description="팀 프로젝트용 전자상거래 API",
docs_url="/docs/"
)
# 라우터 등록
api.add_router("/users", users_router)
api.add_router("/products", products_router)
api.add_router("/orders", orders_router)
# URL 구성
# urls.py
from django.urls import path
from .api import api
urlpatterns = [
path("api/", api.urls),
]
🔐 3. 인증 및 권한 관리
토큰 기반 인증 구현
# auth.py
from ninja.security import HttpBearer
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
from rest_framework_simplejwt.tokens import RefreshToken
class AuthBearer(HttpBearer):
"""JWT 토큰 기반 인증"""
def authenticate(self, request, token):
try:
# JWT 토큰 검증 로직
user = self.get_user_from_token(token)
return user
except Exception:
return None
def get_user_from_token(self, token):
# 실제 JWT 검증 로직 구현
pass
# 로그인 엔드포인트
@router.post("/auth/login")
def login(request, credentials: LoginSchema):
"""
사용자 로그인
성공 시 access_token과 refresh_token 반환
"""
user = authenticate(
username=credentials.username,
password=credentials.password
)
if user:
refresh = RefreshToken.for_user(user)
return {
"access_token": str(refresh.access_token),
"refresh_token": str(refresh),
"user": UserResponseSchema.from_orm(user)
}
return {"error": "Invalid credentials"}, 401
📊 4. 에러 핸들링과 응답 표준화
일관된 에러 응답 구조
# exceptions.py
from ninja import Schema
class ErrorSchema(Schema):
"""표준 에러 응답 스키마"""
error: str
message: str
details: dict = None
class ValidationErrorSchema(Schema):
"""유효성 검사 에러 스키마"""
error: str = "validation_error"
field_errors: dict
# 전역 예외 핸들러
@api.exception_handler(ValidationError)
def validation_exception_handler(request, exc):
return api.create_response(
request,
{"error": "validation_error", "field_errors": exc.errors},
status=400
)
@api.exception_handler(PermissionError)
def permission_exception_handler(request, exc):
return api.create_response(
request,
{"error": "permission_denied", "message": str(exc)},
status=403
)
응답 래퍼 활용
# response_wrapper.py
from ninja import Schema
from typing import Generic, TypeVar, Optional
T = TypeVar('T')
class ApiResponse(Schema, Generic[T]):
"""표준 API 응답 래퍼"""
success: bool
data: Optional[T] = None
message: str = ""
pagination: Optional[dict] = None
# 사용 예시
@router.get("/", response=ApiResponse[List[UserResponseSchema]])
def list_users(request, page: int = 1, size: int = 20):
"""페이지네이션이 적용된 사용자 목록"""
users = User.objects.all()
paginated_users = paginate(users, page, size)
return ApiResponse(
success=True,
data=paginated_users.items,
pagination={
"current_page": page,
"total_pages": paginated_users.pages,
"total_items": paginated_users.total
}
)
🧪 5. 테스트 작성으로 코드 신뢰성 확보
API 엔드포인트 테스트
# tests/test_users_api.py
from django.test import TestCase
from django.contrib.auth.models import User
from ninja.testing import TestClient
from api import api
class UserAPITestCase(TestCase):
def setUp(self):
self.client = TestClient(api)
self.user = User.objects.create_user(
username="testuser",
email="test@example.com",
password="testpass123"
)
def test_create_user_success(self):
"""사용자 생성 성공 테스트"""
user_data = {
"username": "newuser",
"email": "new@example.com",
"password": "newpass123",
"full_name": "New User"
}
response = self.client.post("/users/", json=user_data)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["username"], "newuser")
self.assertTrue(User.objects.filter(username="newuser").exists())
def test_create_user_duplicate_username(self):
"""중복 사용자명 생성 실패 테스트"""
user_data = {
"username": "testuser", # 이미 존재하는 사용자명
"email": "another@example.com",
"password": "anotherpass123"
}
response = self.client.post("/users/", json=user_data)
self.assertEqual(response.status_code, 400)
self.assertIn("username", response.json()["field_errors"])
📚 6. 문서화와 주석 Best Practices
자동 문서 생성 활용
@router.get("/search", response=List[UserResponseSchema])
def search_users(
request,
q: str = Query(..., description="검색 키워드"),
active_only: bool = Query(True, description="활성 사용자만 조회"),
limit: int = Query(20, ge=1, le=100, description="결과 개수 제한")
):
"""
사용자 검색
**검색 기능:**
- 사용자명, 이메일, 전체 이름에서 검색
- 대소문자 구분 없음
- 부분 문자열 매칭
**필터 옵션:**
- active_only: 활성 사용자만 조회 (기본값: True)
- limit: 최대 결과 개수 (1-100, 기본값: 20)
**예시 요청:**
```
GET /users/search?q=john&active_only=true&limit=10
```
"""
queryset = User.objects.all()
if active_only:
queryset = queryset.filter(is_active=True)
queryset = queryset.filter(
Q(username__icontains=q) |
Q(email__icontains=q) |
Q(first_name__icontains=q) |
Q(last_name__icontains=q)
)
return queryset[:limit]
팀 컨벤션 문서화
# docs/api_conventions.md 참조용 코드
"""
API 개발 컨벤션
1. 네이밍 규칙:
- 스키마: PascalCase + 용도 suffix (CreateSchema, ResponseSchema)
- 함수: snake_case
- 경로: kebab-case
2. 응답 코드 규칙:
- 200: 성공
- 201: 생성 성공
- 400: 클라이언트 오류
- 401: 인증 필요
- 403: 권한 부족
- 404: 리소스 없음
- 500: 서버 오류
3. 에러 메시지:
- 영어로 작성
- 클라이언트가 이해할 수 있는 수준
- 보안 정보 노출 금지
"""
🚀 7. 성능 최적화와 모니터링
데이터베이스 쿼리 최적화
@router.get("/", response=List[UserWithProfileSchema])
def list_users_optimized(request):
"""
최적화된 사용자 목록 조회
N+1 쿼리 문제를 해결하기 위해 select_related 사용
"""
users = User.objects.select_related('profile').prefetch_related('groups')
return users
# 쿼리 분석 데코레이터
from django.db import connection
from functools import wraps
def query_debugger(func):
"""개발 환경에서 SQL 쿼리 분석"""
@wraps(func)
def wrapper(*args, **kwargs):
initial_queries = len(connection.queries)
result = func(*args, **kwargs)
final_queries = len(connection.queries)
if settings.DEBUG:
print(f"Function {func.__name__} executed {final_queries - initial_queries} queries")
return result
return wrapper
🎉 결론
Django Ninja를 활용한 협업에서 코드 가시성을 높이는 핵심 요소들:
✅ 핵심 포인트
- 타입 힌트 활용: 명확한 데이터 구조 정의
- 모듈화 구조: 도메인별 Router 분리
- 표준화된 응답: 일관된 API 스펙
- 자동 문서화: Swagger/OpenAPI 활용
- 테스트 커버리지: 신뢰할 수 있는 코드
- 성능 고려: 최적화된 쿼리 패턴
🛠️ 실무 적용 팁
- 점진적 도입: 기존 Django REST Framework에서 단계적 마이그레이션
- 팀 교육: Django Ninja의 FastAPI 스타일 익히기
- 컨벤션 정립: 팀만의 코딩 스타일 가이드 작성
- CI/CD 통합: 자동 테스트와 문서 배포
Django Ninja는 FastAPI의 편의성과 Django의 안정성을 결합하여, 팀 프로젝트에서 높은 생산성과 코드 품질을 동시에 달성할 수 있는 강력한 도구입니다. 이러한 패턴들을 적용하여 더 나은 협업 환경을 만들어보세요!