← 블로그 홈

Django 대용량 트래픽 처리 완전 가이드: 초당 10만 요청을 감당하는 아키텍처

Django로 구축한 서비스가 성장하면서 대용량 트래픽을 처리해야 하는 상황에 직면하게 됩니다. 초당 수천, 수만 건의 요청을 안정적으로 처리하면서도 빠른 응답 시간을 유지하는 것은 쉽지 않은 도전입니다. 이 글에서는 Django 애플리케이션을...

Django로 구축한 서비스가 성장하면서 대용량 트래픽을 처리해야 하는 상황에 직면하게 됩니다. 초당 수천, 수만 건의 요청을 안정적으로 처리하면서도 빠른 응답 시간을 유지하는 것은 쉽지 않은 도전입니다. 이 글에서는 Django 애플리케이션을 대용량 트래픽에 최적화하는 종합적인 전략을 실전 중심으로 다루겠습니다.

🎯 대용량 트래픽 처리의 핵심 원칙

성능 목표 설정

실제 대용량 서비스에서 목표로 하는 성능 지표들을 살펴보겠습니다.

# 성능 목표 예시
PERFORMANCE_TARGETS = {
    '응답_시간': {
        'P95': '200ms',  # 95%의 요청이 200ms 이내 응답
        'P99': '500ms',  # 99%의 요청이 500ms 이내 응답
        '평균': '100ms'   # 평균 응답 시간
    },
    '처리량': {
        '초당_요청수': 50000,  # 50K RPS
        '일일_요청수': 4_000_000_000,  # 40억 요청/일
        '동시_사용자': 100000  # 10만 동시 사용자
    },
    '가용성': {
        'SLA': '99.9%',  # 연간 8.76시간 다운타임
        'MTTR': '5분',   # 평균 복구 시간
        'MTBF': '30일'   # 평균 장애 간격
    },
    '리소스': {
        'CPU_사용률': '70%',  # 평균 CPU 사용률
        '메모리_사용률': '80%',  # 평균 메모리 사용률
        'DB_커넥션풀': '80%'   # DB 연결 사용률
    }
}

확장성 패턴 이해

# 확장성의 두 가지 방향
SCALABILITY_PATTERNS = {
    '수직적_확장': {
        '설명': 'Scale Up - 서버 사양 업그레이드',
        '장점': ['구현 단순', '데이터 일관성'],
        '단점': ['비용 증가', '물리적 한계'],
        '적용_예': 'CPU/메모리 증설, SSD 업그레이드'
    },
    '수평적_확장': {
        '설명': 'Scale Out - 서버 대수 증가',
        '장점': ['무제한 확장', '장애 격리'],
        '단점': ['복잡성 증가', '데이터 동기화'],
        '적용_예': '로드 밸런서, 마이크로서비스'
    }
}

🏗️ 아키텍처 최적화 전략

1. ASGI vs WSGI: 비동기 처리의 위력

# WSGI 기반 전통적 구조
# gunicorn settings.py
bind = "0.0.0.0:8000"
workers = 16  # CPU 코어 수 * 2
worker_class = "sync"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 100
keepalive = 2
timeout = 30

# 동시 처리 가능 요청: 16 * 1 = 16개 (블로킹 I/O)

# ASGI 기반 비동기 구조
# uvicorn settings
import uvicorn

if __name__ == "__main__":
    uvicorn.run(
        "myproject.asgi:application",
        host="0.0.0.0",
        port=8000,
        workers=4,  # CPU 코어 수
        loop="uvloop",  # 고성능 이벤트 루프
        http="httptools",  # 고성능 HTTP 파서
        access_log=False,  # 운영환경에서는 비활성화
        server_header=False
    )

# 동시 처리 가능 요청: 수천 개 (비블로킹 I/O)

2. Django 설정 최적화

# settings/production.py
import os
from .base import *

# 보안 설정
DEBUG = False
ALLOWED_HOSTS = ['*.yourdomain.com', 'yourdomain.com']
SECRET_KEY = os.environ['SECRET_KEY']

# 데이터베이스 최적화
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ['DB_NAME'],
        'USER': os.environ['DB_USER'],
        'PASSWORD': os.environ['DB_PASSWORD'],
        'HOST': os.environ['DB_HOST'],
        'PORT': os.environ['DB_PORT'],
        'CONN_MAX_AGE': 600,  # 연결 재사용
        'OPTIONS': {
            'MAX_CONNS': 20,  # 최대 연결 수
            'connect_timeout': 10,
            'options': '-c default_transaction_isolation=read_committed'
        }
    },
    # 읽기 전용 복제본
    'read_replica': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ['DB_NAME'],
        'USER': os.environ['DB_READONLY_USER'],
        'PASSWORD': os.environ['DB_READONLY_PASSWORD'],
        'HOST': os.environ['DB_READONLY_HOST'],
        'PORT': os.environ['DB_PORT'],
        'CONN_MAX_AGE': 600,
        'OPTIONS': {
            'MAX_CONNS': 30,
            'connect_timeout': 10,
        }
    }
}

# 데이터베이스 라우팅
DATABASE_ROUTERS = ['myproject.routers.DatabaseRouter']

# 캐시 설정
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': f"redis://{os.environ['REDIS_HOST']}:6379/0",
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            'CONNECTION_POOL_KWARGS': {
                'max_connections': 50,
                'retry_on_timeout': True,
            },
            'SERIALIZER': 'django_redis.serializers.json.JSONSerializer',
            'COMPRESSOR': 'django_redis.compressors.zlib.ZlibCompressor',
        },
        'TIMEOUT': 300,
        'KEY_PREFIX': 'myproject',
        'VERSION': 1,
    },
    'sessions': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': f"redis://{os.environ['REDIS_HOST']}:6379/1",
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            'CONNECTION_POOL_KWARGS': {'max_connections': 30},
        },
        'TIMEOUT': 86400,  # 24시간
    }
}

# 세션 최적화
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'sessions'
SESSION_COOKIE_AGE = 86400  # 24시간
SESSION_SAVE_EVERY_REQUEST = False  # 성능 최적화

# 정적 파일 최적화
STATIC_URL = f"https://{os.environ['CDN_DOMAIN']}/static/"
MEDIA_URL = f"https://{os.environ['CDN_DOMAIN']}/media/"

# 압축 및 최적화
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

🗄️ 데이터베이스 최적화 전략

데이터베이스는 대용량 트래픽 처리에서 가장 중요한 병목점 중 하나입니다. 효과적인 데이터베이스 최적화를 통해 성능을 극적으로 향상시킬 수 있습니다.

1. 데이터베이스 라우팅과 읽기 복제본 활용

# myproject/routers.py
class DatabaseRouter:
    """
    읽기/쓰기 분산을 위한 데이터베이스 라우터
    읽기 작업은 복제본으로, 쓰기 작업은 마스터로 라우팅
    """
    
    READ_ONLY_MODELS = {
        'analytics', 'reports', 'logs'  # 읽기 전용 모델들
    }
    
    def db_for_read(self, model, **hints):
        """읽기 작업 라우팅"""
        
        # 특정 모델은 항상 읽기 전용 DB 사용
        if model._meta.app_label in self.READ_ONLY_MODELS:
            return 'read_replica'
        
        # 현재 스레드가 트랜잭션 중이면 마스터 DB 사용
        from django.db import transaction
        if transaction.get_connection().in_atomic_block:
            return 'default'
        
        # 일반적인 읽기 작업은 복제본으로
        return 'read_replica'
    
    def db_for_write(self, model, **hints):
        """쓰기 작업은 항상 마스터 DB"""
        return 'default'
    
    def allow_relation(self, obj1, obj2, **hints):
        """관계 허용 여부"""
        db_set = {'default', 'read_replica'}
        if obj1._state.db in db_set and obj2._state.db in db_set:
            return True
        return None
    
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """마이그레이션 허용 여부"""
        return db == 'default'

# 수동 데이터베이스 선택을 위한 매니저
class ReadOnlyManager(models.Manager):
    """읽기 전용 매니저"""
    
    def get_queryset(self):
        return super().get_queryset().using('read_replica')

class OptimizedQueryManager(models.Manager):
    """최적화된 쿼리 매니저"""
    
    def get_queryset(self):
        return super().get_queryset().select_related().prefetch_related()
    
    def high_traffic_filter(self, **kwargs):
        """대용량 트래픽용 필터"""
        return self.get_queryset().filter(**kwargs).only(
            'id', 'name', 'status'  # 필요한 필드만 조회
        )
    
    def with_cache(self, cache_key, timeout=300):
        """캐시와 함께 조회"""
        from django.core.cache import cache
        
        cached_result = cache.get(cache_key)
        if cached_result is not None:
            return cached_result
        
        result = list(self.get_queryset())
        cache.set(cache_key, result, timeout)
        return result

# 모델 예제
class Product(models.Model):
    name = models.CharField(max_length=200, db_index=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    category = models.ForeignKey('Category', on_delete=models.CASCADE)
    status = models.CharField(max_length=20, default='active', db_index=True)
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    objects = models.Manager()  # 기본 매니저
    readonly = ReadOnlyManager()  # 읽기 전용 매니저
    optimized = OptimizedQueryManager()  # 최적화된 매니저
    
    class Meta:
        indexes = [
            models.Index(fields=['status', 'created_at']),  # 복합 인덱스
            models.Index(fields=['category', 'price']),
            models.Index(fields=['-created_at']),  # 정렬용 인덱스
        ]
        db_table = 'products'

2. 연결 풀링과 커넥션 최적화

# PostgreSQL 연결 풀 최적화
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ['DB_NAME'],
        'USER': os.environ['DB_USER'],
        'PASSWORD': os.environ['DB_PASSWORD'],
        'HOST': os.environ['DB_HOST'],
        'PORT': os.environ['DB_PORT'],
        'CONN_MAX_AGE': 600,  # 10분간 연결 재사용
        'OPTIONS': {
            'MAX_CONNS': 20,  # 프로세스당 최대 연결 수
            'MIN_CONNS': 5,   # 최소 연결 수 유지
            'connect_timeout': 10,
            'options': '-c default_transaction_isolation=read_committed'
        },
        'TEST': {
            'NAME': 'test_' + os.environ['DB_NAME'],
        }
    }
}

