FastAPI는 파일 다운로드를 구현하기 위한 여러 방법을 제공합니다. 특히 FileResponse와 StreamingResponse는 각각 다른 상황에 최적화된 기능을 제공합니다. 이 글에서는 두 방식의 차이점과 한글 파일명 문제 해결, 그리고 다양한 파일 타입별 다운로드 구현 방법을 알아보겠습니다.
FileResponse와 StreamingResponse의 차이점
FileResponse
FileResponse는 파일 시스템에 있는 파일을 직접 제공하는 데 최적화된 응답 클래스입니다.
from fastapi import FastAPI
from fastapi.responses import FileResponse
import os
app = FastAPI()
@app.get("/download/file")
async def download_file():
file_path = os.path.join(os.getcwd(), "files", "example.txt")
return FileResponse(file_path, filename="downloaded_example.txt")
장점:
- 파일 경로만 지정하면 되어 사용이 간단합니다.
- 메모리 효율적: 전체 파일을 메모리에 로드하지 않고 스트리밍합니다.
- filename, media_type 등 파일 다운로드에 필요한 헤더를 자동으로 설정합니다.
- 적절한 청크 크기로 대용량 파일도 효율적으로 전송합니다.
적합한 상황:
- 디스크에 저장된 정적 파일 제공
- 대용량 파일 다운로드
- 간단한 구현으로 파일 다운로드 제공 시
StreamingResponse
StreamingResponse는 바이트 스트림을 생성하는 제너레이터를 통해 데이터를 스트리밍하는 방식입니다.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import io
app = FastAPI()
@app.get("/download/stream")
async def download_stream():
# 스트림 생성
def file_generator():
for i in range(10):
yield f"Line {i}\n".encode('utf-8')
# 응답 헤더 설정
headers = {
'Content-Disposition': 'attachment; filename="generated_file.txt"'
}
return StreamingResponse(file_generator(), media_type="text/plain", headers=headers)
장점:
- 파일이 디스크에 없어도 실시간으로 생성하여 전송 가능합니다.
- 메모리 사용을 최소화하며 스트리밍할 수 있습니다.
- 동적 콘텐츠 생성에 유연합니다.
적합한 상황:
- 동적으로 생성되는 콘텐츠
- 데이터베이스 쿼리 결과를 직접 스트리밍
- 대용량 데이터 처리 시 메모리 사용 최적화가 필요한 경우
- 실시간으로 생성되는 데이터 스트림 제공
한글 파일명 문제 해결하기
한글이 포함된 파일명으로 다운로드 시 다음과 같은 에러가 발생할 수 있습니다:
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 22-25: ordinal not in range(256)
이 문제는 HTTP 헤더가 기본적으로 ASCII 문자만 지원하기 때문에 발생합니다. RFC 6266과 RFC 5987에 따라 filename*=UTF-8''인코딩된_파일명 형식을 사용하여 해결할 수 있습니다.
해결 방법
import urllib.parse
from fastapi import FastAPI
from fastapi.responses import FileResponse
app = FastAPI()
@app.get("/download/korean")
async def download_korean_filename():
file_path = "files/example.txt"
# 한글 파일명
filename = "한글파일명.txt"
# RFC 5987 형식으로 인코딩
encoded_filename = urllib.parse.quote(filename)
headers = {
"Content-Disposition": f"attachment; filename=\"{filename}\"; filename*=UTF-8''{encoded_filename}"
}
return FileResponse(file_path, headers=headers)
이 방식을 사용하면:
- 일반 ASCII 지원 브라우저는 filename 값을 사용합니다.
- 유니코드를 지원하는 현대 브라우저는 filename* 값을 우선적으로 사용합니다.
다양한 파일 타입별 다운로드 구현
텍스트 파일 (TXT)
from fastapi import FastAPI
from fastapi.responses import FileResponse
import os
app = FastAPI()
@app.get("/download/text")
async def download_text():
file_path = os.path.join("files", "document.txt")
# 한글 파일명 처리
filename = "텍스트문서.txt"
encoded_filename = urllib.parse.quote(filename)
headers = {
"Content-Disposition": f"attachment; filename=\"{filename}\"; filename*=UTF-8''{encoded_filename}"
}
return FileResponse(file_path, headers=headers, media_type="text/plain")
이미지 파일 (JPG)
@app.get("/download/image")
async def download_image():
file_path = os.path.join("files", "image.jpg")
# 한글 파일명 처리
filename = "이미지.jpg"
encoded_filename = urllib.parse.quote(filename)
headers = {
"Content-Disposition": f"attachment; filename=\"{filename}\"; filename*=UTF-8''{encoded_filename}"
}
return FileResponse(file_path, headers=headers, media_type="image/jpeg")
PDF 파일
@app.get("/download/pdf")
async def download_pdf():
file_path = os.path.join("files", "document.pdf")
# 한글 파일명 처리
filename = "문서파일.pdf"
encoded_filename = urllib.parse.quote(filename)
headers = {
"Content-Disposition": f"attachment; filename=\"{filename}\"; filename*=UTF-8''{encoded_filename}"
}
return FileResponse(file_path, headers=headers, media_type="application/pdf")
엑셀 파일 (XLSX)
엑셀 파일은 특별한 주의가 필요합니다. openpyxl 또는 pandas 라이브러리를 사용하여 동적으로 생성하는 경우가 많습니다.
import pandas as pd
from io import BytesIO
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import urllib.parse
app = FastAPI()
@app.get("/download/excel")
async def download_excel():
# 데이터 생성 (예시)
df = pd.DataFrame({
'이름': ['홍길동', '김철수', '이영희'],
'나이': [30, 25, 28],
'직업': ['개발자', '디자이너', '마케터']
})
# 엑셀 파일로 변환
excel_buffer = BytesIO()
df.to_excel(excel_buffer, index=False, engine='openpyxl')
excel_buffer.seek(0)
# 한글 파일명 처리
filename = "데이터분석.xlsx"
encoded_filename = urllib.parse.quote(filename)
headers = {
"Content-Disposition": f"attachment; filename=\"{filename}\"; filename*=UTF-8''{encoded_filename}"
}
return StreamingResponse(
excel_buffer,
headers=headers,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
동적으로 생성되는 대용량 CSV 파일
대용량 데이터를 CSV로 제공할 때는 StreamingResponse가 메모리 효율성 측면에서 유리합니다.
import csv
from io import StringIO
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import urllib.parse
app = FastAPI()
@app.get("/download/large-csv")
async def download_large_csv():
async def generate_csv():
# CSV 헤더 생성
buffer = StringIO()
writer = csv.writer(buffer)
writer.writerow(["ID", "이름", "이메일"])
yield buffer.getvalue().encode('utf-8')
buffer.seek(0)
buffer.truncate(0)
# 대량의 데이터를 청크로 생성
for i in range(100000): # 대량 데이터 예시
writer.writerow([i, f"사용자{i}", f"user{i}@example.com"])
output = buffer.getvalue().encode('utf-8')
buffer.seek(0)
buffer.truncate(0)
yield output
# 한글 파일명 처리
filename = "대용량데이터.csv"
encoded_filename = urllib.parse.quote(filename)
headers = {
"Content-Disposition": f"attachment; filename=\"{filename}\"; filename*=UTF-8''{encoded_filename}"
}
return StreamingResponse(
generate_csv(),
headers=headers,
media_type="text/csv"
)
파일 다운로드 구현 시 추가 고려사항
1. 파일 타입별 적절한 MIME 타입 설정
파일 확장자별 MIME 타입을 정확히 설정하면 브라우저가 파일을 올바르게 처리할 수 있습니다.
def get_mime_type(file_extension):
mime_types = {
"txt": "text/plain",
"pdf": "application/pdf",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xls": "application/vnd.ms-excel",
"csv": "text/csv",
"zip": "application/zip",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"mp4": "video/mp4",
# 기타 필요한 확장자 추가
}
return mime_types.get(file_extension.lower(), "application/octet-stream")
@app.get("/download/{filename}")
async def download_file(filename: str):
file_path = os.path.join("files", filename)
# 파일 확장자 추출
_, extension = os.path.splitext(filename)
extension = extension[1:] # 앞의 '.' 제거
# MIME 타입 설정
media_type = get_mime_type(extension)
# 한글 파일명 처리
encoded_filename = urllib.parse.quote(filename)
headers = {
"Content-Disposition": f"attachment; filename=\"{filename}\"; filename*=UTF-8''{encoded_filename}"
}
return FileResponse(file_path, headers=headers, media_type=media_type)
2. 보안 고려사항
파일 다운로드 기능을 구현할 때는 경로 탐색 공격(path traversal)을 방지해야 합니다.
import os
from pathlib import Path
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/download/secure/{filename}")
async def download_secure(filename: str):
# 안전한 디렉토리 경로
base_dir = Path("safe_files")
# 경로 조작 방지
try:
file_path = base_dir / filename
file_path = file_path.resolve()
# 요청된 파일이 base_dir 외부에 있는지 확인
if not str(file_path).startswith(str(base_dir.resolve())):
raise HTTPException(status_code=403, detail="접근이 금지된 파일입니다")
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="파일을 찾을 수 없습니다")
# 파일 다운로드 제공
return FileResponse(file_path)
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="잘못된 파일명입니다")
3. 대용량 파일의 부분 다운로드 지원
HTTP Range 요청을 지원하면 대용량 파일의 부분 다운로드나 스트리밍 재생이 가능합니다. FastAPI의 FileResponse는 기본적으로 이를 지원합니다.
@app.get("/stream/video")
async def stream_video():
video_path = os.path.join("files", "video.mp4")
# FileResponse는 자동으로 Range 요청을 처리합니다
return FileResponse(
video_path,
media_type="video/mp4",
filename="동영상.mp4"
)
통합 파일 다운로드 유틸리티 함수
위의 모든 내용을 고려한 통합 파일 다운로드 유틸리티 함수를 만들어 보겠습니다.
import os
import urllib.parse
from typing import Optional
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, StreamingResponse
from pathlib import Path
app = FastAPI()
def create_download_response(
file_path: str,
filename: Optional[str] = None,
media_type: Optional[str] = None,
as_attachment: bool = True
):
"""
파일 다운로드 응답을 생성하는 유틸리티 함수
Args:
file_path: 파일 경로
filename: 다운로드 시 표시될 파일명 (None이면 원본 파일명 사용)
media_type: 미디어 타입 (None이면 자동 감지)
as_attachment: True면 다운로드, False면 브라우저에서 열기
Returns:
FileResponse: 파일 다운로드 응답
"""
# 파일 존재 확인
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="파일을 찾을 수 없습니다")
# 파일명 처리
if filename is None:
filename = os.path.basename(file_path)
# 파일 확장자로 미디어 타입 추론
if media_type is None:
ext = os.path.splitext(filename)[1].lower()[1:]
media_type = get_mime_type(ext)
# 한글 파일명 처리
encoded_filename = urllib.parse.quote(filename)
# Content-Disposition 헤더 설정
disposition_type = "attachment" if as_attachment else "inline"
headers = {
"Content-Disposition": f"{disposition_type}; filename=\"{filename}\"; filename*=UTF-8''{encoded_filename}"
}
return FileResponse(
file_path,
headers=headers,
media_type=media_type
)
# 사용 예시
@app.get("/files/{filename}")
async def download_any_file(filename: str, inline: bool = False):
file_path = os.path.join("files", filename)
return create_download_response(file_path, as_attachment=not inline)
# 엑셀 파일 동적 생성 예시
@app.get("/reports/excel")
async def generate_excel_report():
# 파일 동적 생성 (예시)
df = pd.DataFrame({"데이터": range(10)})
buffer = BytesIO()
df.to_excel(buffer, index=False)
buffer.seek(0)
# 한글 파일명 처리
filename = "보고서.xlsx"
encoded_filename = urllib.parse.quote(filename)
headers = {
"Content-Disposition": f"attachment; filename=\"{filename}\"; filename*=UTF-8''{encoded_filename}"
}
return StreamingResponse(
buffer,
headers=headers,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
결론
FastAPI는 FileResponse와 StreamingResponse를 통해 다양한 방식의 파일 다운로드를 구현할 수 있습니다. 정적 파일 제공에는 FileResponse가 효율적이며, 동적으로 생성되는 콘텐츠나 대용량 데이터는 StreamingResponse가 적합합니다.
한글 파일명 문제는 filename*=UTF-8''인코딩된_파일명 형식을 사용하여 해결할 수 있으며, 각 파일 타입에 맞는 적절한 MIME 타입 설정과 보안 고려사항을 함께 적용하면 안정적인 파일 다운로드 기능을 구현할 수 있습니다.
특히 엑셀 파일과 같은 바이너리 형식의 파일 다운로드에는 적절한 미디어 타입과 파일 확장자를 설정하여 브라우저가 올바르게 처리할 수 있도록 해야 합니다. 대용량 파일 처리 시에는 메모리 사용을 최소화하는 스트리밍 방식을 활용하는 것이 좋습니다.
FastAPI의 비동기 특성을 활용하면 높은 동시성을 지원하는 파일 다운로드 시스템을 구현할 수 있으며, 이는 대규모 API 서비스에서 특히 유용합니다.
'파이썬 > Fast API' 카테고리의 다른 글
FastAPI Response Set-Cookie와 HttpOnly 보안 구현 (1) | 2025.02.27 |
---|---|
FastAPI Security: API 보안 구현 (0) | 2025.02.24 |
SlowAPI: FastAPI에서 Rate Limiting 구현 (0) | 2025.02.23 |
FastAPI Permissions: 권한 관리 구현 (0) | 2025.02.22 |
FastAPI-Users with MongoDB: JWT 인증 (0) | 2025.02.21 |