# Django가 파일을 “직접” 내려주지 말고 Nginx가 “대신” 내려주게 하자: X-Accel-Redirect로 다운로드 성능 끌어올리기 대부분의 Django 서비스는 보호된 파일(로그인 사용자만 다운로드 가능, 결제 후 열람 가능 등)을 내려줄 때 `FileResponse` 같은 방식으로 **Python 프로세스가 파일을 읽어서** 클라이언트에게 전달합니다. 트래픽이 작거나 내부 서버 간 통신이라면 이 방식도 충분히 괜찮습니다. 하지만 파일 요청이 폭증하면 이야기가 달라집니다. **애플리케이션 서버(Python)가 파일 배달 작업에 묶여** 원래 해야 할 일(권한 체크, 비즈니스 로직, API 처리)을 처리하기 어려워집니다. 이때 “권한 체크는 Django가 하고, 파일 전송은 Nginx가 하게” 위임하는 대표적인 기법이 **X-Accel-Redirect** 입니다. --- ## 왜 Python이 파일을 직접 보내면 병목이 생길까? {#sec-df5683b425c0} Django가 파일을 직접 내려주는 방식은 보통 이런 흐름입니다. 1. 요청 수신 2. 권한 체크 3. 디스크/스토리지에서 파일 읽기 4. 애플리케이션 프로세스가 네트워크로 전송(streaming) 문제는 **3~4번이 “무거운 일”** 이라는 점입니다. * 큰 파일일수록 전송 시간이 길어짐 * 동시 다운로드가 늘수록 워커/스레드/프로세스가 점점 잠김 * 결과적으로 API 응답 지연, 타임아웃, 서버 증설 압박으로 이어짐 반면 **Nginx는 정적 파일 전송에 최적화**되어 있고, 커널 레벨 최적화(`sendfile`), 효율적인 이벤트 루프, 버퍼링/레인지 요청 처리 등 “파일 배달”에 특화된 기능을 잘 활용합니다. --- ## X-Accel-Redirect의 핵심 아이디어 {#sec-8f39639ab234} **Django는 ‘검사만’ 하고, Nginx는 ‘전송만’ 한다.** ### 동작 원리 {#sec-acfe1fded112} 1. 클라이언트가 `/download/123` 같은 URL 요청 2. Django가 DB 조회/권한 체크만 수행 3. Django가 응답 헤더에 아래처럼 적어 **빈 바디로 반환** * `X-Accel-Redirect: /_protected/real/path/to/file.webp` 4. Nginx가 이 헤더를 보고 **내부적으로 파일을 찾아 클라이언트에게 직접 전송** * Django는 파일 내용을 직접 읽지 않음 즉, Django는 “이 사용자가 이 파일을 받아도 되는가?”만 책임지고, 실제 파일 전송은 Nginx에게 넘깁니다. --- ## 언제 이 방식이 특히 좋을까? {#sec-fc58c3d25d4f} 아래 상황일수록 효과가 큽니다. ### 1) 다운로드/이미지 요청이 많고 동시성이 높은 서비스 {#sec-00164dd4ae34} * 커뮤니티/메신저 이미지, 첨부파일, 리포트 PDF 다운로드 * “요청 수는 많고 로직은 단순한” 패턴일수록 X-Accel-Redirect가 빛납니다. ### 2) 파일 크기가 크거나 Range 요청이 중요한 서비스 {#sec-e39fd6155745} * 동영상/오디오/대용량 압축 파일 * 브라우저/플레이어가 `Range`(구간 요청)로 재생/이어받기 하는 경우 → Nginx가 이런 전송을 훨씬 안정적으로 처리합니다. ### 3) 앱 서버 비용을 낮추고 싶을 때 {#sec-c09b34610e6f} * Python 워커는 비싸고(메모리/CPU), 파일 전송에 묶이면 “돈이 새는” 구조가 됩니다. * 파일 전송을 프록시 계층으로 넘기면 **앱 서버는 로직 처리에 집중**할 수 있습니다. --- ## 반대로, 굳이 안 써도 되는 경우 {#sec-1357fdc58f72} * 내부 서버 간 통신이고 트래픽이 낮음 * 파일 요청이 적고 대부분이 API/DB 로직이 병목 * 파일이 로컬 디스크가 아니라 S3 같은 외부 오브젝트 스토리지이며, 이미 CDN/프리사인 URL로 잘 해결되는 구조 이런 경우는 `FileResponse`로도 운영상 충분합니다. “필요해질 때 도입”해도 늦지 않습니다. --- ## 구현 예시: Django + Nginx {#sec-7b7a69572111} ![웹 리퀘스트 처리 흐름도](/media/editor_temp/6/a1ad0374-979a-447e-b586-81ac02f2b447.png) ### Nginx 설정 예시 {#sec-f9441826ea70} 핵심은 `internal` 입니다. `internal`로 설정된 location은 **클라이언트가 직접 접근할 수 없고**, 오직 **X-Accel-Redirect 같은 내부 리다이렉트로만** 접근 가능합니다. ```nginx # 보호 파일을 실제로 서빙할 내부 엔드포인트 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 뷰 예시 {#sec-0bf0a74802d0} Django는 권한만 확인한 뒤, 파일 내용을 읽지 않고 헤더만 내려줍니다. ```python 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는 요청당 처리 시간이 매우 짧아지고, 워커가 파일 전송으로 잠기지 않습니다. --- ## 보안 체크리스트 {#sec-2fdde1f8ddfe} ### 1) 내부 경로는 반드시 “서버가 결정” {#sec-267595d99eac} * 클라이언트 입력으로 `/_protected/../../etc/passwd` 같은 경로가 만들어지지 않도록 * DB에 저장된 “안전한 상대 경로”만 사용하거나, 화이트리스트 기반으로 매핑하세요. ### 2) Nginx location은 꼭 `internal` {#sec-16c9962b5114} * `internal`이 없으면 사용자가 `/_protected/...`를 직접 때려서 우회 다운로드할 수 있습니다. ### 3) 권한 체크 로직은 Django에서만 신뢰 {#sec-019d34af62f6} * Nginx는 “전송 엔진” 역할이고, 접근 제어는 Django가 책임지는 구조가 안전합니다. --- ## 제3의 서비스를 이용 대안 {#sec-735de93cc272} 비용에 부담이 없다면, 애당초 파일을 제3의 스토리지에서 제공하도록 설계를 할 수도 있을 것입니다. 비용은 들지만 안정적이고 내 서버의 자원을 아낄 수 있겠지요. 프로젝트 팀의 여건에 맞게 선택을 하면 좋을 것입니다. * **CDN 캐시**: 공개 파일이라면 Nginx 이전에 CDN 캐시가 더 큰 효과 * **프리사인 URL(S3 등)**: 오브젝트 스토리지 기반이면 X-Accel-Redirect 대신 프리사인 URL이 더 단순한 경우도 많음 --- ## 마무리 {#sec-d2260ecd846a} 요컨대, 파일의 서빙은 웹애플리케이션에서 하는 것 보다, Nginx가 프록시 단계에서 하는 것이 확실히 성능이 좋습니다. **정적 전송에 최적화된 Nginx가 커널 최적화까지 활용하며 처리하게 만들기 때문**입니다. 그 결과, 앱 서버는 “권한 체크 + 비즈니스 로직”에 집중하고, 다운로드 트래픽이 늘어도 전체 시스템이 훨씬 잘 버팁니다. 트래픽이 크지 않다면 `FileResponse`는 여전히 깔끔하고 충분히 좋은 선택입니다. 다만 “파일 요청이 폭증할 때 앱 서버가 무너지는 패턴”은 매우 흔하고, 그때 가장 빠르게 효과를 보는 카드가 **X-Accel-Redirect** 입니다. 키워드 하나만 기억해두면 됩니다: **“권한은 Django, 전송은 Nginx”**. --- **관련글도 확인 해보세요!** - [nginx 로드밸런싱 실전 가이드](/ko/whitedec/2025/12/11/nginx-load-balancing-guide/) - [악성 봇은 못 멈춘다. 대신 앱 앞에서 잘라버립시다 - nginx 단계에서 이상한 URL 정리하기](/ko/whitedec/2025/12/5/malicious-bot-nginx-blackhole/) - [리버스 프록시란? 포워드 프록시와의 차이, 목적, 사용 시나리오 한눈에 정리](/ko/whitedec/2025/12/10/reverse-proxy-forward-proxy-differences/)