파이썬 requests 대용량 다운로드 가이드 stream=True와 파일 청크로 메모리 피크 억제
🚀 실무에서 통하는 안전한 파일 다운로드 최적화 비법을 한 번에 정리합니다
대용량 파일을 내려받을 때 프로그램이 버벅이거나 메모리가 치솟는 경험을 한 번쯤 겪게 됩니다.
웹 서버는 응답을 보내고 있는데, 클라이언트 쪽에서 한 번에 내용을 다 읽으려다 메모리 한도를 넘겨 실패하는 경우도 적지 않습니다.
특히 로그, 백업, 동영상, 모델 가중치처럼 수백 MB에서 수 GB까지 커지는 파일일수록 문제가 반복됩니다.
이 글은 그런 낭비를 근본적으로 줄이고, 같은 코드로 더 큰 파일을 안정적으로 다루기 위한 실전 설정과 패턴을 알기 쉽게 풀어냅니다.
필수 개념부터 예외 처리와 검증, 규모 확장까지 차근차근 짚어 실서비스에 곧바로 적용할 수 있도록 구성했습니다.
핵심은 간단합니다.
파이썬 requests로 대용량을 다룰 때는 stream=True로 응답을 스트리밍하고, 디스크에 파일 청크 쓰기를 적용해 메모리 피크를 억제하는 것입니다.
이 기본 원칙에 타임아웃과 재시도, 무결성 검증, 병렬 전략을 더하면 속도와 안정성을 동시에 끌어올릴 수 있습니다.
아래 목차에 맞춰 개념과 코드, 체크리스트를 순서대로 정리했습니다.
📋 목차
🔗 파이썬 requests로 대용량 다운로드 기본 개념
파이썬 requests로 대용량 파일을 안정적으로 내려받기 위한 핵심은 스트리밍과 디스크 청크 쓰기입니다.
요지를 한 문장으로 정리하면 다음과 같습니다.
파이썬 requests > 성능·규모화 > 대용량 다운로드는 stream=True + 파일청크 쓰기·메모리 피크 억제가 정석입니다.
기본 요청처럼 전체 응답 본문을 즉시 메모리에 적재하면, 수백 MB 이상의 파일에서 RAM이 급격히 치솟아 OOM에 가까워질 수 있습니다.
반대로 stream=True로 네트워크 소켓에서 데이터를 흐름대로 가져오고, iter_content()로 일정 크기의 블록을 나눠 즉시 디스크에 기록하면 메모리 사용량을 수십 KB~수 MB 수준으로 고정할 수 있습니다.
이 구조는 단일 머신의 자원 한계를 늦추고, 컨테이너 환경이나 서버리스처럼 메모리 제한이 엄격한 실행 환경에서 특히 유리합니다.
스트리밍은 TCP 수신 버퍼에 쌓이는 데이터를 사용자 공간으로 조금씩 끌어와 처리하는 방식이라, 응답 본문 전체를 .content로 한 번에 읽을 때와 달리 파이썬 객체가 폭증하지 않습니다.
파일 청크 쓰기를 병행하면 파이썬 레벨에서 불필요한 복사를 줄이고, 운영체제의 페이지 캐시가 디스크 I/O를 효율적으로 묶어 처리할 여지도 커집니다.
또한 Content-Length 헤더로 예상 용량을 확인하고, 서버가 전송을 중단하더라도 부분 파일을 안전하게 재시도할 수 있는 기반을 마련합니다.
결국 네트워크, 파일 I/O, 메모리 세 축의 균형을 맞추는 것이 대용량 다운로드의 본질입니다.
import requests
url = "https://example.com/large-file.bin"
chunk_size = 1024 * 1024 # 1MB 권장(네트워크/디스크 상황에 따라 256KB~4MB 조정)
with requests.get(url, stream=True, timeout=(5, 60)) as r:
r.raise_for_status()
total = int(r.headers.get("Content-Length", 0) or 0)
written = 0
with open("large-file.bin", "wb") as f:
for chunk in r.iter_content(chunk_size=chunk_size):
if not chunk:
continue # keep-alive 청크 필터링
f.write(chunk)
written += len(chunk)
# 선택: 전송 중단/부분 쓰기 탐지
if total and written != total:
raise IOError(f"Incomplete download: {written} of {total} bytes")
- 🔌stream=True로 응답 본문을 즉시 메모리에 올리지 않기
- 📦iter_content(chunk_size)로 파일 청크 쓰기 적용
- ⏱️연결/읽기 timeout 튜플 설정으로 장기 대기 방지
- 🧮Content-Length 확인 후 불완전 다운로드 감지
- 🧱청크 크기 256KB~4MB 사이에서 환경에 맞게 벤치마크
💡 TIP: 메모리 피크를 더 낮추려면 with 컨텍스트로 연결 수명과 파일 핸들을 엄격히 관리하고, 필요 시 임시 저장소(예: 다른 파티션, NVMe)를 지정해 스왑 유발을 줄입니다.
⚠️ 주의: response.content나 response.text를 호출하면 스트리밍 이점을 잃고 전체 본문이 메모리에 적재됩니다.
대용량에서는 피해야 할 패턴입니다.
🛠️ stream=True로 메모리 피크 억제하는 원리
일반적으로 requests.get()를 사용할 때는 서버로부터 받은 전체 데이터를 한 번에 읽어 response.content 속성에 담습니다.
이 과정은 간단하지만, 대용량 데이터에서는 프로그램이 모든 바이트를 메모리에 보관하기 때문에 한계가 금방 드러납니다.
즉, 5GB 파일을 다운로드하면 5GB의 RAM이 필요하게 되는 셈이죠.
이 문제를 해결하는 핵심이 바로 stream=True 옵션입니다.
이 옵션을 지정하면 requests는 서버로부터 받은 데이터를 즉시 모두 읽지 않고, TCP 소켓 버퍼 수준에서 데이터를 부분적으로 보관한 뒤 애플리케이션이 직접 꺼내갈 때까지 기다립니다.
즉, 응답 본문을 스트리밍 방식으로 “흘려보내기” 때문에, 다운로드 중에도 메모리 사용량이 거의 일정하게 유지됩니다.
이 덕분에 시스템의 전체 성능과 안정성이 크게 향상됩니다.
파이썬 인터프리터가 객체를 무수히 생성하고 해제하는 일을 줄여 CPU 부담도 함께 낮아집니다.
💬 stream=True는 요청 시점에 데이터 읽기를 지연시키는 ‘게으른 로딩(lazy loading)’ 전략으로, 메모리와 네트워크를 효율적으로 병행할 수 있게 해줍니다.
stream 모드에서는 iter_content() 메서드를 통해 일정한 크기의 바이트 블록을 순차적으로 읽어올 수 있습니다.
이 블록 크기를 적절히 조정하면 I/O 효율과 속도를 최적화할 수 있습니다.
보통은 1MB 전후를 권장하지만, 네트워크 속도나 스토리지 성능에 따라 256KB에서 4MB까지 조정하면서 벤치마크하는 것이 좋습니다.
단, 청크가 너무 작으면 CPU 오버헤드가 늘고, 너무 크면 다시 메모리 버퍼가 커지므로 균형이 중요합니다.
import requests
url = "https://example.com/bigdata.zip"
with requests.get(url, stream=True) as response:
for chunk in response.iter_content(chunk_size=8192):
process(chunk) # 메모리에 적재하지 않고 즉시 처리
💎 핵심 포인트:
stream=True를 쓰면 응답 본문이 한 번에 메모리에 적재되지 않고, iter_content() 호출 시에만 필요한 만큼만 읽습니다. 따라서 수 GB급 파일도 안전하게 다운로드할 수 있습니다.
이 접근법은 단순히 메모리 절약만이 아니라, 네트워크 속도의 일관성을 높이고 다른 I/O 작업과 병행할 여유를 제공합니다.
특히 백엔드 서버나 크롤러, 데이터 파이프라인 등에서 병렬 다운로드를 수행할 때 stream 옵션은 필수에 가깝습니다.
단, 요청 후 반드시 response.close() 또는 with 블록을 사용해야 연결 누수를 막을 수 있습니다.
⚠️ 주의: stream=True를 설정했더라도, 데이터를 끝까지 읽지 않으면 연결이 해제되지 않은 채 남을 수 있습니다.
반드시 루프를 끝까지 돌거나 명시적으로 close()를 호출하세요.
⚙️ 파일 청크 쓰기 패턴과 안전한 코드 예시
stream=True로 데이터를 스트리밍하더라도, 그 데이터를 파일에 어떻게 기록하느냐에 따라 효율이 크게 달라집니다.
대부분의 초보자들은 단순히 반복문으로 파일에 쓰는 수준에서 멈추지만, 안정적인 다운로드를 위해서는 청크 단위의 쓰기와 무결성 검증이 필수입니다.
이 두 가지를 적절히 조합해야 네트워크 지연, 일시적인 끊김, 부분 다운로드 같은 문제를 방지할 수 있습니다.
일반적으로 iter_content()로 읽은 데이터는 청크 단위로 파일에 바로 쓰는 것이 좋습니다.
파일을 여는 시점에는 반드시 “wb” 모드(바이너리 쓰기)를 사용해야 하며, 청크 크기는 1MB 전후가 가장 안정적입니다.
또한 다운로드 중 예기치 않게 중단되더라도 손상된 파일을 구분할 수 있도록, 최종적으로 Content-Length와 실제 쓰인 바이트 수를 비교합니다.
import requests
import os
def download_file(url, filename, chunk_size=1024*1024):
with requests.get(url, stream=True) as r:
r.raise_for_status()
total = int(r.headers.get('Content-Length', 0))
written = 0
with open(filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
written += len(chunk)
if total and written != total:
raise IOError(f"Incomplete file: expected {total}, got {written}")
return filename
download_file("https://example.com/large-data.zip", "large-data.zip")
이 코드의 핵심은 다운로드 중에도 메모리 사용량이 일정하게 유지된다는 점입니다.
iter_content()는 내부적으로 제너레이터(generator)처럼 작동하므로, 매 루프마다 새로운 데이터가 들어올 때만 잠시 버퍼를 차지합니다.
이를 통해 수 GB급 파일도 손쉽게 다룰 수 있고, 운영체제의 페이지 캐시가 파일 쓰기를 효율적으로 처리할 수 있습니다.
💡 TIP: 다운로드가 중단되어 이어받기가 필요할 경우, Range 헤더를 이용해 특정 바이트부터 재요청할 수 있습니다.
예를 들어 headers={“Range”: “bytes=1000000-“} 와 같이 지정하면 1MB 이후부터 다시 다운로드됩니다.
| 항목 | 설명 |
|---|---|
| stream=True | 응답 본문을 즉시 읽지 않고 순차 처리 |
| iter_content() | 스트리밍된 데이터를 청크 단위로 가져오기 |
| chunk_size | 청크 크기(256KB~4MB 권장) |
| Content-Length | 파일 전체 크기 검증용 헤더 |
이 패턴은 단일 파일 다운로드뿐 아니라 병렬 분할 다운로드, 데이터 스트리밍 처리 등 다양한 곳에 응용할 수 있습니다.
대규모 데이터 환경에서도 최소한의 자원으로 최대 효율을 얻을 수 있는 가장 기본적이고 강력한 구조입니다.
🔒 타임아웃 재시도 무결성 검증 필수 설정
대용량 다운로드에서 stream=True와 청크 쓰기를 적용해도, 네트워크 중단이나 연결 타임아웃 문제가 남을 수 있습니다.
이럴 때 재시도 로직과 무결성 검증을 병행하면 신뢰도를 크게 높일 수 있습니다.
특히 수백 MB 이상을 여러 번 전송할 때는 일시적인 끊김이 흔하므로, 자동 복구를 고려한 구조가 필요합니다.
requests 라이브러리는 내부적으로 urllib3를 사용하기 때문에, 세션 객체를 활용하면 재시도 정책을 손쉽게 설정할 수 있습니다.
또한, 파일 다운로드 후에는 SHA256 해시 또는 Content-Length를 비교하여 데이터의 손상 여부를 확인하는 것이 좋습니다.
이 과정을 통해 다운로드가 불완전하거나 손상된 파일을 자동으로 걸러낼 수 있습니다.
import requests
from requests.adapters import HTTPAdapter, Retry
import hashlib
def sha256sum(filename):
h = hashlib.sha256()
with open(filename, "rb") as f:
for block in iter(lambda: f.read(8192), b""):
h.update(block)
return h.hexdigest()
def download_with_retry(url, filename, expected_hash=None):
session = requests.Session()
retries = Retry(total=5, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
adapter = HTTPAdapter(max_retries=retries)
session.mount("http://", adapter)
session.mount("https://", adapter)
with session.get(url, stream=True, timeout=(5, 60)) as r:
r.raise_for_status()
with open(filename, "wb") as f:
for chunk in r.iter_content(chunk_size=1024*1024):
if chunk:
f.write(chunk)
if expected_hash:
actual_hash = sha256sum(filename)
if actual_hash != expected_hash:
raise IOError("File hash mismatch. Download corrupted.")
return filename
이 구조는 네트워크 오류가 발생할 때 자동으로 재시도하며, HTTP 500~504 같은 서버 오류에도 대응합니다.
또한 backoff_factor를 통해 재시도 간격을 점진적으로 늘려 서버 부담을 완화할 수 있습니다.
다운로드가 끝난 뒤에는 해시를 검증해 데이터 무결성을 보장하므로, 백업 시스템이나 데이터 분석 파이프라인에서도 안전하게 활용할 수 있습니다.
💎 핵심 포인트:
타임아웃과 재시도 설정은 단순히 네트워크 안정성을 높이는 수준을 넘어, 자동화된 대규모 다운로드 환경에서 데이터 신뢰성을 확보하는 핵심 요소입니다.
- 🧩Retry() 객체로 재시도 횟수와 상태 코드 지정
- 🕒timeout=(connect, read) 형태로 타임아웃 구분 설정
- 🔐SHA256 해시로 다운로드 파일 검증
- 📡backoff_factor로 재시도 간 대기시간 점진적 증가
- 🧾예외 발생 시 raise_for_status()로 즉시 실패 처리
⚠️ 주의: 대용량 파일을 여러 번 재시도하는 경우 서버 측에 과부하를 유발할 수 있습니다.
필요 이상으로 total 횟수를 높이지 말고, 지수 백오프(backoff)를 반드시 설정하세요.
⚡ 병렬 처리와 규모화 전략 성능 튜닝
stream=True와 파일 청크 쓰기만으로도 단일 다운로드는 충분히 안정적입니다.
그러나 다수의 파일을 동시에 처리해야 하거나, 한 파일을 분할해 병렬로 내려받는 경우에는 더 정교한 병렬 처리 전략이 필요합니다.
파이썬은 기본적으로 GIL(Global Interpreter Lock) 때문에 CPU 병렬성은 제한되지만, 네트워크 I/O 중심의 다운로드 작업은 스레드나 비동기 I/O로 쉽게 확장할 수 있습니다.
병렬 다운로드는 크게 두 가지 방식으로 나뉩니다.
첫째는 여러 개의 파일을 동시에 요청하는 방식이고, 둘째는 하나의 파일을 여러 구간으로 나눠 동시에 받아서 병합하는 방식입니다.
두 번째 방법은 서버가 Range 헤더를 지원해야 하지만, 지원만 된다면 대용량 파일에서도 눈에 띄는 속도 향상을 기대할 수 있습니다.
예를 들어 4개의 스레드로 분할 다운로드를 수행하면 네트워크 대역폭을 효율적으로 활용할 수 있습니다.
import requests
import threading
def download_part(url, start, end, filename, idx):
headers = {"Range": f"bytes={start}-{end}"}
with requests.get(url, headers=headers, stream=True) as r:
with open(f"{filename}.part{idx}", "wb") as f:
for chunk in r.iter_content(chunk_size=1024*512):
if chunk:
f.write(chunk)
def merge_parts(filename, parts):
with open(filename, "wb") as outfile:
for i in range(parts):
part_name = f"{filename}.part{i}"
with open(part_name, "rb") as infile:
outfile.write(infile.read())
url = "https://example.com/large.iso"
file_size = int(requests.head(url).headers["Content-Length"])
threads = []
part_size = file_size // 4
for i in range(4):
start = i * part_size
end = file_size - 1 if i == 3 else (start + part_size - 1)
t = threading.Thread(target=download_part, args=(url, start, end, "large.iso", i))
t.start()
threads.append(t)
for t in threads:
t.join()
merge_parts("large.iso", 4)
이 코드는 4개의 스레드가 파일의 서로 다른 범위를 동시에 다운로드한 뒤, 모든 조각을 합쳐 완전한 파일을 만듭니다.
실제 환경에서는 스레드 개수, 청크 크기, 네트워크 환경에 따라 성능이 달라지므로 적절한 균형을 찾아야 합니다.
특히 SSD 또는 NVMe 환경에서는 디스크 쓰기 속도가 충분히 빠르므로 병렬 다운로드의 이점이 더욱 커집니다.
💡 TIP: 다수의 파일을 병렬로 받을 때는 concurrent.futures.ThreadPoolExecutor를 이용하면 코드가 훨씬 간결해집니다.
또한 CPU 부하가 낮은 I/O 중심 작업에서는 asyncio 기반의 aiohttp도 좋은 대안입니다.
💬 병렬 다운로드는 단순히 속도를 높이기 위한 기능이 아니라, 대규모 배포·백업·데이터 수집 파이프라인을 안정화하는 핵심 기술입니다.
- ⚙️서버가 Range 요청을 지원하는지 사전 확인
- 📊스레드 수는 네트워크 대역폭과 CPU 여유에 따라 조정
- 💾각 파트 파일을 순서대로 병합 후 임시 파일 삭제
- 🚀단일 요청 대비 1.5~3배까지 다운로드 속도 향상 가능
- 🧭asyncio 기반 aiohttp를 사용하면 수십 개 병렬 다운로드도 효율적
결국 requests 기반 대용량 다운로드는 stream=True로 메모리 피크를 억제하고, 파일 청크 쓰기로 I/O 효율을 확보하며, 병렬 처리로 속도를 극대화하는 세 가지 단계가 완성형 구조라 할 수 있습니다.
❓ 자주 묻는 질문 (FAQ)
stream=True를 쓰면 속도가 느려지는 이유가 있나요?
적절한 chunk_size를 설정하면 오히려 더 안정적이고 빠르게 다운로드할 수 있습니다.
iter_content()와 raw.read()는 어떤 차이가 있나요?
자동으로 keep-alive 청크를 걸러냅니다. 반면 raw.read()는 저수준 I/O이므로 수동으로 제어해야 합니다.
대용량 파일을 받을 때 프로그레스바를 표시할 수 있나요?
다운로드 진행률을 실시간으로 표시할 수 있습니다.
stream=True는 어떤 상황에서 꼭 필요하나요?
메모리 폭주를 방지하고 안정적인 다운로드를 보장합니다.
파일 무결성 검증은 꼭 해야 하나요?
SHA256 해시 검증은 필수 보안 절차로 봐야 합니다.
requests와 aiohttp 중 어떤 게 더 빠른가요?
비동기 기반 aiohttp가 훨씬 효율적입니다.
메모리 사용량을 최소화하려면 어떻게 해야 하나요?
스트리밍 데이터를 디스크에 바로 쓰는 구조를 유지하면 됩니다.
추가적으로 불필요한 리스트나 버퍼 사용을 피하세요.
requests로 멀티파트 업로드도 stream처럼 할 수 있나요?
업로드 시에도 청크 단위 스트리밍을 지원하므로 대용량 업로드에도 유용합니다.
📘 파이썬 requests 대용량 다운로드, 성능을 지키는 최적의 방법
대용량 파일 다운로드는 단순한 코드 몇 줄로 끝나는 작업 같지만, 실무에서는 성능과 안정성을 동시에 잡아야 하는 까다로운 영역입니다.
이 글에서 살펴본 것처럼, stream=True와 파일 청크 쓰기를 조합하면 메모리 사용량을 최소화하면서도 효율적인 다운로드를 구현할 수 있습니다.
여기에 Retry, timeout, SHA256 검증 등을 추가하면 네트워크 불안정성에도 끄떡없는 구조가 완성됩니다.
또한, 병렬 다운로드와 Range 요청을 활용하면 속도까지 확보할 수 있습니다.
결국 이 모든 전략은 단 하나의 목적, 즉 “한정된 자원으로 대규모 데이터를 안전하게 다루는 것”에 맞춰져 있습니다.
이 글을 기반으로 여러분의 서비스나 데이터 파이프라인에서도 안정적인 대용량 다운로드 환경을 구축해보세요.
🏷️ 관련 태그 : 파이썬requests, 대용량다운로드, streamTrue, 파일청크쓰기, 메모리피크억제, 다운로드최적화, 병렬처리, SHA256검증, 타임아웃설정, 네트워크성능