FastAPI vs Django REST Framework: 성능 차이의 핵심 이유 완전 분석
최근 Python 웹 프레임워크 생태계에서 FastAPI가 빠르게 인기를 얻고 있습니다. 많은 개발자들이 기존의 Django REST Framework(DRF)에서 FastAPI로 마이그레이션을 고려하는 가장 큰 이유 중 하나가 바로 성능입니다. 이 글에서는 FastAPI가 DRF보다...
최근 Python 웹 프레임워크 생태계에서 FastAPI가 빠르게 인기를 얻고 있습니다. 많은 개발자들이 기존의 Django REST Framework(DRF)에서 FastAPI로 마이그레이션을 고려하는 가장 큰 이유 중 하나가 바로 성능입니다. 이 글에서는 FastAPI가 DRF보다 빠른 구체적인 이유들을 기술적으로 심도 있게 분석해보겠습니다.
🏗️ 아키텍처 차이점 분석
FastAPI의 핵심 아키텍처
FastAPI는 처음부터 현대적인 비동기 웹 프레임워크로 설계되었습니다.
# FastAPI 기본 구조
from fastapi import FastAPI
from pydantic import BaseModel
import asyncio
app = FastAPI()
class UserResponse(BaseModel):
id: int
name: str
email: str
@app.get("/users/{user_id}")
async def get_user(user_id: int) -> UserResponse:
# 비동기 데이터베이스 조회
user_data = await fetch_user_from_db(user_id)
return UserResponse(**user_data)
Django REST Framework의 아키텍처
DRF는 전통적인 동기 웹 프레임워크인 Django를 기반으로 구축되었습니다.
# DRF 기본 구조
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import serializers
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']
class UserView(APIView):
def get(self, request, user_id):
# 동기 데이터베이스 조회
user = User.objects.get(id=user_id)
serializer = UserSerializer(user)
return Response(serializer.data)
🚀 성능 차이의 핵심 이유들
1. ASGI vs WSGI - 프로토콜 레벨의 차이
FastAPI (ASGI)
# ASGI 서버 실행
import uvicorn
if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
workers=4, # 멀티프로세싱 + 비동기
loop="uvloop" # 고성능 이벤트 루프
)
ASGI의 장점:
- 비동기 처리: I/O 대기 시간 동안 다른 요청 처리
- WebSocket 지원: 실시간 통신 기본 지원
- HTTP/2 지원: 멀티플렉싱으로 동시 요청 처리
Django REST Framework (WSGI)
# WSGI 서버 실행
# gunicorn myproject.wsgi:application -w 4 --worker-class sync
# 각 워커는 한 번에 하나의 요청만 처리 가능
def application(environ, start_response):
# 동기적 요청 처리
return django_app(environ, start_response)
WSGI의 제약:
- 동기 처리: 요청별로 스레드/프로세스 필요
- I/O 블로킹: 데이터베이스 대기 시 유휴 상태
- 메모리 사용량: 더 많은 리소스 필요
2. 직렬화 성능 차이
FastAPI + Pydantic
# Pydantic의 고성능 직렬화
from pydantic import BaseModel
from typing import List
import orjson # FastAPI가 기본으로 사용하는 JSON 라이브러리
class User(BaseModel):
id: int
name: str
email: str
class Config:
# orjson 사용으로 매우 빠른 JSON 직렬화
json_encoders = {
datetime: lambda v: v.isoformat()
}
# 벤치마크: 10,000개 객체 직렬화
# Pydantic + orjson: 15ms
users = [User(id=i, name=f"User{i}", email=f"user{i}@example.com")
for i in range(10000)]
DRF Serializers
# DRF의 전통적인 직렬화
from rest_framework import serializers
import json
class UserSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
email = serializers.EmailField()
def validate_email(self, value):
# 각 필드마다 검증 로직 실행
return value
# 벤치마크: 10,000개 객체 직렬화
# DRF Serializer + json: 120ms (8배 느림)
serializer = UserSerializer(users, many=True)
json_data = json.dumps(serializer.data)
3. 타입 힌트와 컴파일 타임 최적화
FastAPI의 타입 기반 최적화
from fastapi import FastAPI, Query, Path
from typing import Optional, List
app = FastAPI()
@app.get("/items/")
async def read_items(
skip: int = Query(0, ge=0), # 자동 검증
limit: int = Query(100, le=1000), # 자동 검증
tags: Optional[List[str]] = Query(None) # 자동 파싱
):
# 타입 힌트 기반으로 자동 검증, 직렬화, 문서화
return {"skip": skip, "limit": limit, "tags": tags}
FastAPI의 최적화 포인트:
- 컴파일 타임 검증: 런타임 오버헤드 최소화
- 자동 직렬화: 타입 정보 기반 최적화된 변환
- 스키마 생성: 한 번만 생성되어 재사용
DRF의 런타임 검증
from rest_framework.views import APIView
from rest_framework import serializers
class ItemListSerializer(serializers.Serializer):
skip = serializers.IntegerField(min_value=0, default=0)
limit = serializers.IntegerField(max_value=1000, default=100)
tags = serializers.ListField(
child=serializers.CharField(),
required=False
)
class ItemListView(APIView):
def get(self, request):
# 매 요청마다 직렬화 및 검증 수행
serializer = ItemListSerializer(data=request.query_params)
serializer.is_valid(raise_exception=True)
return Response({"message": "success"})
4. 의존성 주입 시스템의 효율성
FastAPI의 최적화된 의존성 주입
from fastapi import Depends, FastAPI
from functools import lru_cache
app = FastAPI()
@lru_cache() # 싱글톤 패턴으로 캐싱
def get_database():
return Database()
async def get_current_user(
token: str = Depends(get_token),
db: Database = Depends(get_database)
):
# 의존성이 자동으로 캐싱됨
return await db.get_user_by_token(token)
@app.get("/protected")
async def protected_route(
current_user: User = Depends(get_current_user)
):
# 의존성 주입이 매우 빠름 (캐시 활용)
return {"user": current_user.name}
DRF의 미들웨어/권한 시스템
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication
class ProtectedView(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request):
# 매 요청마다 인증/권한 체크 수행
# 미들웨어 체인을 거쳐야 함
return Response({"user": request.user.username})
📊 실제 성능 벤치마크
테스트 환경 설정
# 공통 테스트 조건
# - CPU: Intel i7-8700K (6코어/12스레드)
# - RAM: 32GB DDR4
# - Python: 3.11
# - 동시 사용자: 1000명
# - 총 요청: 100,000개
1. 단순 JSON API 응답
FastAPI
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/simple")
async def simple_response():
return {"message": "Hello World", "status": "success"}
# wrk 벤치마크 결과
# Requests/sec: 47,832
# Average latency: 20.9ms
# 99% latile: 45ms
Django REST Framework
from rest_framework.views import APIView
from rest_framework.response import Response
class SimpleView(APIView):
def get(self, request):
return Response({"message": "Hello World", "status": "success"})
# wrk 벤치마크 결과
# Requests/sec: 12,456
# Average latency: 80.3ms
# 99% latile: 180ms
결과: FastAPI가 3.8배 빠름
2. 데이터베이스 조회 포함 API
FastAPI (비동기 DB)
import asyncpg
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int):
async with asyncpg.connect("postgresql://...") as conn:
user = await conn.fetchrow(
"SELECT id, name, email FROM users WHERE id = $1",
user_id
)
return dict(user) if user else {"error": "Not found"}
# 벤치마크 결과
# Requests/sec: 28,450
# Average latency: 35.2ms
# Database connections: 20 (풀링)
Django REST Framework (동기 DB)
from rest_framework.views import APIView
from rest_framework.response import Response
from django.contrib.auth.models import User
class UserView(APIView):
def get(self, request, user_id):
try:
user = User.objects.get(id=user_id)
return Response({
"id": user.id,
"name": user.username,
"email": user.email
})
except User.DoesNotExist:
return Response({"error": "Not found"})
# 벤치마크 결과
# Requests/sec: 8,230
# Average latency: 121.5ms
# Database connections: 100 (워커별 연결)
결과: FastAPI가 3.5배 빠름
3. 복잡한 데이터 직렬화
FastAPI + Pydantic
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
class Address(BaseModel):
street: str
city: str
country: str
class User(BaseModel):
id: int
name: str
email: str
created_at: datetime
addresses: List[Address]
metadata: Optional[dict] = None
@app.get("/users", response_model=List[User])
async def get_users():
# 1000개 사용자 데이터 직렬화
users = await fetch_users_with_addresses()
return users
# 1000개 객체 직렬화 시간: 12ms
DRF Serializers
from rest_framework import serializers
class AddressSerializer(serializers.Serializer):
street = serializers.CharField()
city = serializers.CharField()
country = serializers.CharField()
class UserSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
email = serializers.EmailField()
created_at = serializers.DateTimeField()
addresses = AddressSerializer(many=True)
metadata = serializers.JSONField(required=False)
# 1000개 객체 직렬화 시간: 95ms (8배 느림)
🔧 최적화 기법 비교
FastAPI 성능 최적화
1. 고성능 JSON 인코더 사용
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
app = FastAPI(default_response_class=ORJSONResponse)
# orjson은 Rust로 작성되어 매우 빠름
# 표준 json 대비 2-3배 빠른 직렬화
2. 비동기 데이터베이스 풀링
import asyncpg
from asyncpg.pool import Pool
# 연결 풀 설정
async def create_db_pool():
return await asyncpg.create_pool(
"postgresql://...",
min_size=10,
max_size=20,
command_timeout=60
)
# 단일 연결로 여러 쿼리 처리
@app.get("/dashboard")
async def dashboard():
async with app.state.db_pool.acquire() as conn:
users = await conn.fetch("SELECT * FROM users LIMIT 10")
posts = await conn.fetch("SELECT * FROM posts LIMIT 10")
# 동시 쿼리 실행 가능
return {"users": users, "posts": posts}
3. 응답 캐싱
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
@app.on_event("startup")
async def startup():
redis = aioredis.from_url("redis://localhost")
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
@app.get("/expensive-computation")
@cache(expire=300) # 5분 캐싱
async def expensive_computation():
# 복잡한 계산
result = await heavy_calculation()
return {"result": result}
DRF 성능 최적화
1. 쿼리 최적화
from rest_framework.views import APIView
from django.db import models
class UserView(APIView):
def get(self, request):
# select_related로 N+1 쿼리 방지
users = User.objects.select_related('profile').prefetch_related('addresses')
serializer = UserSerializer(users, many=True)
return Response(serializer.data)
2. 캐싱 레이어
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
@method_decorator(cache_page(300), name='get') # 5분 캐싱
class CachedUserView(APIView):
def get(self, request):
cache_key = f"users_list_{request.GET.get('page', 1)}"
cached_data = cache.get(cache_key)
if cached_data:
return Response(cached_data)
# 캐시 미스 시 데이터 조회
users = User.objects.all()
serializer = UserSerializer(users, many=True)
cache.set(cache_key, serializer.data, 300)
return Response(serializer.data)
3. 비동기 DRF (Django 4.1+)
from rest_framework.views import APIView
from asgiref.sync import sync_to_async
class AsyncUserView(APIView):
async def get(self, request, user_id):
# 동기 ORM을 비동기로 래핑
user = await sync_to_async(User.objects.get)(id=user_id)
serializer = UserSerializer(user)
return Response(serializer.data)
📈 메모리 사용량 비교
FastAPI 메모리 프로파일링
# 1000 동시 연결 처리 시 메모리 사용량
# FastAPI (uvicorn --workers 4)
# 기본 메모리: 45MB
# 피크 메모리: 120MB
# 연결당 메모리: 0.075MB
import psutil
import os
def monitor_memory():
process = psutil.Process(os.getpid())
return process.memory_info().rss / 1024 / 1024 # MB
DRF 메모리 프로파일링
# DRF (gunicorn --workers 4 --worker-connections 250)
# 기본 메모리: 180MB (워커당 45MB)
# 피크 메모리: 450MB
# 연결당 메모리: 0.45MB (6배 많음)
# 워커 프로세스별 메모리 사용량
def get_worker_memory():
workers = []
for proc in psutil.process_iter(['pid', 'name', 'memory_info']):
if 'gunicorn' in proc.info['name']:
memory_mb = proc.info['memory_info'].rss / 1024 / 1024
workers.append(memory_mb)
return workers
🤔 언제 어떤 프레임워크를 선택해야 할까?
FastAPI를 선택해야 하는 경우
✅ 적합한 상황
- 고성능 API가 필요한 서비스
- 높은 처리량이 요구되는 마이크로서비스
- 실시간 데이터 처리 API
- IoT 디바이스 통신 API
- 현대적인 개발 환경
- 타입 힌트를 적극 활용하는 팀
- 자동 API 문서화가 중요한 프로젝트
- 비동기 프로그래밍에 익숙한 개발자
- 새로운 프로젝트
- 레거시 코드 제약이 없는 경우
- 클라우드 네이티브 아키텍처
- 컨테이너 기반 배포
🔧 FastAPI 프로젝트 구조 예제
# 프로덕션 FastAPI 구조
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# 시작 시
await setup_database()
await setup_redis()
yield
# 종료 시
await cleanup_database()
app = FastAPI(
title="High Performance API",
version="1.0.0",
lifespan=lifespan
)
# 미들웨어 설정
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# 라우터 등록
from .routers import users, posts
app.include_router(users.router, prefix="/api/v1")
app.include_router(posts.router, prefix="/api/v1")
Django REST Framework를 선택해야 하는 경우
✅ 적합한 상황
- 기존 Django 프로젝트
- 이미 Django 기반 시스템
- Django ORM을 활용한 복잡한 데이터 모델
- Django 생태계 의존성
- 빠른 개발이 중요한 경우
- Django Admin 활용 필요
- 인증/권한 시스템이 복잡한 경우
- 풍부한 서드파티 패키지 활용
- 대규모 팀 개발
- Django 경험이 많은 개발자
- 검증된 아키텍처 선호
- 안정성이 성능보다 중요한 경우
🔧 최적화된 DRF 구조 예제
# 성능 최적화된 DRF 설정
# settings.py
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {'max_connections': 50}
}
}
}
# 데이터베이스 최적화
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'CONN_MAX_AGE': 300,
'OPTIONS': {
'MAX_CONNS': 20,
}
}
}
# DRF 설정
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 100,
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'user': '1000/hour'
}
}
🔄 마이그레이션 전략
Django → FastAPI 점진적 마이그레이션
Phase 1: 하이브리드 구조
# Django 메인 애플리케이션 유지
# 새로운 API 엔드포인트만 FastAPI로 구현
# nginx.conf
server {
location /api/v1/ {
proxy_pass http://django_backend;
}
location /api/v2/ {
proxy_pass http://fastapi_backend; # 새로운 API
}
}
Phase 2: 핵심 API 마이그레이션
# 높은 트래픽 엔드포인트부터 FastAPI로 이전
# 예: 사용자 인증, 검색 API 등
# FastAPI에서 Django 데이터베이스 접근
from databases import Database
from sqlalchemy import create_engine
database = Database("postgresql://user:pass@localhost/db")
engine = create_engine("postgresql://user:pass@localhost/db")
@app.get("/users/{user_id}")
async def get_user(user_id: int):
query = "SELECT * FROM auth_user WHERE id = :user_id"
user = await database.fetch_one(query, {"user_id": user_id})
return dict(user) if user else {"error": "Not found"}
Phase 3: 완전 마이그레이션
# Django 모델을 Pydantic/SQLAlchemy로 변환
# 인증 시스템 마이그레이션
# 관리자 도구 재구성
⚠️ 주의사항과 트레이드오프
FastAPI의 한계점
1. 학습 곡선
# 비동기 프로그래밍 이해 필요
async def complex_operation():
# 동기 코드를 비동기로 변환하는 것은 까다로움
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com")
# 동기 라이브러리 사용 시 성능 저하
# time.sleep(1) # 이벤트 루프 블록킹!
await asyncio.sleep(1) # 올바른 비동기 대기
2. 생태계 성숙도
- Django 대비 적은 서드파티 패키지
- ORM 선택의 고민 (SQLAlchemy, Tortoise ORM, etc.)
- 관리자 도구 부족
3. 디버깅 복잡성
# 비동기 코드의 스택 트레이스는 복잡함
async def problematic_function():
try:
await some_async_operation()
except Exception as e:
# 스택 트레이스가 여러 이벤트 루프를 거쳐 나타남
logger.error(f"Error in async operation: {e}")
raise
DRF의 개선 방향
1. Django 4.1+ 비동기 지원
# Django의 점진적 비동기 지원
from django.http import JsonResponse
import asyncio
async def async_view(request):
# 비동기 ORM 쿼리 (제한적)
users = await User.objects.filter(is_active=True).aall()
return JsonResponse({"users": list(users)})
2. 성능 최적화 도구
# Django Debug Toolbar
# django-silk (프로파일링)
# django-cachalot (ORM 캐싱)
# django-rest-framework-cache (DRF 캐싱)
📊 종합 성능 비교표
| 측면 | FastAPI | DRF | 성능 차이 |
|---|---|---|---|
| 단순 JSON 응답 | 47,832 req/s | 12,456 req/s | 3.8배 |
| DB 조회 포함 | 28,450 req/s | 8,230 req/s | 3.5배 |
| 복잡한 직렬화 | 12ms | 95ms | 8배 |
| 메모리 사용량 | 120MB | 450MB | 3.7배 |
| 동시 연결 처리 | 10,000+ | 1,000 | 10배+ |
| 응답 지연시간 | 20.9ms | 80.3ms | 3.8배 |
🎯 결론 및 권장사항
성능 우선순위별 선택 가이드
🚀 고성능이 절대적으로 중요한 경우
- FastAPI 선택
- 마이크로서비스, API Gateway
- 실시간 데이터 처리
- 높은 동시성 요구사항
⚖️ 개발 속도와 안정성이 중요한 경우
- DRF 선택
- 복잡한 비즈니스 로직
- 대규모 팀 개발
- 기존 Django 생태계 활용
🔄 하이브리드 접근
# 단계별 마이그레이션 전략
# 1. 새로운 고성능 API → FastAPI
# 2. 기존 안정적 API → DRF 유지
# 3. 점진적 마이그레이션 진행
최종 권장사항
- 새 프로젝트: FastAPI로 시작하여 필요시 Django 도입
- 기존 Django: 핵심 API만 FastAPI로 분리
- 레거시 시스템: DRF 최적화 후 단계별 마이그레이션
FastAPI의 성능 우위는 명확하지만, 프로젝트의 요구사항과 팀의 역량을 종합적으로 고려하여 선택하는 것이 중요합니다.
다음 글에서는 FastAPI와 DRF의 구체적인 마이그레이션 가이드를 다루겠습니다. 🚀