# 커스텀 데이터베이스 래퍼
class DatabaseConnectionManager:
    """데이터베이스 연결 관리"""
    
    def __init__(self):
        self.connection_stats = {
            'total_queries': 0,
            'slow_queries': 0,
            'connection_errors': 0,
            'active_connections': 0
        }
    
    def execute_query(self, sql, params=None, using='default'):
        """쿼리 실행 with 모니터링"""
        import time
        from django.db import connections
        
        start_time = time.time()
        
        try:
            connection = connections[using]
            with connection.cursor() as cursor:
                cursor.execute(sql, params or [])
                result = cursor.fetchall()
            
            execution_time = time.time() - start_time
            self.connection_stats['total_queries'] += 1
            
            # 느린 쿼리 감지
            if execution_time > 1.0:
                self.connection_stats['slow_queries'] += 1
                self.log_slow_query(sql, execution_time, params)
            
            return result
            
        except Exception as e:
            self.connection_stats['connection_errors'] += 1
            raise e
    
    def log_slow_query(self, sql, execution_time, params):
        """느린 쿼리 로깅"""
        import logging
        logger = logging.getLogger('slow_queries')
        
        logger.warning(f"Slow query detected: {execution_time:.3f}s")
        logger.warning(f"SQL: {sql}")
        logger.warning(f"Params: {params}")
    
    def get_connection_stats(self):
        """연결 통계 반환"""
        return self.connection_stats

# 전역 데이터베이스 매니저
db_manager = DatabaseConnectionManager()

3. 인덱스 최적화와 쿼리 성능 향상

# 인덱스 전략
class IndexOptimizedModel(models.Model):
    """인덱스 최적화된 모델 예제"""
    
    # 기본 필드들
    name = models.CharField(max_length=200)
    status = models.CharField(max_length=20, choices=[
        ('active', 'Active'),
        ('inactive', 'Inactive'),
        ('pending', 'Pending')
    ])
    category_id = models.IntegerField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        indexes = [
            # 1. 단일 필드 인덱스
            models.Index(fields=['status']),
            models.Index(fields=['category_id']),
            models.Index(fields=['-created_at']),  # 내림차순 정렬용
            
            # 2. 복합 인덱스 (순서 중요!)
            models.Index(fields=['status', 'category_id']),
            models.Index(fields=['status', 'created_at']),
            models.Index(fields=['category_id', 'price']),
            
            # 3. 부분 인덱스 (PostgreSQL)
            models.Index(
                fields=['name'],
                name='idx_active_products_name',
                condition=models.Q(status='active')
            ),
        ]

# 쿼리 최적화 유틸리티
class QueryOptimizer:
    """쿼리 최적화 도구"""
    
    @staticmethod
    def optimize_select_related(queryset, depth=2):
        """자동으로 select_related 최적화"""
        model = queryset.model
        select_fields = []
        
        for field in model._meta.get_fields():
            if hasattr(field, 'related_model') and depth > 0:
                if field.one_to_one or field.many_to_one:
                    select_fields.append(field.name)
        
        return queryset.select_related(*select_fields)
    
    @staticmethod
    def bulk_create_optimized(model_class, objects, batch_size=1000):
        """최적화된 bulk_create"""
        
        # 배치 단위로 처리
        for i in range(0, len(objects), batch_size):
            batch = objects[i:i + batch_size]
            model_class.objects.bulk_create(
                batch,
                batch_size=batch_size,
                ignore_conflicts=True  # 중복 무시
            )

# 사용 예제
def get_products_optimized():
    """최적화된 상품 조회"""
    
    queryset = Product.objects.all()
    
    # 자동 최적화 적용
    queryset = QueryOptimizer.optimize_select_related(queryset)
    
    # 필요한 필드만 선택
    queryset = queryset.only('id', 'name', 'price', 'status')
    
    # 조건 최적화
    queryset = queryset.filter(
        status='active'
    ).order_by('-created_at')
    
    return queryset[:100]  # 페이지네이션

🎯 캐싱 전략: 성능의 핵심

캐싱은 대용량 트래픽 처리에서 가장 즉각적이고 효과적인 성능 향상 방법입니다. Django는 다양한 레벨의 캐싱을 지원합니다.

1. 다층 캐싱 아키텍처

# 캐싱 계층 구조
CACHING_LAYERS = {
    'L1_Browser': {
        'location': 'Client Browser',
        'ttl': '1 hour',
        'scope': 'Static assets, API responses'
    },
    'L2_CDN': {
        'location': 'CloudFlare/AWS CloudFront',
        'ttl': '24 hours',
        'scope': 'Static files, Public content'
    },
    'L3_Reverse_Proxy': {
        'location': 'Nginx/Varnish',
        'ttl': '10 minutes',
        'scope': 'Full page cache'
    },
    'L4_Application': {
        'location': 'Django Cache Framework',
        'ttl': '5 minutes',
        'scope': 'View cache, Template fragments'
    },
    'L5_Database': {
        'location': 'Redis/Memcached',
        'ttl': '1 hour',
        'scope': 'Query results, Session data'
    }
}

# Redis 클러스터 설정
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': [
            f"redis://{os.environ['REDIS_NODE1']}:6379/0",
            f"redis://{os.environ['REDIS_NODE2']}:6379/0",
            f"redis://{os.environ['REDIS_NODE3']}:6379/0",
        ],
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.ShardClient',
            'CONNECTION_POOL_KWARGS': {
                'max_connections': 100,
                'retry_on_timeout': True,
            },
            'SERIALIZER': 'django_redis.serializers.msgpack.MSGPackSerializer',
            'COMPRESSOR': 'django_redis.compressors.zlib.ZlibCompressor',
        },
        'TIMEOUT': 300,
        'KEY_PREFIX': 'myproject',
    },
    'sessions': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': f"redis://{os.environ['REDIS_SESSION']}:6379/1",
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            'CONNECTION_POOL_KWARGS': {'max_connections': 50},
        },
        'TIMEOUT': 86400,
    },
    'long_term': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': f"redis://{os.environ['REDIS_LONGTERM']}:6379/2",
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            'CONNECTION_POOL_KWARGS': {'max_connections': 30},
        },
        'TIMEOUT': 3600,  # 1시간
    }
}

2. 스마트 캐싱 매니저

import hashlib
import pickle
import time
from django.core.cache import cache, caches
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
import logging

logger = logging.getLogger(__name__)

class SmartCacheManager:
    """지능형 캐싱 관리자"""
    
    def __init__(self):
        self.cache_stats = {
            'hits': 0,
            'misses': 0,
            'sets': 0,
            'deletes': 0
        }
        self.cache_strategies = {
            'hot_data': {'timeout': 60, 'alias': 'default'},
            'warm_data': {'timeout': 300, 'alias': 'default'},
            'cold_data': {'timeout': 3600, 'alias': 'long_term'},
            'session_data': {'timeout': 86400, 'alias': 'sessions'}
        }
    
    def generate_cache_key(self, prefix, *args, **kwargs):
        """캐시 키 생성"""
        key_data = f"{prefix}:{':'.join(map(str, args))}:{':'.join(f'{k}={v}' for k, v in sorted(kwargs.items()))}"
        return hashlib.md5(key_data.encode()).hexdigest()
    
    def get_cached_data(self, cache_key, strategy='warm_data'):
        """캐시된 데이터 조회"""
        cache_config = self.cache_strategies.get(strategy, self.cache_strategies['warm_data'])
        cache_instance = caches[cache_config['alias']]
        
        try:
            data = cache_instance.get(cache_key)
            if data is not None:
                self.cache_stats['hits'] += 1
                return data
            else:
                self.cache_stats['misses'] += 1
                return None
        except Exception as e:
            logger.error(f"Cache get error: {e}")
            self.cache_stats['misses'] += 1
            return None
    
    def set_cached_data(self, cache_key, data, strategy='warm_data'):
        """데이터 캐싱"""
        cache_config = self.cache_strategies.get(strategy, self.cache_strategies['warm_data'])
        cache_instance = caches[cache_config['alias']]
        
        try:
            cache_instance.set(cache_key, data, cache_config['timeout'])
            self.cache_stats['sets'] += 1
            return True
        except Exception as e:
            logger.error(f"Cache set error: {e}")
            return False
    
    def delete_cached_data(self, cache_key, strategy='warm_data'):
        """캐시 삭제"""
        cache_config = self.cache_strategies.get(strategy, self.cache_strategies['warm_data'])
        cache_instance = caches[cache_config['alias']]
        
        try:
            cache_instance.delete(cache_key)
            self.cache_stats['deletes'] += 1
            return True
        except Exception as e:
            logger.error(f"Cache delete error: {e}")
            return False
    
    def cached_function(self, timeout=300, strategy='warm_data', key_prefix=None):
        """함수 결과 캐싱 데코레이터"""
        def decorator(func):
            def wrapper(*args, **kwargs):
                # 캐시 키 생성
                if key_prefix:
                    cache_key = self.generate_cache_key(key_prefix, func.__name__, *args, **kwargs)
                else:
                    cache_key = self.generate_cache_key(func.__module__, func.__name__, *args, **kwargs)
                
                # 캐시에서 확인
                cached_result = self.get_cached_data(cache_key, strategy)
                if cached_result is not None:
                    return cached_result
                
                # 함수 실행
                result = func(*args, **kwargs)
                
                # 결과 캐싱
                self.set_cached_data(cache_key, result, strategy)
                
                return result
            return wrapper
        return decorator
    
    def invalidate_pattern(self, pattern):
        """패턴 매칭으로 캐시 무효화"""
        from django_redis import get_redis_connection
        
        for alias in ['default', 'long_term', 'sessions']:
            try:
                redis_conn = get_redis_connection(alias)
                for key in redis_conn.scan_iter(match=pattern):
                    redis_conn.delete(key)
            except Exception as e:
                logger.error(f"Pattern invalidation error for {alias}: {e}")
    
    def get_cache_statistics(self):
        """캐시 통계 반환"""
        total_requests = self.cache_stats['hits'] + self.cache_stats['misses']
        hit_rate = (self.cache_stats['hits'] / total_requests * 100) if total_requests > 0 else 0
        
        return {
            'hit_rate': f"{hit_rate:.2f}%",
            'total_requests': total_requests,
            **self.cache_stats
        }

# 전역 캐시 매니저
cache_manager = SmartCacheManager()

3. 뷰 레벨 캐싱

from django.views.generic import ListView, DetailView
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers, vary_on_cookie

# 페이지 전체 캐싱
@method_decorator(cache_page(60 * 5), name='dispatch')  # 5분 캐싱
@method_decorator(vary_on_headers('User-Agent', 'Accept-Language'), name='dispatch')
class ProductListView(ListView):
    """상품 목록 뷰 (전체 페이지 캐싱)"""
    
    model = Product
    template_name = 'products/list.html'
    paginate_by = 20
    
    def get_queryset(self):
        # 이미 캐싱되므로 복잡한 쿼리도 OK
        return Product.objects.select_related('category').prefetch_related('tags').filter(
            status='active'
        ).order_by('-created_at')

