Django가 파일을 “직접” 내려주지 말고 Nginx가 “대신” 내려주게 하자: X-Accel-Redirect로 다운로드 성능 끌어올리기
대부분의 Django 서비스는 보호된 파일(로그인 사용자만 다운로드 가능, 결제 후 열람 가능 등)을 내려줄 때 FileResponse 같은 방식으로 Python 프로세스가 파일을 읽어서 클라이언트에게 전달합니다. 트래픽이 작거나 내부 서버 간 통신이라면 이 방식도 충분히 괜찮습니다.
하지만 파일 요청이 폭증하면 이야기가 달라집니다. 애플리케이션 서버(Python)가 파일 배달 작업에 묶여 원래 해야 할 일(권한 체크, 비즈니스 로직, API 처리)을 처리하기 어려워집니다. 이때 “권한 체크는 Django가 하고, 파일 전송은 Nginx가 하게” 위임하는 대표적인 기법이 X-Accel-Redirect 입니다.
왜 Python이 파일을 직접 보내면 병목이 생길까?
Django가 파일을 직접 내려주는 방식은 보통 이런 흐름입니다.
- 요청 수신
- 권한 체크
- 디스크/스토리지에서 파일 읽기
- 애플리케이션 프로세스가 네트워크로 전송(streaming)
문제는 3~4번이 “무거운 일” 이라는 점입니다.
- 큰 파일일수록 전송 시간이 길어짐
- 동시 다운로드가 늘수록 워커/스레드/프로세스가 점점 잠김
- 결과적으로 API 응답 지연, 타임아웃, 서버 증설 압박으로 이어짐
반면 Nginx는 정적 파일 전송에 최적화되어 있고, 커널 레벨 최적화(sendfile), 효율적인 이벤트 루프, 버퍼링/레인지 요청 처리 등 “파일 배달”에 특화된 기능을 잘 활용합니다.
X-Accel-Redirect의 핵심 아이디어
Django는 ‘검사만’ 하고, Nginx는 ‘전송만’ 한다.
동작 원리
- 클라이언트가
/download/123같은 URL 요청 - Django가 DB 조회/권한 체크만 수행
- Django가 응답 헤더에 아래처럼 적어 빈 바디로 반환
-
X-Accel-Redirect: /_protected/real/path/to/file.webp4. Nginx가 이 헤더를 보고 내부적으로 파일을 찾아 클라이언트에게 직접 전송 -
Django는 파일 내용을 직접 읽지 않음
즉, Django는 “이 사용자가 이 파일을 받아도 되는가?”만 책임지고, 실제 파일 전송은 Nginx에게 넘깁니다.
언제 이 방식이 특히 좋을까?
아래 상황일수록 효과가 큽니다.
1) 다운로드/이미지 요청이 많고 동시성이 높은 서비스
- 커뮤니티/메신저 이미지, 첨부파일, 리포트 PDF 다운로드
- “요청 수는 많고 로직은 단순한” 패턴일수록 X-Accel-Redirect가 빛납니다.
2) 파일 크기가 크거나 Range 요청이 중요한 서비스
- 동영상/오디오/대용량 압축 파일
- 브라우저/플레이어가
Range(구간 요청)로 재생/이어받기 하는 경우 → Nginx가 이런 전송을 훨씬 안정적으로 처리합니다.
3) 앱 서버 비용을 낮추고 싶을 때
- Python 워커는 비싸고(메모리/CPU), 파일 전송에 묶이면 “돈이 새는” 구조가 됩니다.
- 파일 전송을 프록시 계층으로 넘기면 앱 서버는 로직 처리에 집중할 수 있습니다.
반대로, 굳이 안 써도 되는 경우
- 내부 서버 간 통신이고 트래픽이 낮음
- 파일 요청이 적고 대부분이 API/DB 로직이 병목
- 파일이 로컬 디스크가 아니라 S3 같은 외부 오브젝트 스토리지이며, 이미 CDN/프리사인 URL로 잘 해결되는 구조
이런 경우는 FileResponse로도 운영상 충분합니다. “필요해질 때 도입”해도 늦지 않습니다.
구현 예시: Django + Nginx

