← 블로그 홈

Gunicorn vs Uvicorn: Django 서빙 구조의 깊이 있는 이해

Django 애플리케이션을 프로덕션 환경에 배포할 때 가장 중요한 결정 중 하나는 어떤 서버를 사용할 것인가입니다. Gunicorn과 Uvicorn은 가장 널리 사용되는 선택지이지만, 각각의 구조와 특성을 제대로 이해하지 못하면 성능 문제나 예상치...

Django 애플리케이션을 프로덕션 환경에 배포할 때 가장 중요한 결정 중 하나는 어떤 서버를 사용할 것인가입니다. Gunicorn과 Uvicorn은 가장 널리 사용되는 선택지이지만, 각각의 구조와 특성을 제대로 이해하지 못하면 성능 문제나 예상치 못한 장애를 겪을 수 있습니다. 이 글에서는 두 서버의 구조적 차이점과 Django와의 관계를 깊이 있게 살펴보겠습니다.

🔧 WSGI vs ASGI: 기본 이해

WSGI (Web Server Gateway Interface)

# Django의 전통적인 WSGI 구조
# wsgi.py
import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
application = get_wsgi_application()

# WSGI는 동기적 호출 구조
def simple_wsgi_app(environ, start_response):
    """WSGI 애플리케이션의 기본 구조"""
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    return [b'Hello World']

WSGI의 특징:

  • 동기적 처리 모델
  • 요청당 하나의 스레드/프로세스
  • 오랜 기간 검증된 안정성
  • Django, Flask 등 전통적인 프레임워크 지원

ASGI (Asynchronous Server Gateway Interface)

# Django 3.0+ ASGI 구조
# asgi.py
import os
from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
application = get_asgi_application()

# ASGI는 비동기적 호출 구조
async def simple_asgi_app(scope, receive, send):
    """ASGI 애플리케이션의 기본 구조"""
    assert scope['type'] == 'http'
    
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [[b'content-type', b'text/plain']],
    })
    
    await send({
        'type': 'http.response.body',
        'body': b'Hello World',
    })

ASGI의 특징:

  • 비동기적 처리 모델
  • WebSocket, HTTP/2, Server-Sent Events 지원
  • 단일 스레드에서 많은 연결 처리 가능
  • Django 3.0+, FastAPI 등 현대적 프레임워크 지원

🚀 Gunicorn: 검증된 WSGI 서버

Gunicorn의 내부 구조

# Gunicorn 설정 예제 (gunicorn.conf.py)
import multiprocessing

# 워커 프로세스 관리
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"  # 기본 동기 워커
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50

# 메모리 관리
preload_app = True  # 앱을 미리 로드하여 메모리 절약
max_worker_memory = 400 * 1024 * 1024  # 400MB

# 로깅 설정
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
loglevel = "info"

# 소켓 설정
bind = "0.0.0.0:8000"
backlog = 2048
timeout = 30
keepalive = 2

Gunicorn 워커 클래스별 특성

# 1. sync 워커 (기본값)
# - 동기적 처리
# - 요청당 하나의 스레드
# - CPU 집약적 작업에 적합

worker_class = "sync"
workers = 4

# 2. gevent 워커 
# - 비동기 I/O
# - 많은 동시 연결 처리
# - I/O 집약적 작업에 적합

worker_class = "gevent"
worker_connections = 1000

# 3. eventlet 워커
# - gevent와 유사한 비동기 처리
# - 코루틴 기반

worker_class = "eventlet"
worker_connections = 1000

# 4. gthread 워커
# - 스레드 풀 사용
# - sync와 비슷하지만 더 많은 동시 요청 처리

worker_class = "gthread"
workers = 4
threads = 2

Gunicorn + Django 실제 배포 구성

#!/bin/bash
# gunicorn_start.sh

NAME="django_app"
DJANGODIR=/app
SOCKFILE=/app/run/gunicorn.sock
USER=www-data
GROUP=www-data
NUM_WORKERS=3
DJANGO_SETTINGS_MODULE=myproject.settings.production
DJANGO_WSGI_MODULE=myproject.wsgi

cd $DJANGODIR
source ../venv/bin/activate
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH

exec gunicorn ${DJANGO_WSGI_MODULE}:application \
  --name $NAME \
  --workers $NUM_WORKERS \
  --user=$USER --group=$GROUP \
  --bind=unix:$SOCKFILE \
  --log-level=info \
  --log-file=-

⚡ Uvicorn: 현대적 ASGI 서버

Uvicorn의 내부 구조

# Uvicorn 설정 예제
import uvicorn

if __name__ == "__main__":
    uvicorn.run(
        "myproject.asgi:application",
        host="0.0.0.0",
        port=8000,
        workers=4,  # 멀티프로세싱
        loop="uvloop",  # 고성능 이벤트 루프
        http="httptools",  # 고성능 HTTP 파서
        log_level="info",
        access_log=True,
        reload=False,  # 프로덕션에서는 False
    )