# 조건부 캐싱
class ConditionalCacheView(DetailView):
    """조건부 캐싱 뷰"""
    
    model = Product
    template_name = 'products/detail.html'
    
    @cache_manager.cached_function(timeout=600, strategy='warm_data', key_prefix='product_detail')
    def get_object(self):
        """객체 조회 (캐싱됨)"""
        return super().get_object()
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        
        # 사용자별 다른 데이터는 별도 처리
        if self.request.user.is_authenticated:
            context['user_favorites'] = self.get_user_favorites()
            context['recommendations'] = self.get_recommendations()
        
        return context
    
    @cache_manager.cached_function(timeout=1800, strategy='cold_data')
    def get_recommendations(self):
        """추천 상품 (30분 캐싱)"""
        return Product.objects.filter(
            category=self.object.category
        ).exclude(
            id=self.object.id
        ).order_by('?')[:5]

# API 응답 캐싱
from rest_framework.views import APIView
from rest_framework.response import Response
from django.core.cache import cache

class CachedAPIView(APIView):
    """캐시된 API 뷰"""
    
    def get(self, request, *args, **kwargs):
        # 캐시 키 생성 (사용자, 매개변수 포함)
        cache_key = f"api_response:{request.user.id}:{request.GET.urlencode()}"
        
        # 캐시 확인
        cached_response = cache.get(cache_key)
        if cached_response:
            return Response(cached_response)
        
        # 데이터 생성
        data = self.generate_response_data()
        
        # 응답 캐싱 (5분)
        cache.set(cache_key, data, 300)
        
        return Response(data)
    
    def generate_response_data(self):
        """응답 데이터 생성"""
        return {
            'products': list(Product.objects.values('id', 'name', 'price')[:100]),
            'timestamp': time.time()
        }

4. 템플릿 프래그먼트 캐싱

{% raw %}

<!-- templates/products/detail.html -->
{% load cache %}

<div class="product-detail">
    <!-- 기본 상품 정보 (자주 변경되지 않음) -->
    {% cache 3600 product_basic product.id %}
    <div class="product-info">
        <h1>{{ product.name }}</h1>
        <p class="price">${{ product.price }}</p>
        <div class="description">{{ product.description }}</div>
    </div>
    {% endcache %}
    
    <!-- 리뷰 섹션 (중간 정도 캐싱) -->
    {% cache 600 product_reviews product.id %}
    <div class="reviews">
        <h3>고객 리뷰</h3>
        {% for review in product.reviews.all %}
            <div class="review">
                <div class="rating">{{ review.rating }}★</div>
                <p>{{ review.comment }}</p>
            </div>
        {% endfor %}
    </div>
    {% endcache %}
    
    <!-- 사용자별 데이터 (캐싱하지 않음) -->
    <div class="user-actions">
        {% if user.is_authenticated %}
            <button class="add-to-cart">장바구니 담기</button>
            <button class="add-to-wishlist">찜하기</button>
        {% endif %}
    </div>
    
    <!-- 관련 상품 (긴 캐싱) -->
    {% cache 1800 related_products product.category.id %}
    <div class="related-products">
        <h3>관련 상품</h3>
        {% for related in related_products %}
            <div class="product-item">
                <a href="{{ related.get_absolute_url }}">{{ related.name }}</a>
            </div>
        {% endfor %}
    </div>
    {% endcache %}
</div>

{% endraw %}

5. 캐시 무효화 전략

from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
import logging

logger = logging.getLogger(__name__)

class CacheInvalidationManager:
    """캐시 무효화 관리자"""
    
    @staticmethod
    def invalidate_product_cache(product_id):
        """상품 관련 캐시 무효화"""
        patterns = [
            f"*product_detail*{product_id}*",
            f"*product_list*",
            f"*api_response*",
            f"*product_basic*{product_id}*",
            f"*product_reviews*{product_id}*"
        ]
        
        for pattern in patterns:
            cache_manager.invalidate_pattern(pattern)
        
        logger.info(f"Invalidated cache for product {product_id}")
    
    @staticmethod
    def invalidate_category_cache(category_id):
        """카테고리 관련 캐시 무효화"""
        patterns = [
            f"*category*{category_id}*",
            f"*related_products*{category_id}*",
            f"*product_list*"
        ]
        
        for pattern in patterns:
            cache_manager.invalidate_pattern(pattern)
        
        logger.info(f"Invalidated cache for category {category_id}")

# 시그널 기반 캐시 무효화
@receiver(post_save, sender=Product)
def invalidate_product_cache_on_save(sender, instance, created, **kwargs):
    """상품 저장 시 캐시 무효화"""
    CacheInvalidationManager.invalidate_product_cache(instance.id)
    
    if instance.category:
        CacheInvalidationManager.invalidate_category_cache(instance.category.id)

@receiver(post_delete, sender=Product)
def invalidate_product_cache_on_delete(sender, instance, **kwargs):
    """상품 삭제 시 캐시 무효화"""
    CacheInvalidationManager.invalidate_product_cache(instance.id)
    
    if instance.category:
        CacheInvalidationManager.invalidate_category_cache(instance.category.id)

# 배치 캐시 갱신
class CacheWarmupManager:
    """캐시 예열 관리자"""
    
    @staticmethod
    def warmup_popular_products():
        """인기 상품 캐시 예열"""
        popular_products = Product.objects.filter(
            status='active'
        ).order_by('-view_count')[:100]
        
        for product in popular_products:
            # 미리 캐시에 로드
            cache_key = cache_manager.generate_cache_key('product_detail', product.id)
            cache_manager.set_cached_data(
                cache_key, 
                product, 
                strategy='hot_data'
            )
        
        logger.info(f"Warmed up cache for {len(popular_products)} popular products")
    
    @staticmethod
    def warmup_categories():
        """카테고리 캐시 예열"""
        categories = Category.objects.all()
        
        for category in categories:
            # 카테고리별 상품 미리 로드
            cache_key = cache_manager.generate_cache_key('category_products', category.id)
            products = list(category.products.filter(status='active')[:20])
            cache_manager.set_cached_data(
                cache_key,
                products,
                strategy='warm_data'
            )
        
        logger.info(f"Warmed up cache for {len(categories)} categories")

# Celery 태스크로 예열 실행
from celery import shared_task

@shared_task
def warmup_cache_periodic():
    """주기적 캐시 예열"""
    CacheWarmupManager.warmup_popular_products()
    CacheWarmupManager.warmup_categories()
    return "Cache warmup completed"

⚡ 비동기 처리와 큐 시스템

대용량 트래픽에서는 무거운 작업을 백그라운드로 처리하여 응답 시간을 최소화해야 합니다. Celery와 Redis를 활용한 비동기 처리 전략을 살펴보겠습니다.

1. Celery 최적화 설정

# celery_app.py
import os
from celery import Celery
from django.conf import settings

# Django 설정 모듈 지정
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings.production')

app = Celery('myproject')

# Django 설정에서 Celery 설정 로드
app.config_from_object('django.conf:settings', namespace='CELERY')

# 고성능 설정
app.conf.update(
    # 브로커 설정
    broker_url='redis://redis-cluster:6379/0',
    result_backend='redis://redis-cluster:6379/0',
    
    # 성능 최적화
    task_serializer='msgpack',
    result_serializer='msgpack',
    accept_content=['msgpack', 'json'],
    result_expires=3600,  # 1시간 후 결과 만료
    
    # 워커 최적화
    worker_prefetch_multiplier=4,  # 동시 처리 태스크 수
    worker_max_tasks_per_child=1000,  # 메모리 누수 방지
    worker_disable_rate_limits=True,  # 속도 제한 비활성화
    
    # 라우팅 설정
    task_routes={
        'myproject.tasks.send_email': {'queue': 'email'},
        'myproject.tasks.process_image': {'queue': 'media'},
        'myproject.tasks.generate_report': {'queue': 'reports'},
        'myproject.tasks.cleanup_data': {'queue': 'maintenance'},
    },
    
    # 큐별 우선순위 설정
    task_default_queue='default',
    task_queue_max_priority=10,
    task_default_priority=5,
    
    # 모니터링 설정
    worker_send_task_events=True,
    task_send_sent_event=True,
)

# 자동으로 태스크 발견
app.autodiscover_tasks()

# 시작 시 설정 출력
@app.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
    """주기적 태스크 설정"""
    
    # 캐시 예열 (매 10분)
    sender.add_periodic_task(
        600.0,
        warmup_cache_periodic.s(),
        name='cache_warmup'
    )
    
    # 성능 통계 수집 (매 5분)
    sender.add_periodic_task(
        300.0,
        collect_performance_stats.s(),
        name='performance_stats'
    )
    
    # 데이터 정리 (매일 새벽 2시)
    sender.add_periodic_task(
        crontab(hour=2, minute=0),
        cleanup_old_data.s(),
        name='daily_cleanup'
    )

2. 태스크 우선순위와 분산 처리

# tasks.py
from celery import shared_task, group, chain, chord
from celery.exceptions import Retry
from django.core.mail import send_mail
from django.core.cache import cache
import logging
import time

logger = logging.getLogger(__name__)

# 우선순위별 태스크 정의
@shared_task(bind=True, priority=9, max_retries=3)  # 높은 우선순위
def send_critical_notification(self, user_id, message):
    """중요한 알림 발송 (즉시 처리)"""
    try:
        user = User.objects.get(id=user_id)
        
        # 이메일 발송
        send_mail(
            subject='중요 알림',
            message=message,
            from_email=settings.DEFAULT_FROM_EMAIL,
            recipient_list=[user.email],
            fail_silently=False
        )
        
        # SMS 발송 (외부 API)
        send_sms_notification(user.phone, message)
        
        logger.info(f"Critical notification sent to user {user_id}")
        return f"Notification sent to {user.email}"
        
    except Exception as exc:
        logger.error(f"Failed to send critical notification: {exc}")
        
        # 재시도 전략
        if self.request.retries < self.max_retries:
            # 지수 백오프
            countdown = 2 ** self.request.retries
            raise self.retry(countdown=countdown, exc=exc)
        else:
            # 최종 실패 처리
            handle_notification_failure(user_id, message, str(exc))
            raise exc

@shared_task(priority=5)  # 일반 우선순위
def process_user_action(user_id, action_type, action_data):
    """사용자 액션 처리"""
    try:
        user = User.objects.get(id=user_id)
        
        if action_type == 'purchase':
            process_purchase(user, action_data)
        elif action_type == 'review':
            process_review(user, action_data)
        elif action_type == 'wishlist':
            process_wishlist(user, action_data)
        
        # 통계 업데이트
        update_user_statistics.delay(user_id)
        
        logger.info(f"Processed {action_type} for user {user_id}")
        
    except Exception as exc:
        logger.error(f"Failed to process user action: {exc}")
        raise exc

@shared_task(priority=2)  # 낮은 우선순위
def generate_analytics_report(report_type, date_range):
    """분석 보고서 생성 (배치 처리)"""
    try:
        start_time = time.time()
        
        if report_type == 'sales':
            data = generate_sales_report(date_range)
        elif report_type == 'user_behavior':
            data = generate_user_behavior_report(date_range)
        elif report_type == 'performance':
            data = generate_performance_report(date_range)
        
        # 보고서 저장
        report = Report.objects.create(
            type=report_type,
            data=data,
            generation_time=time.time() - start_time
        )
        
        # 관련자들에게 알림
        notify_report_completion.delay(report.id)
        
        logger.info(f"Generated {report_type} report in {report.generation_time:.2f}s")
        return report.id
        
    except Exception as exc:
        logger.error(f"Failed to generate report: {exc}")
        raise exc

