프로그래밍/Python

📤 Django 파일 업로드 처리 오류: 대용량 파일 타임아웃과 메모리 부족 해결하기

Tiboong 2025. 12. 15. 17:48

🎬 실제 업무 현장에서

어느 날, 동영상 교육 플랫폼을 개발하는 스타트업의 개발팀에 긴급 요청이 들어왔습니다.

"강사님들이 500MB 이상의 강의 영상을 올리려고 하면 계속 실패한다고 하는데요, 급한 촬영 일정이 있어서 빨리 해결해주셔야 합니다!"

간단할 줄 알았던 파일 업로드 기능이 실제 서비스에서는 악몽이 되어버렸습니다. 작은 이미지 파일은 잘 올라가는데, 동영상 파일만 업로드하면:

  • 업로드 중 화면이 멈춤 → 결국 타임아웃
  • 서버 메모리 사용량이 급증하다가 502 Bad Gateway
  • 운 좋게 업로드되어도 다른 사용자들 화면이 느려짐

이런 경험, 있으신가요?


🤔 왜 대용량 파일 업로드는 이렇게 어려운가?

파일 업로드의 작동 원리를 이해해야 합니다

Django의 기본 파일 업로드 처리 방식을 "물건 배달"에 비유해볼까요?

작은 패키지 배달 (소용량 파일)

  • 택배 기사가 물건을 들고 → 엘리베이터 타고 → 문 앞까지 직접 배달
  • 한 번에 처리 가능, 빠르고 간단함

대형 가구 배달 (대용량 파일)

  • 문제 1: 엘리베이터에 안 들어감 (메모리 제한)
  • 문제 2: 들고 계단 올라가면 너무 오래 걸림 (타임아웃)
  • 문제 3: 배달하는 동안 다른 고객 배달 못함 (동기 처리)

Django도 마찬가지입니다:

[클라이언트] → 500MB 파일 전송 시작
     ↓
[Django View] → 메모리에 전체 파일 로드 시도
     ↓
[메모리 부족] → 서버 다운 또는 타임아웃

숨어있는 함정들

1. FILE_UPLOAD_MAX_MEMORY_SIZE (메모리 함정)

# settings.py 기본값
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440  # 2.5MB

이 크기 이하 파일은 메모리에 전체를 올립니다. 작은 파일이야 괜찮지만, 여러 사용자가 동시에 올리면?

  • 사용자 10명이 각각 2MB 파일 업로드 = 20MB 메모리 사용
  • 괜찮아 보이지만, Django 프로세스가 여러 개면 × 4, × 8...
  • 순식간에 서버 메모리 포화

2. 웹 서버의 타임아웃 설정
Django만 고치면 될 것 같지만, 실제로는 레이어가 여러 개입니다:

[브라우저 타임아웃: 보통 없음]
       ↓
[Nginx 타임아웃: 60초]  ← 여기서 먼저 끊김!
       ↓
[Gunicorn 타임아웃: 30초]  ← 여기서도 끊김!
       ↓
[Django View]  ← 코드도 안 들어옴

 

3. 동기 처리의 위험성
500MB 파일 업로드 = 5분 소요
그 5분 동안:

  • 해당 워커(프로세스)는 다른 요청 처리 못함
  • 워커 4개 서버에서 4명이 동시 업로드하면?
  • 다른 사용자들은 화면 로딩도 안 됨

💡 문제의 근본 원인

1. 메모리 문제의 본질

전체 로딩 방식의 문제

# 이렇게 처리하면 안 됩니다
def bad_upload(request):
    uploaded_file = request.FILES['video']
    # 전체 파일을 메모리에 읽음
    content = uploaded_file.read()  # 500MB가 한 번에 메모리로!

    # 이제 뭔가 처리...
    process_video(content)

이건 마치 500페이지 책을 통째로 외우려는 것과 같습니다. 한 페이지씩 읽으면 되는데...

 

비즈니스 임팩트

  • 서버 메모리 부족으로 다른 기능까지 영향
  • 서비스 전체가 느려지거나 다운
  • 클라우드 환경에서는 메모리 증설 = 비용 증가
  • 스타트업에게는 치명적인 운영 비용 부담

2. 타임아웃 문제의 레이어

왜 타임아웃이 발생하는가?