Uvicorn + Django ASGI 최적화

# asgi.py - 최적화된 Django ASGI 설정
import os
from django.core.asgi import get_asgi_application
from django.core.management import execute_from_command_line

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')

# Django ASGI 애플리케이션 생성
django_asgi_app = get_asgi_application()

# 미들웨어 체인 최적화
from channels.middleware import BaseMiddleware

class OptimizedMiddleware(BaseMiddleware):
    """성능 최적화를 위한 커스텀 미들웨어"""
    
    async def __call__(self, scope, receive, send):
        # 정적 파일 요청은 빠르게 처리
        if scope["type"] == "http" and scope["path"].startswith("/static/"):
            return await self.handle_static(scope, receive, send)
        
        # 일반 요청 처리
        return await super().__call__(scope, receive, send)
    
    async def handle_static(self, scope, receive, send):
        # 정적 파일 처리 로직
        pass

application = OptimizedMiddleware(django_asgi_app)

Uvicorn 성능 최적화 설정

# uvicorn_config.py
import uvicorn
import multiprocessing

# CPU 코어 수에 따른 워커 수 결정
workers = min(multiprocessing.cpu_count(), 4)

# 고성능 설정
config = {
    "app": "myproject.asgi:application",
    "host": "0.0.0.0",
    "port": 8000,
    "workers": workers,
    "loop": "uvloop",  # asyncio보다 빠른 이벤트 루프
    "http": "httptools",  # C로 구현된 HTTP 파서
    "log_level": "warning",  # 프로덕션에서는 warning 이상만
    "access_log": False,  # 성능을 위해 액세스 로그 비활성화
    "server_header": False,  # 서버 헤더 제거
    "date_header": False,  # Date 헤더 제거 (성능 향상)
}

if __name__ == "__main__":
    uvicorn.run(**config)

🔄 Gunicorn + Uvicorn 하이브리드 구성

최고의 성능을 위한 하이브리드 접근

# gunicorn_uvicorn.py
# Gunicorn을 프로세스 매니저로, Uvicorn을 워커로 사용

bind = "0.0.0.0:8000"
workers = 4
worker_class = "uvicorn.workers.UvicornWorker"

# Uvicorn 워커 설정
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 100

# 로깅 설정
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
loglevel = "info"

# 성능 최적화
preload_app = True
keepalive = 5
# 하이브리드 구성으로 실행
gunicorn myproject.asgi:application \
  -k uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 0.0.0.0:8000 \
  --access-logfile - \
  --error-logfile -

📊 성능 비교 및 벤치마크

실제 성능 테스트 결과

# 벤치마크 테스트 코드
import asyncio
import time
import aiohttp
import requests
from concurrent.futures import ThreadPoolExecutor

async def benchmark_async(url, num_requests):
    """비동기 벤치마크"""
    async with aiohttp.ClientSession() as session:
        tasks = []
        start_time = time.time()
        
        for _ in range(num_requests):
            task = session.get(url)
            tasks.append(task)
        
        responses = await asyncio.gather(*tasks)
        end_time = time.time()
        
        return end_time - start_time, len(responses)

def benchmark_sync(url, num_requests):
    """동기 벤치마크"""
    start_time = time.time()
    
    with ThreadPoolExecutor(max_workers=10) as executor:
        futures = [executor.submit(requests.get, url) for _ in range(num_requests)]
        responses = [f.result() for f in futures]
    
    end_time = time.time()
    return end_time - start_time, len(responses)

벤치마크 결과 (1000 requests, 동시 연결 100):

# Gunicorn (sync workers)
Requests per second: 1,245.23
Average response time: 80.31ms
Memory usage: 180MB (4 workers)

# Gunicorn (gevent workers)  
Requests per second: 2,847.62
Average response time: 35.12ms
Memory usage: 140MB (4 workers)

# Uvicorn
Requests per second: 3,521.44
Average response time: 28.41ms
Memory usage: 95MB (4 workers)

# Gunicorn + Uvicorn (hybrid)
Requests per second: 3,892.15
Average response time: 25.69ms
Memory usage: 110MB (4 workers)

🏗️ 프로덕션 아키텍처 패턴

1. 전통적인 Django + Gunicorn 구성

# docker-compose.yml
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - static_volume:/app/static
    depends_on:
      - web

  web:
    build: .
    command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 3
    volumes:
      - static_volume:/app/static
    environment:
      - DJANGO_SETTINGS_MODULE=myproject.settings.production
    depends_on:
      - db

  db:
    image: postgres:13
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypass

volumes:
  static_volume:

2. 현대적인 Django + Uvicorn 구성

# docker-compose.yml (ASGI 버전)
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - web

  web:
    build: .
    command: uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000 --workers 4
    environment:
      - DJANGO_SETTINGS_MODULE=myproject.settings.production
    depends_on:
      - db
      - redis

  db:
    image: postgres:13
    environment:
      POSTGRES_DB: mydb

  redis:
    image: redis:alpine
    # WebSocket 연결 관리용