# 병렬 처리 패턴
@shared_task
def process_bulk_data(data_chunks):
    """대량 데이터 병렬 처리"""
    
    # 데이터를 청크로 분할하여 병렬 처리
    job = group(
        process_data_chunk.s(chunk) for chunk in data_chunks
    )
    
    result = job.apply_async()
    
    # 모든 청크 처리 완료 대기
    processed_data = result.get()
    
    # 결과 병합
    merged_result = merge_processed_data(processed_data)
    
    return merged_result

@shared_task
def process_data_chunk(chunk):
    """데이터 청크 처리"""
    processed_items = []
    
    for item in chunk:
        try:
            processed_item = complex_data_processing(item)
            processed_items.append(processed_item)
        except Exception as e:
            logger.error(f"Failed to process item {item}: {e}")
    
    return processed_items

# 파이프라인 처리 패턴
@shared_task
def image_processing_pipeline(image_id):
    """이미지 처리 파이프라인"""
    
    # 체인 방식으로 순차 처리
    pipeline = chain(
        validate_image.s(image_id),
        resize_image.s(),
        apply_watermark.s(),
        generate_thumbnails.s(),
        upload_to_cdn.s(),
        update_database.s()
    )
    
    return pipeline.apply_async()

@shared_task
def validate_image(image_id):
    """이미지 검증"""
    image = Image.objects.get(id=image_id)
    
    if not is_valid_image(image.file):
        raise ValueError("Invalid image format")
    
    return image_id

@shared_task
def resize_image(image_id):
    """이미지 리사이즈"""
    image = Image.objects.get(id=image_id)
    
    resized_path = resize_image_file(image.file.path)
    image.resized_file = resized_path
    image.save()
    
    return image_id

# 조건부 처리 패턴
@shared_task
def smart_notification_dispatch(notification_data):
    """스마트 알림 발송"""
    
    user_id = notification_data['user_id']
    message = notification_data['message']
    urgency = notification_data.get('urgency', 'normal')
    
    # 사용자 선호도 확인
    user_preferences = get_user_notification_preferences(user_id)
    
    # 발송 방법 결정
    notification_methods = []
    
    if urgency == 'critical':
        notification_methods = ['email', 'sms', 'push']
    elif urgency == 'high':
        notification_methods = ['email', 'push']
    else:
        notification_methods = ['email']
    
    # 선호도에 따라 필터링
    filtered_methods = [
        method for method in notification_methods
        if user_preferences.get(method, True)
    ]
    
    # 병렬로 발송
    job = group(
        send_notification_by_method.s(user_id, message, method)
        for method in filtered_methods
    )
    
    return job.apply_async()

@shared_task
def send_notification_by_method(user_id, message, method):
    """특정 방법으로 알림 발송"""
    try:
        if method == 'email':
            return send_email_notification(user_id, message)
        elif method == 'sms':
            return send_sms_notification(user_id, message)
        elif method == 'push':
            return send_push_notification(user_id, message)
    except Exception as e:
        logger.error(f"Failed to send {method} notification: {e}")
        raise e

3. 큐 모니터링과 관리

# monitoring.py
from celery import current_app
from django.core.management.base import BaseCommand
import redis
import json
import time

class CeleryMonitor:
    """Celery 모니터링 클래스"""
    
    def __init__(self):
        self.app = current_app
        self.redis_client = redis.Redis.from_url(settings.CELERY_BROKER_URL)
    
    def get_queue_lengths(self):
        """큐별 대기 중인 태스크 수"""
        queues = ['default', 'email', 'media', 'reports', 'maintenance']
        queue_lengths = {}
        
        for queue in queues:
            length = self.redis_client.llen(f"celery_{queue}")
            queue_lengths[queue] = length
        
        return queue_lengths
    
    def get_active_workers(self):
        """활성 워커 정보"""
        inspect = self.app.control.inspect()
        
        try:
            active = inspect.active() or {}
            stats = inspect.stats() or {}
            
            worker_info = {}
            for worker_name, worker_stats in stats.items():
                worker_info[worker_name] = {
                    'active_tasks': len(active.get(worker_name, [])),
                    'total_tasks': worker_stats.get('total', {}),
                    'pool_processes': worker_stats.get('pool', {}).get('processes'),
                    'rusage': worker_stats.get('rusage', {})
                }
            
            return worker_info
            
        except Exception as e:
            logger.error(f"Failed to get worker info: {e}")
            return {}
    
    def get_failed_tasks(self, limit=10):
        """실패한 태스크 목록"""
        failed_tasks = []
        
        # Redis에서 실패한 태스크 조회
        failed_keys = self.redis_client.keys("celery-task-meta-*")
        
        for key in failed_keys[:limit]:
            try:
                task_data = self.redis_client.get(key)
                if task_data:
                    task_info = json.loads(task_data)
                    if task_info.get('status') == 'FAILURE':
                        failed_tasks.append({
                            'task_id': key.decode().split('-')[-1],
                            'result': task_info.get('result'),
                            'traceback': task_info.get('traceback')
                        })
            except Exception as e:
                continue
        
        return failed_tasks
    
    def get_performance_metrics(self):
        """성능 메트릭"""
        queue_lengths = self.get_queue_lengths()
        worker_info = self.get_active_workers()
        failed_tasks = self.get_failed_tasks()
        
        # 총 대기 태스크 수
        total_pending = sum(queue_lengths.values())
        
        # 총 활성 워커 수
        total_workers = len(worker_info)
        
        # 총 활성 태스크 수
        total_active_tasks = sum(
            info['active_tasks'] for info in worker_info.values()
        )
        
        return {
            'queue_lengths': queue_lengths,
            'total_pending_tasks': total_pending,
            'total_workers': total_workers,
            'total_active_tasks': total_active_tasks,
            'failed_tasks_count': len(failed_tasks),
            'worker_details': worker_info,
            'timestamp': time.time()
        }

# 자동 스케일링
class CeleryAutoScaler:
    """Celery 워커 자동 스케일링"""
    
    def __init__(self):
        self.monitor = CeleryMonitor()
        self.scaling_rules = {
            'scale_up_threshold': 100,    # 대기 태스크 100개 이상 시 스케일 업
            'scale_down_threshold': 10,   # 대기 태스크 10개 이하 시 스케일 다운
            'max_workers': 20,            # 최대 워커 수
            'min_workers': 3,             # 최소 워커 수
            'scale_up_count': 2,          # 한 번에 추가할 워커 수
            'scale_down_count': 1         # 한 번에 제거할 워커 수
        }
    
    def should_scale_up(self, metrics):
        """스케일 업 필요 여부 판단"""
        return (
            metrics['total_pending_tasks'] > self.scaling_rules['scale_up_threshold'] and
            metrics['total_workers'] < self.scaling_rules['max_workers']
        )
    
    def should_scale_down(self, metrics):
        """스케일 다운 필요 여부 판단"""
        return (
            metrics['total_pending_tasks'] < self.scaling_rules['scale_down_threshold'] and
            metrics['total_workers'] > self.scaling_rules['min_workers']
        )
    
    def execute_scaling(self):
        """스케일링 실행"""
        metrics = self.monitor.get_performance_metrics()
        
        if self.should_scale_up(metrics):
            self.scale_up()
        elif self.should_scale_down(metrics):
            self.scale_down()
    
    def scale_up(self):
        """워커 수 증가"""
        # Kubernetes, Docker Swarm 등과 연동
        logger.info("Scaling up Celery workers")
        # kubectl scale deployment celery-worker --replicas=+2
    
    def scale_down(self):
        """워커 수 감소"""
        logger.info("Scaling down Celery workers")
        # kubectl scale deployment celery-worker --replicas=-1

# 관리 명령어
class Command(BaseCommand):
    """Celery 모니터링 명령어"""
    
    def add_arguments(self, parser):
        parser.add_argument('--action', choices=['status', 'autoscale'], default='status')
        parser.add_argument('--interval', type=int, default=30)
    
    def handle(self, *args, **options):
        monitor = CeleryMonitor()
        autoscaler = CeleryAutoScaler()
        
        if options['action'] == 'status':
            self.show_status(monitor)
        elif options['action'] == 'autoscale':
            self.run_autoscaler(autoscaler, options['interval'])
    
    def show_status(self, monitor):
        """상태 표시"""
        metrics = monitor.get_performance_metrics()
        
        self.stdout.write("=== Celery Status ===")
        self.stdout.write(f"Total pending tasks: {metrics['total_pending_tasks']}")
        self.stdout.write(f"Active workers: {metrics['total_workers']}")
        self.stdout.write(f"Active tasks: {metrics['total_active_tasks']}")
        self.stdout.write(f"Failed tasks: {metrics['failed_tasks_count']}")
        
        self.stdout.write("\n=== Queue Lengths ===")
        for queue, length in metrics['queue_lengths'].items():
            self.stdout.write(f"{queue}: {length}")
    
    def run_autoscaler(self, autoscaler, interval):
        """자동 스케일러 실행"""
        self.stdout.write(f"Starting autoscaler with {interval}s interval")
        
        while True:
            try:
                autoscaler.execute_scaling()
                time.sleep(interval)
            except KeyboardInterrupt:
                self.stdout.write("Autoscaler stopped")
                break
            except Exception as e:
                self.stderr.write(f"Autoscaler error: {e}")
                time.sleep(interval)

5. 모니터링 및 성능 측정

대용량 트래픽을 처리하는 Django 애플리케이션에서는 실시간 모니터링과 성능 측정이 필수입니다. 문제가 발생하기 전에 미리 감지하고 대응할 수 있는 모니터링 시스템을 구축해야 합니다.

1. Django APM(Application Performance Monitoring)

Django Debug Toolbar와 Silk를 활용한 프로파일링

# settings/development.py
INSTALLED_APPS = [
    # ... 다른 앱들
    'debug_toolbar',
    'silk',
]

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    'silk.middleware.SilkyMiddleware',
    # ... 다른 미들웨어들
]

# Debug Toolbar 설정
INTERNAL_IPS = [
    '127.0.0.1',
    'localhost',
]

# Silk 설정
SILKY_PYTHON_PROFILER = True
SILKY_PYTHON_PROFILER_BINARY = True
SILKY_AUTHENTICATION = True
SILKY_AUTHORISATION = True
SILKY_META = True

커스텀 성능 미들웨어

# middleware/performance.py
import time
import logging
from django.utils.deprecation import MiddlewareMixin
from django.db import connection
from django.core.cache import cache

logger = logging.getLogger('performance')