Nginx 설정 예시
핵심은 internal 입니다.
internal로 설정된 location은 클라이언트가 직접 접근할 수 없고, 오직 X-Accel-Redirect 같은 내부 리다이렉트로만 접근 가능합니다.
# 보호 파일을 실제로 서빙할 내부 엔드포인트
location /_protected/ {
internal;
# 실제 파일이 있는 디렉터리
alias /var/app/protected_media/;
# 성능 옵션(환경에 맞게)
sendfile on;
tcp_nopush on;
# 필요시 캐시/헤더 제어 가능
# add_header Cache-Control "private, max-age=0";
}
/var/app/protected_media/아래에 실제 파일이 있다고 가정- 외부에 노출되는 URL은
/download/...같은 Django 라우트 - 내부 전송 경로는
/_protected/...로 통일
Django 뷰 예시
Django는 권한만 확인한 뒤, 파일 내용을 읽지 않고 헤더만 내려줍니다.
from django.http import HttpResponse, Http404
from django.contrib.auth.decorators import login_required
from django.utils.encoding import iri_to_uri
@login_required
def download(request, file_id):
# 1) DB 조회 + 권한 체크
obj = get_file_object_or_404(file_id) # 예시
if not obj.can_download(request.user):
raise Http404
# 2) 내부 경로 구성 (Nginx의 /_protected/ 아래로 매핑)
internal_path = f"/_protected/{obj.storage_relpath}"
# 3) X-Accel-Redirect 헤더만 설정하고 바디는 비움
response = HttpResponse()
response["X-Accel-Redirect"] = iri_to_uri(internal_path)
# (선택) 다운로드 파일명/콘텐츠 타입 지정
response["Content-Type"] = obj.content_type or "application/octet-stream"
response["Content-Disposition"] = f'attachment; filename="{obj.download_name}"'
return response
포인트:
FileResponse(open(...))같은 파일 I/O가 없습니다.- Django는 요청당 처리 시간이 매우 짧아지고, 워커가 파일 전송으로 잠기지 않습니다.
보안 체크리스트
1) 내부 경로는 반드시 “서버가 결정”
- 클라이언트 입력으로
/_protected/../../etc/passwd같은 경로가 만들어지지 않도록 - DB에 저장된 “안전한 상대 경로”만 사용하거나, 화이트리스트 기반으로 매핑하세요.
2) Nginx location은 꼭 internal
internal이 없으면 사용자가/_protected/...를 직접 때려서 우회 다운로드할 수 있습니다.
3) 권한 체크 로직은 Django에서만 신뢰
- Nginx는 “전송 엔진” 역할이고, 접근 제어는 Django가 책임지는 구조가 안전합니다.
제3의 서비스를 이용 대안
비용에 부담이 없다면, 애당초 파일을 제3의 스토리지에서 제공하도록 설계를 할 수도 있을 것입니다. 비용은 들지만 안정적이고 내 서버의 자원을 아낄 수 있겠지요. 프로젝트 팀의 여건에 맞게 선택을 하면 좋을 것입니다.
- CDN 캐시: 공개 파일이라면 Nginx 이전에 CDN 캐시가 더 큰 효과
- 프리사인 URL(S3 등): 오브젝트 스토리지 기반이면 X-Accel-Redirect 대신 프리사인 URL이 더 단순한 경우도 많음
마무리
요컨대, 파일의 서빙은 웹애플리케이션에서 하는 것 보다, Nginx가 프록시 단계에서 하는 것이 확실히 성능이 좋습니다. 정적 전송에 최적화된 Nginx가 커널 최적화까지 활용하며 처리하게 만들기 때문입니다. 그 결과, 앱 서버는 “권한 체크 + 비즈니스 로직”에 집중하고, 다운로드 트래픽이 늘어도 전체 시스템이 훨씬 잘 버팁니다.
트래픽이 크지 않다면 FileResponse는 여전히 깔끔하고 충분히 좋은 선택입니다.
다만 “파일 요청이 폭증할 때 앱 서버가 무너지는 패턴”은 매우 흔하고, 그때 가장 빠르게 효과를 보는 카드가 X-Accel-Redirect 입니다.
키워드 하나만 기억해두면 됩니다: “권한은 Django, 전송은 Nginx”.
관련글도 확인 해보세요!