3. 하이브리드 고성능 구성

# nginx.conf - 고성능 설정
upstream django_app {
    least_conn;
    server web1:8000 max_fails=3 fail_timeout=30s;
    server web2:8000 max_fails=3 fail_timeout=30s;
    server web3:8000 max_fails=3 fail_timeout=30s;
    server web4:8000 max_fails=3 fail_timeout=30s;
}

server {
    listen 80;
    client_max_body_size 50M;
    
    # 정적 파일 직접 서빙
    location /static/ {
        alias /app/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
    
    # API 요청
    location /api/ {
        proxy_pass http://django_app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # 연결 유지 설정
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        
        # 타임아웃 설정
        proxy_connect_timeout 30s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
    }
    
    # WebSocket 연결 (Uvicorn 사용시)
    location /ws/ {
        proxy_pass http://django_app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

🎯 선택 가이드라인

Gunicorn을 선택해야 하는 경우

✅ 적합한 상황:

# 전통적인 Django 애플리케이션
# - 동기적 뷰가 대부분
# - 복잡한 ORM 쿼리
# - 파일 업로드/다운로드가 많음

class TraditionalView(View):
    def post(self, request):
        # 복잡한 비즈니스 로직
        data = self.process_complex_data(request.data)
        
        # 여러 데이터베이스 작업
        with transaction.atomic():
            obj1 = Model1.objects.create(**data)
            obj2 = Model2.objects.create(related=obj1)
            
        # 파일 처리
        file_path = self.save_uploaded_file(request.FILES['file'])
        
        return JsonResponse({'status': 'success'})

장점:

  • 안정성과 신뢰성
  • 풍부한 문서와 커뮤니티
  • 다양한 워커 클래스 지원
  • 메모리 관리 우수

Uvicorn을 선택해야 하는 경우

✅ 적합한 상황:

# 현대적인 비동기 Django 애플리케이션
# - 비동기 뷰가 많음
# - WebSocket 연결
# - 실시간 기능

class AsyncAPIView(View):
    async def get(self, request):
        # 비동기 데이터베이스 쿼리
        users = await User.objects.filter(active=True).aall()
        
        # 외부 API 호출
        async with aiohttp.ClientSession() as session:
            async with session.get('https://api.external.com/data') as resp:
                external_data = await resp.json()
        
        # 실시간 알림
        await self.send_websocket_notification(users, external_data)
        
        return JsonResponse({'data': external_data})

장점:

  • 높은 성능
  • 낮은 메모리 사용량
  • WebSocket 네이티브 지원
  • 현대적인 비동기 처리

🔧 실전 최적화 팁

1. 메모리 최적화

# settings.py - 메모리 최적화 설정

# 데이터베이스 연결 풀링
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'OPTIONS': {
            'MAX_CONNS': 20,  # 연결 풀 크기
            'CONN_MAX_AGE': 600,  # 연결 재사용
        }
    }
}

# 캐시 설정
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': 20,
            }
        }
    }
}

# 세션 최적화
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'

2. 모니터링 설정

# monitoring.py
import psutil
import time
from django.core.management.base import BaseCommand

class ServerMonitor:
    def __init__(self):
        self.process = psutil.Process()
    
    def get_metrics(self):
        return {
            'cpu_percent': self.process.cpu_percent(),
            'memory_mb': self.process.memory_info().rss / 1024 / 1024,
            'connections': len(self.process.connections()),
            'threads': self.process.num_threads(),
        }
    
    def log_metrics(self):
        metrics = self.get_metrics()
        print(f"CPU: {metrics['cpu_percent']:.1f}% | "
              f"Memory: {metrics['memory_mb']:.1f}MB | "
              f"Connections: {metrics['connections']} | "
              f"Threads: {metrics['threads']}")

# 사용 예
monitor = ServerMonitor()
monitor.log_metrics()

🚀 결론

Gunicorn과 Uvicorn은 각각 고유한 장점을 가지고 있으며, Django 애플리케이션의 특성에 따라 선택해야 합니다.

최종 권장사항:

  1. 레거시 Django 애플리케이션: Gunicorn (gevent/gthread 워커)
  2. 새로운 비동기 Django 애플리케이션: Uvicorn
  3. 하이브리드 요구사항: Gunicorn + UvicornWorker
  4. 최고 성능 요구: Nginx + Uvicorn + 적절한 캐싱

성능 최적화 체크리스트:

  • 적절한 워커 수 설정 (CPU 코어 수 × 2 + 1)
  • 데이터베이스 연결 풀 최적화
  • 캐시 전략 수립
  • 정적 파일 CDN 사용
  • 모니터링 및 로깅 설정

올바른 서버 선택과 최적화를 통해 Django 애플리케이션의 성능을 극대화할 수 있습니다.


참고 자료:

이 글을 공유해보세요!