class PerformanceMonitoringMiddleware(MiddlewareMixin):
    """요청별 성능 모니터링 미들웨어"""
    
    def process_request(self, request):
        request._start_time = time.time()
        request._db_queries_start = len(connection.queries)
        
    def process_response(self, request, response):
        # 응답 시간 계산
        if hasattr(request, '_start_time'):
            duration = time.time() - request._start_time
            
            # DB 쿼리 수 계산
            db_queries_count = len(connection.queries) - getattr(request, '_db_queries_start', 0)
            
            # 응답 크기 계산
            content_length = len(response.content) if hasattr(response, 'content') else 0
            
            # 로깅
            log_data = {
                'method': request.method,
                'path': request.path,
                'duration': duration * 1000,  # ms 단위
                'status_code': response.status_code,
                'db_queries': db_queries_count,
                'content_length': content_length,
                'user_agent': request.META.get('HTTP_USER_AGENT', ''),
                'ip': self.get_client_ip(request),
            }
            
            # 성능 임계값 검사
            if duration > 1.0:  # 1초 이상
                logger.warning(f"Slow request: {log_data}")
            elif db_queries_count > 10:  # 10개 이상 쿼리
                logger.warning(f"N+1 query detected: {log_data}")
            else:
                logger.info(f"Request processed: {log_data}")
            
            # 메트릭 수집
            self.collect_metrics(log_data)
            
        return response
    
    def get_client_ip(self, request):
        """클라이언트 IP 주소 추출"""
        x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
        if x_forwarded_for:
            ip = x_forwarded_for.split(',')[0]
        else:
            ip = request.META.get('REMOTE_ADDR')
        return ip
    
    def collect_metrics(self, log_data):
        """메트릭 수집 및 저장"""
        # Redis를 사용한 실시간 메트릭 수집
        cache_key = f"metrics:{int(time.time() // 60)}"  # 분 단위
        
        try:
            current_metrics = cache.get(cache_key, {})
            current_metrics.setdefault('request_count', 0)
            current_metrics.setdefault('total_duration', 0)
            current_metrics.setdefault('slow_requests', 0)
            current_metrics.setdefault('status_codes', {})
            
            current_metrics['request_count'] += 1
            current_metrics['total_duration'] += log_data['duration']
            
            if log_data['duration'] > 1000:  # 1초 이상
                current_metrics['slow_requests'] += 1
            
            status_code = str(log_data['status_code'])
            current_metrics['status_codes'].setdefault(status_code, 0)
            current_metrics['status_codes'][status_code] += 1
            
            cache.set(cache_key, current_metrics, 300)  # 5분 캐시
            
        except Exception as e:
            logger.error(f"Failed to collect metrics: {e}")

2. 데이터베이스 모니터링

PostgreSQL 성능 모니터링

# monitoring/database.py
import psycopg2
from django.db import connection
from django.conf import settings
import logging

logger = logging.getLogger('db_monitor')

class DatabaseMonitor:
    """데이터베이스 성능 모니터링"""
    
    def __init__(self):
        self.connection = connection
    
    def get_slow_queries(self, threshold_ms=1000):
        """느린 쿼리 조회"""
        with self.connection.cursor() as cursor:
            cursor.execute("""
                SELECT 
                    query,
                    calls,
                    total_time,
                    mean_time,
                    rows,
                    100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
                FROM pg_stat_statements 
                WHERE mean_time > %s
                ORDER BY total_time DESC
                LIMIT 20;
            """, [threshold_ms])
            
            columns = [desc[0] for desc in cursor.description]
            results = [dict(zip(columns, row)) for row in cursor.fetchall()]
            
            for query in results:
                logger.warning(f"Slow query detected: {query}")
            
            return results
    
    def get_connection_stats(self):
        """연결 통계 조회"""
        with self.connection.cursor() as cursor:
            cursor.execute("""
                SELECT 
                    datname,
                    numbackends,
                    xact_commit,
                    xact_rollback,
                    blks_read,
                    blks_hit,
                    temp_files,
                    temp_bytes,
                    deadlocks
                FROM pg_stat_database 
                WHERE datname = current_database();
            """)
            
            columns = [desc[0] for desc in cursor.description]
            return dict(zip(columns, cursor.fetchone()))
    
    def get_lock_info(self):
        """잠금 정보 조회"""
        with self.connection.cursor() as cursor:
            cursor.execute("""
                SELECT 
                    bl.pid AS blocked_pid,
                    bl.usename AS blocked_user,
                    bl.query AS blocked_query,
                    kl.pid AS blocking_pid,
                    kl.usename AS blocking_user,
                    kl.query AS blocking_query
                FROM pg_catalog.pg_locks bl
                JOIN pg_catalog.pg_stat_activity bl_act ON bl.pid = bl_act.pid
                JOIN pg_catalog.pg_locks kl ON bl.transactionid = kl.transactionid
                JOIN pg_catalog.pg_stat_activity kl_act ON kl.pid = kl_act.pid
                WHERE bl.granted = false AND kl.granted = true
                AND bl.pid != kl.pid;
            """)
            
            columns = [desc[0] for desc in cursor.description]
            return [dict(zip(columns, row)) for row in cursor.fetchall()]
    
    def analyze_query_performance(self):
        """쿼리 성능 분석"""
        stats = {
            'slow_queries': self.get_slow_queries(),
            'connection_stats': self.get_connection_stats(),
            'locks': self.get_lock_info(),
        }
        
        # 성능 임계값 검사
        conn_stats = stats['connection_stats']
        if conn_stats['numbackends'] > 100:
            logger.warning(f"High connection count: {conn_stats['numbackends']}")
        
        if conn_stats['deadlocks'] > 0:
            logger.error(f"Deadlocks detected: {conn_stats['deadlocks']}")
        
        return stats

3. Redis 모니터링

# monitoring/redis_monitor.py
import redis
import json
import time
from django.core.cache import cache
from django.conf import settings

class RedisMonitor:
    """Redis 성능 모니터링"""
    
    def __init__(self):
        self.redis_client = redis.Redis.from_url(settings.CACHES['default']['LOCATION'])
    
    def get_info(self):
        """Redis 정보 조회"""
        info = self.redis_client.info()
        
        memory_usage = {
            'used_memory': info['used_memory'],
            'used_memory_human': info['used_memory_human'],
            'used_memory_peak': info['used_memory_peak'],
            'used_memory_peak_human': info['used_memory_peak_human'],
            'memory_fragmentation_ratio': info.get('mem_fragmentation_ratio', 0),
        }
        
        performance = {
            'connected_clients': info['connected_clients'],
            'total_commands_processed': info['total_commands_processed'],
            'instantaneous_ops_per_sec': info['instantaneous_ops_per_sec'],
            'keyspace_hits': info['keyspace_hits'],
            'keyspace_misses': info['keyspace_misses'],
            'expired_keys': info['expired_keys'],
            'evicted_keys': info['evicted_keys'],
        }
        
        # 히트율 계산
        if (info['keyspace_hits'] + info['keyspace_misses']) > 0:
            hit_rate = info['keyspace_hits'] / (info['keyspace_hits'] + info['keyspace_misses'])
            performance['hit_rate'] = hit_rate
        
        return {
            'memory': memory_usage,
            'performance': performance,
            'uptime': info['uptime_in_seconds'],
        }
    
    def get_slow_log(self, count=10):
        """느린 명령 로그 조회"""
        slow_commands = self.redis_client.slowlog_get(count)
        
        formatted_commands = []
        for cmd in slow_commands:
            formatted_commands.append({
                'id': cmd['id'],
                'start_time': cmd['start_time'],
                'duration': cmd['duration'],  # 마이크로초
                'command': ' '.join([arg.decode() if isinstance(arg, bytes) else str(arg) 
                                   for arg in cmd['command']]),
            })
        
        return formatted_commands
    
    def monitor_performance(self):
        """성능 모니터링 및 알림"""
        info = self.get_info()
        
        # 메모리 사용량 체크
        memory_usage_percent = (info['memory']['used_memory'] / 
                              (1024 * 1024 * 1024))  # GB 단위
        
        if memory_usage_percent > 0.8:  # 80% 이상
            logging.warning(f"High Redis memory usage: {memory_usage_percent:.2f}GB")
        
        # 히트율 체크
        hit_rate = info['performance'].get('hit_rate', 0)
        if hit_rate < 0.8:  # 80% 미만
            logging.warning(f"Low Redis hit rate: {hit_rate:.2%}")
        
        # 연결 수 체크
        connected_clients = info['performance']['connected_clients']
        if connected_clients > 1000:
            logging.warning(f"High Redis connection count: {connected_clients}")
        
        return info

4. 시스템 메트릭 수집

# monitoring/system_metrics.py
import psutil
import platform
import json
from datetime import datetime
import logging

logger = logging.getLogger('system_metrics')