업로드 단계별 시간:
1. 네트워크 전송: 500MB / 업로드 속도
   - 10Mbps 업로드 기준: 약 400초 (6분 40초)
   - 일반 가정 인터넷: 더 오래 걸림

2. 파일 처리: Django가 파일을 받아서 저장
   - 메모리 복사, 디스크 쓰기
   - 추가로 1-2분 소요 가능

총 소요 시간: 8-10분
하지만 Nginx 타임아웃: 60초

누가 먼저 포기하냐의 문제입니다.

 

실무 영향

  • 사용자는 업로드 실패 경험 → 서비스 이탈
  • 재시도 → 서버 부하만 가중
  • 고객 지원 문의 폭증
  • "업로드가 안 돼요" 리뷰 증가

🛠️ 해결 방법: 여러 접근법 비교

방법 1: 청크 업로드 구현 ⭐⭐⭐⭐⭐

핵심 아이디어
파일을 작은 조각으로 나눠서 여러 번에 걸쳐 업로드합니다. 마치 대형 가구를 분해해서 옮기는 것처럼.

 

어떻게 작동하는가?

500MB 파일을 10MB씩 나눔
↓
[1/50] 10MB 업로드 → 성공
[2/50] 10MB 업로드 → 성공
...
[50/50] 10MB 업로드 → 성공
↓
서버에서 50개 조각을 합쳐서 원본 복원

 

장점

  • 각 청크는 작아서 타임아웃 없음
  • 업로드 실패 시 실패한 조각만 재전송
  • 프로그레스 바 구현 가능 (사용자 경험 향상)
  • 메모리 사용량 예측 가능 (청크 크기만큼만)

단점

  • 프론트엔드도 구현 필요 (JavaScript)
  • 서버에서 청크 관리 로직 필요
  • 구현 복잡도 증가

언제 사용하나?

  • 사용자가 직접 파일 업로드하는 경우
  • 안정적인 업로드 경험이 중요한 경우
  • 대용량 파일이 자주 올라오는 서비스

실무 고려사항

  • 청크 크기: 5-10MB 권장 (너무 작으면 요청 횟수 증가)
  • 청크 저장: 임시 디렉토리 또는 Redis 활용
  • 세션 관리: 업로드 ID로 청크들 추적
  • 정리 작업: 완료되지 않은 청크 주기적 삭제

방법 2: Celery 비동기 처리 ⭐⭐⭐⭐

핵심 아이디어
파일 업로드 자체는 받되, 무거운 처리는 백그라운드에서 합니다.

 

프로세스 흐름

사용자 업로드 요청
↓
Django View: 파일 임시 저장 (빠르게)
↓
Celery Task 생성: "나중에 처리해줘"
↓
즉시 응답: "업로드 접수했어요, 처리 중입니다"
↓
Celery Worker: 천천히 처리 (인코딩, 변환, 검증 등)
↓
완료 후 사용자에게 알림

 

장점

  • 사용자는 빠른 응답 받음 (체감 속도 향상)
  • 서버 워커가 블로킹되지 않음
  • 무거운 작업도 여유있게 처리 가능
  • 에러 발생 시 재시도 가능

단점

  • Celery, Redis/RabbitMQ 추가 인프라 필요
  • 시스템 복잡도 증가
  • 디버깅 어려움 (비동기 흐름)
  • 즉시 결과 확인 불가 (polling 또는 웹소켓 필요)

언제 사용하나?

  • 업로드 후 추가 처리가 무거운 경우 (동영상 인코딩, 이미지 리사이징)
  • 실시간 피드백이 필수가 아닌 경우
  • 이미 Celery 인프라가 있는 경우

실무 주의사항

  • Task 타임아웃 설정: task_time_limit = 3600 (1시간)
  • 사용자 피드백: "처리 중" 상태 표시 필수
  • 에러 처리: 실패 시 사용자에게 알림 방법 마련

방법 3: Django 설정 최적화 ⭐⭐⭐

무엇을 조정하는가?

# settings.py
FILE_UPLOAD_MAX_MEMORY_SIZE = 10485760  # 10MB
# 이것보다 큰 파일은 디스크에 임시 저장

DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760  # 10MB
# 전체 요청 크기 제한

