← 블로그 홈

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를 선택해야 하는 경우

✅ 적합한 상황

  1. 고성능 API가 필요한 서비스
    • 높은 처리량이 요구되는 마이크로서비스
    • 실시간 데이터 처리 API
    • IoT 디바이스 통신 API
  2. 현대적인 개발 환경
    • 타입 힌트를 적극 활용하는 팀
    • 자동 API 문서화가 중요한 프로젝트
    • 비동기 프로그래밍에 익숙한 개발자
  3. 새로운 프로젝트
    • 레거시 코드 제약이 없는 경우
    • 클라우드 네이티브 아키텍처
    • 컨테이너 기반 배포

🔧 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를 선택해야 하는 경우

✅ 적합한 상황

  1. 기존 Django 프로젝트
    • 이미 Django 기반 시스템
    • Django ORM을 활용한 복잡한 데이터 모델
    • Django 생태계 의존성
  2. 빠른 개발이 중요한 경우
    • Django Admin 활용 필요
    • 인증/권한 시스템이 복잡한 경우
    • 풍부한 서드파티 패키지 활용
  3. 대규모 팀 개발
    • 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. 점진적 마이그레이션 진행

최종 권장사항

  1. 새 프로젝트: FastAPI로 시작하여 필요시 Django 도입
  2. 기존 Django: 핵심 API만 FastAPI로 분리
  3. 레거시 시스템: DRF 최적화 후 단계별 마이그레이션

FastAPI의 성능 우위는 명확하지만, 프로젝트의 요구사항과 팀의 역량을 종합적으로 고려하여 선택하는 것이 중요합니다.

다음 글에서는 FastAPI와 DRF의 구체적인 마이그레이션 가이드를 다루겠습니다. 🚀

이 글을 공유해보세요!