class SystemMetricsCollector:
    """시스템 메트릭 수집기"""
    
    def collect_cpu_metrics(self):
        """CPU 메트릭 수집"""
        cpu_percent = psutil.cpu_percent(interval=1, percpu=True)
        cpu_count = psutil.cpu_count()
        load_avg = psutil.getloadavg() if hasattr(psutil, 'getloadavg') else (0, 0, 0)
        
        return {
            'cpu_percent_total': psutil.cpu_percent(),
            'cpu_percent_per_core': cpu_percent,
            'cpu_count': cpu_count,
            'load_average': {
                '1min': load_avg[0],
                '5min': load_avg[1],
                '15min': load_avg[2],
            }
        }
    
    def collect_memory_metrics(self):
        """메모리 메트릭 수집"""
        memory = psutil.virtual_memory()
        swap = psutil.swap_memory()
        
        return {
            'memory': {
                'total': memory.total,
                'available': memory.available,
                'used': memory.used,
                'percent': memory.percent,
                'free': memory.free,
                'cached': getattr(memory, 'cached', 0),
                'buffers': getattr(memory, 'buffers', 0),
            },
            'swap': {
                'total': swap.total,
                'used': swap.used,
                'free': swap.free,
                'percent': swap.percent,
            }
        }
    
    def collect_disk_metrics(self):
        """디스크 메트릭 수집"""
        disk_usage = psutil.disk_usage('/')
        disk_io = psutil.disk_io_counters()
        
        partitions = []
        for partition in psutil.disk_partitions():
            try:
                usage = psutil.disk_usage(partition.mountpoint)
                partitions.append({
                    'device': partition.device,
                    'mountpoint': partition.mountpoint,
                    'fstype': partition.fstype,
                    'total': usage.total,
                    'used': usage.used,
                    'free': usage.free,
                    'percent': (usage.used / usage.total) * 100,
                })
            except PermissionError:
                continue
        
        return {
            'root_disk': {
                'total': disk_usage.total,
                'used': disk_usage.used,
                'free': disk_usage.free,
                'percent': (disk_usage.used / disk_usage.total) * 100,
            },
            'partitions': partitions,
            'io_counters': {
                'read_bytes': disk_io.read_bytes if disk_io else 0,
                'write_bytes': disk_io.write_bytes if disk_io else 0,
                'read_count': disk_io.read_count if disk_io else 0,
                'write_count': disk_io.write_count if disk_io else 0,
            } if disk_io else {}
        }
    
    def collect_network_metrics(self):
        """네트워크 메트릭 수집"""
        net_io = psutil.net_io_counters()
        connections = len(psutil.net_connections())
        
        return {
            'io_counters': {
                'bytes_sent': net_io.bytes_sent,
                'bytes_recv': net_io.bytes_recv,
                'packets_sent': net_io.packets_sent,
                'packets_recv': net_io.packets_recv,
                'errin': net_io.errin,
                'errout': net_io.errout,
                'dropin': net_io.dropin,
                'dropout': net_io.dropout,
            },
            'connections_count': connections,
        }
    
    def collect_process_metrics(self):
        """프로세스 메트릭 수집"""
        processes = []
        
        for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
            try:
                proc_info = proc.info
                if proc_info['name'] in ['python', 'gunicorn', 'uwsgi', 'celery']:
                    processes.append({
                        'pid': proc_info['pid'],
                        'name': proc_info['name'],
                        'cpu_percent': proc_info['cpu_percent'],
                        'memory_percent': proc_info['memory_percent'],
                    })
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                continue
        
        return processes
    
    def collect_all_metrics(self):
        """모든 메트릭 수집"""
        metrics = {
            'timestamp': datetime.now().isoformat(),
            'hostname': platform.node(),
            'cpu': self.collect_cpu_metrics(),
            'memory': self.collect_memory_metrics(),
            'disk': self.collect_disk_metrics(),
            'network': self.collect_network_metrics(),
            'processes': self.collect_process_metrics(),
        }
        
        # 임계값 검사 및 알림
        self.check_thresholds(metrics)
        
        return metrics
    
    def check_thresholds(self, metrics):
        """임계값 검사"""
        # CPU 사용률 검사
        if metrics['cpu']['cpu_percent_total'] > 80:
            logger.warning(f"High CPU usage: {metrics['cpu']['cpu_percent_total']:.1f}%")
        
        # 메모리 사용률 검사
        if metrics['memory']['memory']['percent'] > 80:
            logger.warning(f"High memory usage: {metrics['memory']['memory']['percent']:.1f}%")
        
        # 디스크 사용률 검사
        if metrics['disk']['root_disk']['percent'] > 80:
            logger.warning(f"High disk usage: {metrics['disk']['root_disk']['percent']:.1f}%")
        
        # 로드 애버리지 검사
        cpu_count = metrics['cpu']['cpu_count']
        load_1min = metrics['cpu']['load_average']['1min']
        if load_1min > cpu_count * 0.8:
            logger.warning(f"High load average: {load_1min:.2f} (CPU count: {cpu_count})")

5. 알림 시스템

# monitoring/alerts.py
import smtplib
import json
import requests
from email.mime.text import MimeText
from email.mime.multipart import MimeMultipart
from django.conf import settings
import logging

logger = logging.getLogger('alerts')

class AlertManager:
    """알림 관리자"""
    
    def __init__(self):
        self.email_config = getattr(settings, 'ALERT_EMAIL_CONFIG', {})
        self.slack_webhook = getattr(settings, 'SLACK_WEBHOOK_URL', None)
        self.discord_webhook = getattr(settings, 'DISCORD_WEBHOOK_URL', None)
    
    def send_email_alert(self, subject, message, recipients=None):
        """이메일 알림 발송"""
        if not self.email_config or not recipients:
            return False
        
        try:
            msg = MimeMultipart()
            msg['From'] = self.email_config['from_email']
            msg['To'] = ', '.join(recipients)
            msg['Subject'] = f"[Django Alert] {subject}"
            
            msg.attach(MimeText(message, 'plain'))
            
            server = smtplib.SMTP(self.email_config['smtp_host'], 
                                 self.email_config['smtp_port'])
            server.starttls()
            server.login(self.email_config['username'], 
                        self.email_config['password'])
            
            text = msg.as_string()
            server.sendmail(self.email_config['from_email'], recipients, text)
            server.quit()
            
            logger.info(f"Email alert sent: {subject}")
            return True
            
        except Exception as e:
            logger.error(f"Failed to send email alert: {e}")
            return False
    
    def send_slack_alert(self, message, channel=None):
        """Slack 알림 발송"""
        if not self.slack_webhook:
            return False
        
        try:
            payload = {
                'text': f"🚨 Django Alert: {message}",
                'username': 'Django Monitor',
                'icon_emoji': ':warning:',
            }
            
            if channel:
                payload['channel'] = channel
            
            response = requests.post(self.slack_webhook, 
                                   data=json.dumps(payload),
                                   headers={'Content-Type': 'application/json'})
            
            if response.status_code == 200:
                logger.info(f"Slack alert sent: {message}")
                return True
            else:
                logger.error(f"Failed to send Slack alert: {response.status_code}")
                return False
                
        except Exception as e:
            logger.error(f"Failed to send Slack alert: {e}")
            return False
    
    def send_discord_alert(self, message):
        """Discord 알림 발송"""
        if not self.discord_webhook:
            return False
        
        try:
            payload = {
                'content': f"🚨 **Django Alert**\n{message}",
                'username': 'Django Monitor',
            }
            
            response = requests.post(self.discord_webhook, 
                                   data=json.dumps(payload),
                                   headers={'Content-Type': 'application/json'})
            
            if response.status_code == 204:
                logger.info(f"Discord alert sent: {message}")
                return True
            else:
                logger.error(f"Failed to send Discord alert: {response.status_code}")
                return False
                
        except Exception as e:
            logger.error(f"Failed to send Discord alert: {e}")
            return False
    
    def send_alert(self, level, subject, message, channels=None):
        """통합 알림 발송"""
        if not channels:
            channels = ['email', 'slack']
        
        success_count = 0
        
        if 'email' in channels and level in ['critical', 'error']:
            recipients = getattr(settings, 'ALERT_EMAIL_RECIPIENTS', [])
            if self.send_email_alert(subject, message, recipients):
                success_count += 1
        
        if 'slack' in channels:
            if self.send_slack_alert(f"{subject}\n{message}"):
                success_count += 1
        
        if 'discord' in channels:
            if self.send_discord_alert(f"**{subject}**\n{message}"):
                success_count += 1
        
        return success_count > 0

# monitoring/tasks.py (Celery 태스크)
from celery import shared_task
from .database import DatabaseMonitor
from .redis_monitor import RedisMonitor
from .system_metrics import SystemMetricsCollector
from .alerts import AlertManager

@shared_task
def collect_system_metrics():
    """시스템 메트릭 수집 태스크"""
    collector = SystemMetricsCollector()
    metrics = collector.collect_all_metrics()
    
    # 메트릭을 Redis에 저장
    from django.core.cache import cache
    cache_key = f"system_metrics:{int(time.time() // 60)}"
    cache.set(cache_key, metrics, 300)  # 5분 캐시
    
    return metrics

@shared_task
def monitor_database_performance():
    """데이터베이스 성능 모니터링 태스크"""
    monitor = DatabaseMonitor()
    stats = monitor.analyze_query_performance()
    
    alert_manager = AlertManager()
    
    # 느린 쿼리 알림
    if stats['slow_queries']:
        message = f"Detected {len(stats['slow_queries'])} slow queries"
        alert_manager.send_alert('warning', 'Slow Queries Detected', message)
    
    # 데드락 알림
    if stats['locks']:
        message = f"Detected {len(stats['locks'])} database locks"
        alert_manager.send_alert('critical', 'Database Locks Detected', message)
    
    return stats

@shared_task
def monitor_redis_performance():
    """Redis 성능 모니터링 태스크"""
    monitor = RedisMonitor()
    info = monitor.monitor_performance()
    
    return info

6. 모니터링 대시보드

# views/monitoring.py
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.contrib.admin.views.decorators import staff_member_required
from django.core.cache import cache
import json

@staff_member_required
def monitoring_dashboard(request):
    """모니터링 대시보드 API"""
    if request.method == 'GET':
        return render(request, 'monitoring/dashboard.html')
    
    # AJAX 요청 처리
    metric_type = request.GET.get('type', 'all')
    
    if metric_type == 'system':
        return JsonResponse(get_system_metrics())
    elif metric_type == 'database':
        return JsonResponse(get_database_metrics())
    elif metric_type == 'redis':
        return JsonResponse(get_redis_metrics())
    elif metric_type == 'application':
        return JsonResponse(get_application_metrics())
    else:
        return JsonResponse({
            'system': get_system_metrics(),
            'database': get_database_metrics(),
            'redis': get_redis_metrics(),
            'application': get_application_metrics(),
        })

def get_system_metrics():
    """시스템 메트릭 조회"""
    collector = SystemMetricsCollector()
    return collector.collect_all_metrics()

def get_database_metrics():
    """데이터베이스 메트릭 조회"""
    monitor = DatabaseMonitor()
    return monitor.analyze_query_performance()

def get_redis_metrics():
    """Redis 메트릭 조회"""
    monitor = RedisMonitor()
    return monitor.get_info()

def get_application_metrics():
    """애플리케이션 메트릭 조회"""
    # 최근 10분간의 메트릭 조회
    current_time = int(time.time() // 60)
    metrics = []
    
    for i in range(10):
        cache_key = f"metrics:{current_time - i}"
        metric = cache.get(cache_key)
        if metric:
            metric['timestamp'] = current_time - i
            metrics.append(metric)
    
    return {
        'recent_metrics': metrics,
        'total_requests': sum(m.get('request_count', 0) for m in metrics),
        'avg_response_time': sum(m.get('total_duration', 0) for m in metrics) / max(sum(m.get('request_count', 0) for m in metrics), 1),
        'slow_requests': sum(m.get('slow_requests', 0) for m in metrics),
    }

7. 설정 예시

# settings/monitoring.py

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
        'json': {
            'format': '{"level": "%(levelname)s", "time": "%(asctime)s", "module": "%(module)s", "message": "%(message)s"}',
        },
    },
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': '/var/log/django/django.log',
            'maxBytes': 50 * 1024 * 1024,  # 50MB
            'backupCount': 5,
            'formatter': 'verbose',
        },
        'performance': {
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': '/var/log/django/performance.log',
            'maxBytes': 100 * 1024 * 1024,  # 100MB
            'backupCount': 10,
            'formatter': 'json',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'INFO',
            'propagate': True,
        },
        'performance': {
            'handlers': ['performance'],
            'level': 'INFO',
            'propagate': False,
        },
        'db_monitor': {
            'handlers': ['file'],
            'level': 'WARNING',
            'propagate': False,
        },
    },
}