FILE_UPLOAD_HANDLERS = [
    'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]
# 처음부터 디스크에 저장

 

장점

  • 코드 변경 최소
  • 즉시 적용 가능
  • 메모리 사용량 제어

단점

  • 디스크 I/O 증가 (속도 저하)
  • 근본적 해결은 아님 (타임아웃 여전히 가능)
  • 임시 파일 정리 필요

언제 사용하나?

  • 빠른 임시 조치가 필요한 경우
  • 파일 크기가 예측 가능한 경우 (예: 최대 100MB)
  • 다른 방법과 병행

방법 4: 웹 서버 설정 조정 ⭐⭐⭐

Nginx 설정

# nginx.conf
client_max_body_size 500M;  # 업로드 가능 최대 크기
client_body_timeout 600s;   # 10분
proxy_read_timeout 600s;    # 10분

 

Gunicorn 설정

# gunicorn.conf.py
timeout = 600  # 10분
worker_class = 'sync'
workers = 4

 

장점

  • 타임아웃 에러 즉시 해결
  • 설정 파일만 수정

단점

  • 긴 타임아웃 = 워커 장시간 점유
  • 동시 처리 능력 감소
  • DoS 공격에 취약해질 수 있음

언제 사용하나?

  • 다른 방법과 병행 필수
  • 내부 시스템 (외부 공격 걱정 없음)
  • 사용자 수가 적은 경우

주의사항

  • 타임아웃을 무한정 늘리면 안 됨
  • 워커 수 × 타임아웃 = 최악의 대기 시간

방법 5: 클라우드 Direct Upload ⭐⭐⭐⭐⭐

핵심 아이디어
Django 서버를 거치지 않고 클라이언트가 직접 S3/GCS에 업로드합니다.

 

프로세스

1. 사용자: "파일 올릴게요"
2. Django: S3 Presigned URL 생성 (임시 업로드 권한)
3. 사용자 → S3로 직접 업로드 (Django 안 거침)
4. 업로드 완료 후 Django에 알림
5. Django: DB에 파일 정보 저장

 

장점

  • Django 서버 부하 제로
  • 타임아웃 걱정 없음
  • AWS/GCP의 강력한 인프라 활용
  • 전 세계 CDN 활용 가능

단점

  • 클라우드 스토리지 비용 발생
  • 보안 설정 신경써야 함
  • 구현 복잡도 높음

언제 사용하나?

  • 프로덕션 환경
  • 대용량 파일 많은 서비스
  • 글로벌 서비스

비용 고려

  • S3 저장: $0.023/GB/월
  • 전송: 무료 (업로드), $0.09/GB (다운로드)
  • 500GB 저장 시: 월 $11.5

🔍 디버깅 팁

1. 어디서 문제가 발생하는지 찾기

타임아웃인지 메모리 부족인지 구분

# views.py
import time
import tracemalloc
import logging

logger = logging.getLogger(__name__)

def upload_view(request):
    # 메모리 추적 시작
    tracemalloc.start()
    start_time = time.time()

    try:
        uploaded_file = request.FILES['file']
        logger.info(f"파일 크기: {uploaded_file.size / 1024 / 1024:.2f}MB")

        # 처리 로직
        handle_upload(uploaded_file)

        # 메모리 사용량 확인
        current, peak = tracemalloc.get_traced_memory()
        logger.info(f"현재 메모리: {current / 1024 / 1024:.2f}MB")
        logger.info(f"최대 메모리: {peak / 1024 / 1024:.2f}MB")

        elapsed = time.time() - start_time
        logger.info(f"처리 시간: {elapsed:.2f}초")

    except Exception as e:
        logger.error(f"업로드 실패: {str(e)}")
        logger.error(traceback.format_exc())
    finally:
        tracemalloc.stop()

 

로그 확인 포인트

  • 메모리가 급증했다면 → 메모리 문제
  • 특정 시간에 멈췄다면 → 타임아웃
  • 서버 로그가 없다면 → Nginx/Gunicorn에서 끊김

2. 네트워크 레벨 디버깅

브라우저 개발자 도구 활용

Network 탭 → 업로드 요청 확인
- Pending: 전송 중
- 502 Bad Gateway: 서버 다운 또는 메모리 부족
- 504 Gateway Timeout: Nginx 타임아웃
- 시간 확인: 정확히 60초? → Nginx 기본 타임아웃

3. 서버 리소스 모니터링

실시간 확인 명령어

# 메모리 사용량
watch -n 1 free -h

# 프로세스별 메모리
ps aux --sort=-%mem | head

# 업로드 진행 중인 요청 확인
netstat -an | grep :80 | grep ESTABLISHED

# 로그 실시간 확인
tail -f /var/log/nginx/error.log
tail -f /var/log/gunicorn/error.log

✅ 베스트 프랙티스 체크리스트

설계 단계

  • 예상 파일 크기 명확히 파악 (최대 몇 MB/GB?)
  • 동시 업로드 사용자 수 예측 (하루 몇 명? 피크 타임은?)
  • 업로드 후 처리 작업 파악 (단순 저장? 인코딩 필요?)
  • 예산 고려 (클라우드 vs 자체 서버)

개발 단계

  • 100MB 이상 파일은 청크 업로드 적용
  • 무거운 후처리는 Celery로 분리
  • FILE_UPLOAD_MAX_MEMORY_SIZE 적절히 설정
  • 프로그레스 바 구현 (사용자 경험)
  • 업로드 파일 타입/크기 검증
  • 에러 메시지 명확하게 (사용자가 이해 가능하게)

인프라 단계

  • Nginx client_max_body_size 설정
  • Nginx/Gunicorn timeout 충분히 설정
  • 임시 파일 디렉토리 용량 확보
  • 로그 레벨 적절히 설정 (디버깅 가능하게)
  • 모니터링 도구 설정 (메모리, CPU, 디스크)

운영 단계

  • 주기적인 임시 파일 정리 (cron job)
  • 업로드 실패 모니터링 (얼마나 실패하는지)
  • 디스크 공간 알림 설정
  • 사용자 피드백 수집 (업로드 경험)

🎯 결론: 파일 업로드는 전략이 필요합니다

대용량 파일 업로드는 단순히 "코드 몇 줄"의 문제가 아닙니다.

 

기억해야 할 핵심 원칙

  1. 작게 나누기: 청크 업로드로 메모리와 타임아웃 동시 해결
  2. 비동기 처리: 사용자 응답 속도와 서버 부하 분산
  3. 레이어별 설정: Django, Gunicorn, Nginx 모두 확인
  4. 직접 업로드: 프로덕션에서는 클라우드 Direct Upload 고려

서비스 규모별 추천

서비스 규모 추천 방법
MVP/초기 스타트업 Django 설정 최적화 + 웹 서버 타임아웃 조정
성장기 스타트업 청크 업로드 + Celery
중대형 서비스 S3 Direct Upload + 청크 업로드

 

실무에서 자주 하는 실수

❌ "일단 타임아웃만 늘려보자" → 근본 해결 아님
❌ "서버 메모리만 늘리면 되겠지" → 비용 폭탄
❌ "사용자가 많지 않으니까 괜찮아" → 갑자기 늘어나면?
✅ "작은 것부터, 확장 가능하게" → 정답


💼 Django 파일 업로드 최적화 컨설팅

27년 경력의 Django 전문가가 여러분의 파일 업로드 문제를 해결해드립니다.

이런 고민 있으신가요?

  • "업로드 기능은 만들었는데, 실제 서비스에서 계속 에러가 나요"
  • "사용자가 늘어나니까 서버가 버티질 못해요"
  • "청크 업로드를 구현하고 싶은데 어떻게 시작할지 모르겠어요"
  • "S3 Direct Upload 도입하고 싶은데 보안 설정이 걱정돼요"

컨설팅 내용

✅ 현재 시스템 진단 및 병목 지점 파악
✅ 서비스 규모에 맞는 최적 솔루션 제안
✅ 청크 업로드 구현 가이드
✅ Celery 비동기 처리 아키텍처 설계
✅ 클라우드 Direct Upload 설정
✅ 모니터링 및 알림 시스템 구축
✅ 성능 테스트 및 최적화

크몽에서 만나보세요

Django 백엔드 개발 컨설팅 - 크몽

📧 실무 경험을 바탕으로 한 실질적인 해결책을 제시합니다.
🚀 단순한 코드 작성이 아닌, 지속 가능한 시스템 설계를 도와드립니다.

 

📝 이 글이 도움이 되셨다면 공유 부탁드립니다!
💬 궁금한 점은 댓글로 남겨주세요.