# 알림 설정
ALERT_EMAIL_CONFIG = {
    'smtp_host': 'smtp.gmail.com',
    'smtp_port': 587,
    'username': 'your-email@gmail.com',
    'password': 'your-app-password',
    'from_email': 'alerts@yourcompany.com',
}

ALERT_EMAIL_RECIPIENTS = [
    'admin@yourcompany.com',
    'devops@yourcompany.com',
]

SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'
DISCORD_WEBHOOK_URL = 'https://discord.com/api/webhooks/YOUR/DISCORD/WEBHOOK'

# Celery 모니터링 태스크 스케줄
CELERY_BEAT_SCHEDULE = {
    'collect-system-metrics': {
        'task': 'monitoring.tasks.collect_system_metrics',
        'schedule': 60.0,  # 1분마다
    },
    'monitor-database': {
        'task': 'monitoring.tasks.monitor_database_performance',
        'schedule': 300.0,  # 5분마다
    },
    'monitor-redis': {
        'task': 'monitoring.tasks.monitor_redis_performance',
        'schedule': 180.0,  # 3분마다
    },
}

6. 실전 사례 및 모범 사례

이 섹션에서는 실제 대용량 트래픽을 처리하는 Django 서비스의 사례와 검증된 모범 사례들을 소개합니다.

1. 실제 사례: 대형 이커머스 플랫폼

사례 개요

  • 일일 활성 사용자: 100만명
  • 일일 주문 건수: 5만건
  • 피크 시간 동시 접속자: 10만명
  • 데이터베이스 크기: 5TB+

아키텍처 구성

# 실제 적용된 설정 예시
# settings/production.py

# 1. ASGI 서버 구성
ASGI_APPLICATION = 'myapp.asgi.application'

# Daphne 설정 (docker-compose.yml에서)
"""
services:
  web:
    image: myapp:latest
    command: daphne -b 0.0.0.0 -p 8000 --access-log - myapp.asgi:application
    deploy:
      replicas: 12
      resources:
        limits:
          cpus: '2'
          memory: 4G
    environment:
      - DJANGO_SETTINGS_MODULE=myapp.settings.production
"""

# 2. 데이터베이스 설정
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'ecommerce_main',
        'USER': 'django_user',
        'PASSWORD': os.environ['DB_PASSWORD'],
        'HOST': 'postgresql-master.internal',
        'PORT': '5432',
        'OPTIONS': {
            'MAX_CONNS': 20,
            'CONN_MAX_AGE': 300,
        },
        'TEST': {
            'NAME': 'test_ecommerce',
        }
    },
    'replica': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'ecommerce_main',
        'USER': 'django_readonly',
        'PASSWORD': os.environ['DB_READONLY_PASSWORD'],
        'HOST': 'postgresql-replica.internal',
        'PORT': '5432',
        'OPTIONS': {
            'MAX_CONNS': 15,
            'CONN_MAX_AGE': 600,
        }
    },
    'analytics': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'ecommerce_analytics',
        'USER': 'analytics_user',
        'PASSWORD': os.environ['ANALYTICS_DB_PASSWORD'],
        'HOST': 'postgresql-analytics.internal',
        'PORT': '5432',
        'OPTIONS': {
            'MAX_CONNS': 10,
        }
    }
}

# 3. 캐시 클러스터 설정
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': [
            'redis://redis-cluster-1.internal:6379/1',
            'redis://redis-cluster-2.internal:6379/1',
            'redis://redis-cluster-3.internal:6379/1',
        ],
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.ShardClient',
            'CONNECTION_POOL_KWARGS': {
                'max_connections': 50,
                'retry_on_timeout': True,
            }
        }
    },
    'sessions': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://redis-sessions.internal:6379/2',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            'CONNECTION_POOL_KWARGS': {
                'max_connections': 100,
            }
        }
    }
}

# 4. Celery 설정
CELERY_BROKER_URL = 'redis://redis-celery.internal:6379/0'
CELERY_RESULT_BACKEND = 'redis://redis-celery.internal:6379/0'

CELERY_TASK_ROUTES = {
    'orders.tasks.process_payment': {'queue': 'high_priority'},
    'orders.tasks.send_confirmation_email': {'queue': 'email'},
    'analytics.tasks.update_metrics': {'queue': 'analytics'},
    'inventory.tasks.update_stock': {'queue': 'inventory'},
}

CELERY_WORKER_CONCURRENCY = 8
CELERY_WORKER_PREFETCH_MULTIPLIER = 2
CELERY_TASK_ACKS_LATE = True
CELERY_WORKER_MAX_TASKS_PER_CHILD = 1000

핵심 최적화 전략

# 1. 스마트 캐싱 전략
# models/product.py
class Product(models.Model):
    name = models.CharField(max_length=255)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.PositiveIntegerField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    @cached_property
    def cache_key(self):
        return f"product:{self.id}:{self.updated_at.timestamp()}"
    
    def get_cached_data(self):
        """캐시된 상품 데이터 조회"""
        cached_data = cache.get(self.cache_key)
        if cached_data is None:
            cached_data = {
                'id': self.id,
                'name': self.name,
                'price': str(self.price),
                'stock': self.stock,
                'category_name': self.category.name,
                'image_urls': list(self.images.values_list('url', flat=True)),
                'average_rating': self.reviews.aggregate(
                    avg_rating=models.Avg('rating')
                )['avg_rating'] or 0,
                'review_count': self.reviews.count(),
            }
            # 상품 데이터는 6시간 캐시
            cache.set(self.cache_key, cached_data, 21600)
        return cached_data
    
    def invalidate_cache(self):
        """캐시 무효화"""
        cache.delete(self.cache_key)
        # 관련 카테고리 캐시도 무효화
        cache.delete(f"category_products:{self.category.id}")

# 2. 주문 처리 최적화
# services/order_service.py
class OrderService:
    """주문 처리 서비스"""
    
    @transaction.atomic
    def create_order(self, user, cart_items):
        """주문 생성"""
        # 1. 재고 확인 및 예약
        self._reserve_inventory(cart_items)
        
        # 2. 주문 생성
        order = Order.objects.create(
            user=user,
            status='PENDING',
            total_amount=self._calculate_total(cart_items)
        )
        
        # 3. 주문 항목 생성
        order_items = []
        for item in cart_items:
            order_items.append(OrderItem(
                order=order,
                product_id=item['product_id'],
                quantity=item['quantity'],
                price=item['price']
            ))
        OrderItem.objects.bulk_create(order_items)
        
        # 4. 비동기 후처리 작업
        from orders.tasks import process_order_async
        process_order_async.delay(order.id)
        
        return order
    
    def _reserve_inventory(self, cart_items):
        """재고 예약 (낙관적 잠금 사용)"""
        for item in cart_items:
            product = Product.objects.select_for_update().get(
                id=item['product_id']
            )
            
            if product.stock < item['quantity']:
                raise ValueError(f"Insufficient stock for {product.name}")
            
            # 재고 차감
            product.stock -= item['quantity']
            product.save(update_fields=['stock'])
            
            # 캐시 무효화
            product.invalidate_cache()

# 3. 검색 최적화
# services/search_service.py
class SearchService:
    """검색 서비스"""
    
    def search_products(self, query, filters=None, page=1, per_page=20):
        """상품 검색"""
        cache_key = self._get_search_cache_key(query, filters, page, per_page)
        
        cached_result = cache.get(cache_key)
        if cached_result:
            return cached_result
        
        # Elasticsearch 또는 PostgreSQL 풀텍스트 검색
        if hasattr(settings, 'ELASTICSEARCH_DSL'):
            results = self._elasticsearch_search(query, filters, page, per_page)
        else:
            results = self._postgresql_search(query, filters, page, per_page)
        
        # 15분 캐시
        cache.set(cache_key, results, 900)
        return results
    
    def _postgresql_search(self, query, filters, page, per_page):
        """PostgreSQL 풀텍스트 검색"""
        from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank
        
        search_vector = SearchVector('name', weight='A') + SearchVector('description', weight='B')
        search_query = SearchQuery(query)
        
        queryset = Product.objects.annotate(
            search=search_vector,
            rank=SearchRank(search_vector, search_query)
        ).filter(search=search_query)
        
        # 필터 적용
        if filters:
            if filters.get('category_id'):
                queryset = queryset.filter(category_id=filters['category_id'])
            if filters.get('price_min'):
                queryset = queryset.filter(price__gte=filters['price_min'])
            if filters.get('price_max'):
                queryset = queryset.filter(price__lte=filters['price_max'])
        
        # 정렬 및 페이징
        queryset = queryset.order_by('-rank', '-created_at')
        
        offset = (page - 1) * per_page
        products = list(queryset[offset:offset + per_page])
        total_count = queryset.count()
        
        return {
            'products': [product.get_cached_data() for product in products],
            'total_count': total_count,
            'page': page,
            'per_page': per_page,
            'total_pages': (total_count + per_page - 1) // per_page,
        }

2. 성능 개선 사례

Before/After 비교

# Before: 비효율적인 코드
def get_user_orders_bad(user_id):
    """비효율적인 주문 조회"""
    user = User.objects.get(id=user_id)  # N+1 문제
    orders = []
    
    for order in user.orders.all():  # 추가 쿼리
        order_data = {
            'id': order.id,
            'created_at': order.created_at,
            'total_amount': order.total_amount,
            'items': []
        }
        
        for item in order.items.all():  # N+1 문제
            product = item.product  # 추가 쿼리
            order_data['items'].append({
                'product_name': product.name,  # 추가 쿼리
                'quantity': item.quantity,
                'price': item.price,
            })
        
        orders.append(order_data)
    
    return orders

# After: 최적화된 코드
def get_user_orders_optimized(user_id):
    """최적화된 주문 조회"""
    # 캐시 확인
    cache_key = f"user_orders:{user_id}"
    cached_orders = cache.get(cache_key)
    if cached_orders:
        return cached_orders
    
    # 최적화된 쿼리 (한 번에 모든 데이터 조회)
    orders = Order.objects.filter(user_id=user_id).select_related('user').prefetch_related(
        'items__product'
    ).order_by('-created_at')
    
    # 데이터 변환
    orders_data = []
    for order in orders:
        order_data = {
            'id': order.id,
            'created_at': order.created_at.isoformat(),
            'total_amount': str(order.total_amount),
            'items': [
                {
                    'product_name': item.product.name,
                    'quantity': item.quantity,
                    'price': str(item.price),
                }
                for item in order.items.all()
            ]
        }
        orders_data.append(order_data)
    
    # 캐시 저장 (1시간)
    cache.set(cache_key, orders_data, 3600)
    return orders_data

# 성능 비교 결과:
# Before: 평균 2.5초, 150+ 쿼리
# After: 평균 0.1초, 2 쿼리

3. 모범 사례 체크리스트

개발 단계

# 코드 리뷰 체크리스트
PERFORMANCE_CHECKLIST = {
    'database': [
        'select_related() 또는 prefetch_related() 사용 확인',
        'bulk_create(), bulk_update() 활용',
        'only(), defer() 필요한 필드만 조회',
        'exists() vs count() 적절한 사용',
        'iterator() 대용량 데이터 처리',
        'F() 표현식으로 데이터베이스 레벨 연산',
        '인덱스 추가 검토',
        'explain() 쿼리 실행 계획 확인'
    ],
    'caching': [
        'view 레벨 캐싱 적용',
        'template fragment 캐싱',
        'low-level 캐시 API 활용',
        '캐시 TTL 설정 검토',
        '캐시 무효화 전략 구현',
        'cache_page 데코레이터 활용',
        'Vary 헤더 설정'
    ],
    'async': [
        '무거운 작업 Celery 태스크로 분리',
        '이메일/SMS 비동기 처리',
        '이미지 처리 백그라운드 작업',
        'API 호출 비동기 처리',
        '데이터 분석 배치 작업'
    ],
    'security': [
        'SQL injection 방지',
        'CSRF 토큰 검증',
        'XSS 방지',
        '인증/인가 로직 검증',
        '민감한 데이터 암호화',
        'rate limiting 적용'
    ]
}

배포 단계

# 배포 전 성능 검증 스크립트
#!/bin/bash

echo "=== Django Performance Check ==="

# 1. 데이터베이스 연결 확인
echo "Checking database connections..."
python manage.py dbshell -c "SELECT COUNT(*) FROM pg_stat_activity;"

# 2. 캐시 연결 확인
echo "Checking cache connections..."
python manage.py shell -c "
from django.core.cache import cache
cache.set('test', 'ok', 10)
print('Cache test:', cache.get('test'))
"

# 3. Celery 워커 상태 확인
echo "Checking Celery workers..."
celery -A myapp inspect active

# 4. 정적 파일 수집
echo "Collecting static files..."
python manage.py collectstatic --noinput

# 5. 데이터베이스 마이그레이션 확인
echo "Checking migrations..."
python manage.py showmigrations --plan

# 6. 시스템 리소스 확인
echo "Checking system resources..."
free -h
df -h

운영 단계

# 운영 모니터링 대시보드
# management/commands/health_check.py
from django.core.management.base import BaseCommand
from django.db import connection
from django.core.cache import cache
import redis
import time

class Command(BaseCommand):
    help = 'Comprehensive health check'
    
    def handle(self, *args, **options):
        health_status = {
            'database': self.check_database(),
            'cache': self.check_cache(),
            'celery': self.check_celery(),
            'disk_space': self.check_disk_space(),
            'memory': self.check_memory(),
        }
        
        overall_status = all(health_status.values())
        
        self.stdout.write(f"Overall Health: {'✅ HEALTHY' if overall_status else '❌ UNHEALTHY'}")
        
        for component, status in health_status.items():
            icon = '✅' if status else '❌'
            self.stdout.write(f"{icon} {component.upper()}: {'OK' if status else 'FAIL'}")
        
        if not overall_status:
            exit(1)
    
    def check_database(self):
        try:
            with connection.cursor() as cursor:
                cursor.execute("SELECT 1")
                return True
        except Exception as e:
            self.stderr.write(f"Database check failed: {e}")
            return False
    
    def check_cache(self):
        try:
            cache.set('health_check', 'ok', 10)
            return cache.get('health_check') == 'ok'
        except Exception as e:
            self.stderr.write(f"Cache check failed: {e}")
            return False
    
    def check_celery(self):
        try:
            from celery import current_app
            inspect = current_app.control.inspect()
            active_workers = inspect.active()
            return bool(active_workers)
        except Exception as e:
            self.stderr.write(f"Celery check failed: {e}")
            return False
    
    def check_disk_space(self):
        import shutil
        try:
            total, used, free = shutil.disk_usage('/')
            free_percent = free / total * 100
            return free_percent > 10  # 10% 이상 여유 공간
        except Exception as e:
            self.stderr.write(f"Disk check failed: {e}")
            return False
    
    def check_memory(self):
        try:
            import psutil
            memory = psutil.virtual_memory()
            return memory.percent < 90  # 90% 미만 사용률
        except Exception as e:
            self.stderr.write(f"Memory check failed: {e}")
            return False

4. 트러블슈팅 가이드

일반적인 성능 문제와 해결책

# 1. N+1 쿼리 문제 해결
# 문제: 리스트에서 관련 객체에 접근할 때 추가 쿼리 발생
def fix_n_plus_one():
    # ❌ 잘못된 방법
    posts = Post.objects.all()
    for post in posts:
        print(post.author.username)  # 각 post마다 author 쿼리 실행
    
    # ✅ 올바른 방법
    posts = Post.objects.select_related('author')
    for post in posts:
        print(post.author.username)  # 한 번의 JOIN 쿼리로 해결

# 2. 메모리 사용량 최적화
def optimize_memory_usage():
    # ❌ 큰 QuerySet을 메모리에 로드
    all_users = list(User.objects.all())
    
    # ✅ iterator() 사용으로 메모리 절약
    for user in User.objects.iterator(chunk_size=1000):
        process_user(user)

# 3. 캐시 미스 최소화
class OptimizedProductView:
    def get_product_data(self, product_id):
        # 다단계 캐시 전략
        
        # L1: 인메모리 캐시 (가장 빠름)
        cache_key = f"product:{product_id}"
        data = self.local_cache.get(cache_key)
        if data:
            return data
        
        # L2: Redis 캐시
        data = cache.get(cache_key)
        if data:
            self.local_cache.set(cache_key, data, 300)  # 5분 로컬 캐시
            return data
        
        # L3: 데이터베이스
        product = Product.objects.select_related('category').get(id=product_id)
        data = product.get_cached_data()
        
        # 캐시에 저장
        cache.set(cache_key, data, 3600)  # 1시간 Redis 캐시
        self.local_cache.set(cache_key, data, 300)  # 5분 로컬 캐시
        
        return data

5. 성능 측정 및 벤치마킹

# performance/benchmarks.py
import time
import statistics
from django.test import TestCase, TransactionTestCase
from django.test.utils import override_settings
import concurrent.futures

class PerformanceBenchmark:
    """성능 벤치마킹 도구"""
    
    def __init__(self, iterations=100):
        self.iterations = iterations
        self.results = []
    
    def benchmark(self, func, *args, **kwargs):
        """함수 성능 측정"""
        times = []
        
        for _ in range(self.iterations):
            start_time = time.time()
            try:
                result = func(*args, **kwargs)
                success = True
            except Exception as e:
                result = None
                success = False
                print(f"Error in benchmark: {e}")
            
            end_time = time.time()
            times.append(end_time - start_time)
        
        stats = {
            'iterations': self.iterations,
            'mean': statistics.mean(times),
            'median': statistics.median(times),
            'min': min(times),
            'max': max(times),
            'std_dev': statistics.stdev(times) if len(times) > 1 else 0,
        }
        
        return stats
    
    def stress_test(self, func, concurrent_users=10, duration=60):
        """스트레스 테스트"""
        start_time = time.time()
        results = []
        
        def worker():
            worker_results = []
            while time.time() - start_time < duration:
                try:
                    start = time.time()
                    func()
                    end = time.time()
                    worker_results.append(end - start)
                except Exception as e:
                    worker_results.append(None)
            return worker_results
        
        with concurrent.futures.ThreadPoolExecutor(max_workers=concurrent_users) as executor:
            futures = [executor.submit(worker) for _ in range(concurrent_users)]
            
            for future in concurrent.futures.as_completed(futures):
                results.extend(future.result())
        
        successful_requests = [r for r in results if r is not None]
        failed_requests = len([r for r in results if r is None])
        
        return {
            'total_requests': len(results),
            'successful_requests': len(successful_requests),
            'failed_requests': failed_requests,
            'success_rate': len(successful_requests) / len(results) * 100,
            'avg_response_time': statistics.mean(successful_requests) if successful_requests else 0,
            'requests_per_second': len(successful_requests) / duration,
        }

# 사용 예시
benchmark = PerformanceBenchmark()

# API 엔드포인트 성능 측정
def test_api_performance():
    from django.test import Client
    client = Client()
    
    def api_call():
        response = client.get('/api/products/')
        return response.status_code == 200
    
    stats = benchmark.benchmark(api_call)
    print(f"API Performance: {stats['mean']:.3f}s average")

# 데이터베이스 쿼리 성능 측정
def test_query_performance():
    def query_test():
        return list(Product.objects.select_related('category')[:100])
    
    stats = benchmark.benchmark(query_test)
    print(f"Query Performance: {stats['mean']:.3f}s average")

결론

Django에서 대용량 트래픽을 효과적으로 처리하기 위해서는 다음과 같은 핵심 원칙들을 기억해야 합니다:

핵심 원칙

  1. 측정 없이는 최적화 없다: 항상 성능을 측정하고 병목점을 파악한 후 최적화를 진행하세요.

  2. 점진적 개선: 한 번에 모든 것을 바꾸려 하지 말고, 가장 큰 impact를 가진 부분부터 차례대로 개선하세요.

  3. 캐싱은 필수: 적절한 캐싱 전략은 성능 향상의 가장 확실한 방법입니다.

  4. 데이터베이스 최적화: N+1 쿼리 문제 해결과 적절한 인덱싱은 기본 중의 기본입니다.

  5. 비동기 처리: 무거운 작업은 반드시 백그라운드에서 처리하세요.

  6. 모니터링과 알림: 문제가 발생하기 전에 미리 감지할 수 있는 시스템을 구축하세요.

마지막 체크리스트

🔍 개발 단계

  • Django Debug Toolbar로 쿼리 최적화 확인
  • select_related/prefetch_related 적용
  • 캐싱 전략 수립
  • 비동기 작업 분리
  • 코드 리뷰에서 성능 체크

🚀 배포 단계

  • 정적 파일 CDN 설정
  • Gzip 압축 활성화
  • ASGI 서버 설정
  • 로드 밸런서 구성
  • SSL 최적화

📊 운영 단계

  • 실시간 모니터링 시스템 운영
  • 정기적인 성능 테스트
  • 용량 계획 수립
  • 장애 대응 매뉴얼 작성
  • 백업 및 복구 전략 검증

Django는 적절한 최적화를 통해 충분히 대용량 트래픽을 처리할 수 있는 프레임워크입니다. 이 가이드에서 제시한 방법들을 단계적으로 적용하여 여러분의 서비스가 더 많은 사용자들에게 안정적으로 서비스를 제공할 수 있기를 바랍니다.

성능 최적화는 지속적인 과정입니다. 사용자가 늘어나고 서비스가 복잡해질수록 새로운 병목점이 나타날 수 있으므로, 항상 모니터링하고 개선해 나가는 자세가 중요합니다.

다음 섹션에서는 실전 사례 및 결론을 다루겠습니다.

이 글을 공유해보세요!