파이썬 requests 파일 다운로드 완벽 가이드 stream=True와 timeout 구성 예시
🐍 안정적이고 빠른 다운로드를 위한 세션 구성부터 예외 처리까지 한 번에 배우는 실전 튜토리얼
대용량 파일을 네트워크 오류 없이 깔끔하게 내려받고 싶은데, 어디서부터 손대야 할지 막막할 때가 많습니다.
단순히 URL만 넣고 요청을 보내면 되는 줄 알았다가, 타임아웃이나 연결 끊김, 메모리 사용량 폭증 같은 문제를 겪기 쉽죠.
이 글은 그런 시행착오를 줄이기 위해 파이썬 requests로 안정적인 다운로드 환경을 만드는 방법을 친근한 설명과 함께 정리합니다.
특히 실무에서 자주 쓰는 세션 기반 구성과 스트리밍 옵션을 중심으로, 재시도 전략과 예외 처리의 기준선을 잡아 드립니다.
현장에서 바로 가져다 쓸 수 있는 예제와 체크리스트를 곁들여, 코드 품질과 다운로드 성공률을 동시에 챙길 수 있도록 도와드릴게요.
핵심 예제는 다음 구성을 반드시 포함합니다.
실제 코드에서 세션을 사용해 요청을 열고, 스트리밍으로 청크를 처리하며, 상태 코드 검증을 통해 실패를 즉시 감지합니다.
필수 구문은 다음과 같습니다.
with s.get(u, stream=True, timeout=(3,30)) as resp: resp.raise_for_status();
이 한 줄이 타임아웃을 단기 연결과 전체 응답으로 분리하고, HTTP 오류 상황을 명확히 예외로 넘겨 재시도나 로깅 로직으로 이어질 수 있도록 해 줍니다.
아래 목차를 따라가며 세션 설정, 저장 로직, 성능 최적화, 보안 관점까지 단계적으로 살펴보겠습니다.
📋 목차
🧠 파이썬 requests 파일 다운로드 핵심 개념
파일 다운로드에서 가장 중요한 기준은 안정성, 메모리 사용량, 예외 처리입니다.
requests는 간결한 API와 세션 재사용으로 이 세 가지를 균형 있게 다루기 좋습니다.
다운로드 요청은 세션으로 열고, 스트리밍을 활성화해 청크 단위로 읽고, 상태 코드를 즉시 검증하는 흐름이 기본입니다.
핵심 구문은 다음과 같습니다.
with s.get(u, stream=True, timeout=(3,30)) as resp: resp.raise_for_status();
여기서 timeout 튜플은 (연결, 읽기) 순서로 적용되어 연결 지연과 응답 지연을 분리해 제어하고, raise_for_status는 4xx, 5xx 응답을 예외로 전환해 재시도·로그 수집·정리 작업을 쉽게 만듭니다.
📦 스트리밍과 청크 처리의 의미
stream=True는 응답 본문을 한꺼번에 메모리에 올리지 않고 네트워크에서 읽히는 대로 순차적으로 소비하도록 설정합니다.
대용량 파일이나 느린 네트워크에서 메모리 폭주를 막고 점진적으로 디스크에 기록할 수 있습니다.
iter_content는 지정한 크기만큼 데이터를 반복적으로 반환하므로, 각 청크를 파일로 바로 쓰면 효율과 안전성을 모두 확보할 수 있습니다.
반면 stream=False는 즉시 다운로드 후 메모리에 보관하므로 작은 JSON·텍스트에는 빠르지만, 큰 파일에는 부적절합니다.
⏱️ timeout 튜플과 예외 처리의 기본
timeout=(3,30)은 연결 단계에서 3초, 본문 읽기에서 30초를 상한으로 둡니다.
연결이 지연되면 빠르게 포기하고, 연결 후에는 서버 속도와 파일 크기를 고려해 충분한 읽기 시간을 허용하는 식으로 안전합니다.
raise_for_status는 HTTPError를 발생시키며, 이를 try-except로 감싸면 네트워크 오류(ConnectionError, Timeout)와 구분해 처리할 수 있습니다.
실전에서는 상태 코드별로 다른 로직(예: 404면 스킵, 429면 대기 후 재시도)을 두고, 예외 발생 시 임시 파일을 정리해 남은 파일 핸들이 리소스를 점유하지 않도록 해야 합니다.
import os
from pathlib import Path
import requests as r
def download_file(u: str, dest: Path, chunk: int = 1024 * 64) -> Path:
dest.parent.mkdir(parents=True, exist_ok=True)
tmp = dest.with_suffix(dest.suffix + ".part")
with r.Session() as s:
s.headers.update({"User-Agent": "requests-download-guide/1.0"})
# 핵심 구성 예시
with s.get(u, stream=True, timeout=(3,30)) as resp:
resp.raise_for_status()
with tmp.open("wb") as f:
for part in resp.iter_content(chunk_size=chunk):
if part: # keep-alive 청크 무시
f.write(part)
tmp.replace(dest)
return dest
- 🧩세션(Session)으로 헤더·쿠키를 재사용해 연결 수를 줄였는가
- 💧stream=True로 메모리 사용량을 통제하고 iter_content로 청크 저장하는가
- ⏲️timeout=(연결, 읽기) 값을 트래픽·파일 크기에 맞게 조정했는가
- 🚨resp.raise_for_status()로 실패를 즉시 감지하고 예외로 넘기는가
💡 TIP: Content-Length 헤더가 있다면 진행률 계산에 활용할 수 있습니다.
없을 수 있으므로 항상 누적 바이트 기준의 안전한 진행률 계산을 병행하는 편이 좋습니다.
⚠️ 주의: stream=True로 열었을 때 파일 객체를 닫지 않으면 연결이 남아 세션 풀을 고갈시킬 수 있습니다.
with 블록을 사용해 응답과 파일을 확실히 닫는 구조를 유지하세요.
🔁 세션과 스트리밍 옵션의 작동 방식
파이썬 requests에서 세션(Session) 객체는 서버와의 연결을 재활용해 속도를 높이고, 쿠키·헤더 정보를 유지할 수 있게 합니다.
파일 다운로드 시에는 세션으로 연결을 유지하면 TCP 핸드셰이크 비용이 줄어들어 다중 파일 다운로드에서 큰 차이를 보입니다.
특히 인증이 필요한 API나 다운로드 서버에서 토큰 기반 접근을 수행할 때 세션은 필수적입니다.
세션 객체를 with 블록으로 관리하면 자동으로 연결이 닫히며, 리소스 누수를 방지할 수 있습니다.
🧩 세션의 이점과 연결 재사용
세션은 내부적으로 HTTP Keep-Alive를 유지하여 동일 호스트와의 요청에서 소켓을 재활용합니다.
이로 인해 각 요청마다 새 연결을 여는 것보다 훨씬 효율적이며, 속도는 20~30%까지 향상될 수 있습니다.
예를 들어 여러 개의 대용량 파일을 같은 서버에서 받을 때, 세션 하나를 열어 순차적으로 요청하는 것이 훨씬 효율적입니다.
또한 기본 헤더를 세션 단위로 설정하면 각 요청마다 헤더를 반복적으로 추가할 필요가 없습니다.
import requests
with requests.Session() as s:
s.headers.update({"User-Agent": "stream-example/1.0"})
urls = [
"https://example.com/file1.zip",
"https://example.com/file2.zip"
]
for url in urls:
with s.get(url, stream=True, timeout=(3,30)) as resp:
resp.raise_for_status()
with open(url.split("/")[-1], "wb") as f:
for chunk in resp.iter_content(8192):
f.write(chunk)
이 예제는 with 블록 중첩 구조를 활용해 세션, 요청, 파일 모두 명확히 닫히도록 보장합니다.
또한 stream=True를 지정했기 때문에 대용량 파일도 일정한 메모리 사용량으로 안정적으로 저장됩니다.
📥 스트리밍 모드에서의 데이터 흐름
스트리밍 모드는 서버로부터 받은 데이터가 완전히 도착하기 전에도 클라이언트가 데이터를 순차적으로 읽을 수 있게 해 줍니다.
이 방식은 RAM 대신 디스크를 활용하기 때문에 1GB 이상의 파일을 처리할 때도 안정적으로 작동합니다.
iter_content()의 chunk_size를 조정하면 메모리 점유율과 처리 속도를 균형 있게 조정할 수 있습니다.
보통 64KB(65536바이트) 단위면 대부분의 환경에서 효율적입니다.
💎 핵심 포인트:
stream=True는 단순 옵션이 아닌, 대용량 파일을 안정적으로 다루는 데 꼭 필요한 메모리 제어 메커니즘입니다.
CPU보다 네트워크 병목이 큰 환경에서 특히 효과적이며, 다운로드 중 인터럽트 발생 시에도 손상 방지를 돕습니다.
- 🔗같은 서버에서 여러 파일을 받을 때는 세션 재사용으로 속도 향상
- ⚙️stream=True를 통해 RAM 과다 사용 방지
- 🧾iter_content()에서 청크 크기를 조정해 속도·안정성 밸런스 확보
- 🧰with 블록으로 세션, 응답, 파일 모두 자동 종료 구조 유지
💡 TIP: 청크를 처리할 때 진행률 표시를 위해 tqdm 같은 라이브러리를 함께 쓰면 다운로드 과정이 훨씬 직관적입니다.
💾 파일 저장 구현과 예외 처리 패턴
파일 다운로드는 단순히 데이터를 받는 것에서 끝나지 않습니다.
정상적으로 저장하고, 예외 발생 시 임시 파일을 정리하며, 중복 다운로드나 손상 파일을 방지하는 처리가 필요합니다.
파이썬 requests에서는 예외를 감지하는 resp.raise_for_status()와 스트리밍 처리 중 발생하는 ConnectionError, Timeout 예외를 조합하여 안전하게 복구할 수 있습니다.
🧱 예외 처리 구조 설계
실무에서는 다운로드 중 네트워크 장애가 일어날 가능성을 고려해야 합니다.
이를 위해 try-except 구문으로 오류를 포착하고, 재시도 로직을 구성하는 것이 일반적입니다.
다음은 세션 기반 다운로드 함수에서 예외를 안전하게 처리하는 예시입니다.
import requests, os
from pathlib import Path
def safe_download(url: str, dest: Path, retry: int = 3):
tmp = dest.with_suffix(".part")
for attempt in range(1, retry + 1):
try:
with requests.Session() as s:
with s.get(url, stream=True, timeout=(3,30)) as resp:
resp.raise_for_status()
with tmp.open("wb") as f:
for chunk in resp.iter_content(8192):
if chunk:
f.write(chunk)
tmp.replace(dest)
print("✅ 다운로드 완료:", dest)
break
except (requests.ConnectionError, requests.Timeout) as e:
print(f"⚠️ 재시도 {attempt}/{retry}: {e}")
except requests.HTTPError as e:
print("❌ 서버 응답 오류:", e)
break
else:
print("⛔ 다운로드 실패, 임시 파일 삭제")
if tmp.exists():
tmp.unlink()
이 구조는 다운로드가 실패하더라도 임시 파일(.part)을 남겨두지 않아 안전합니다.
또한 raise_for_status()를 통해 서버 오류를 명확히 구분할 수 있으며, 재시도 횟수를 초과하면 정리 후 종료합니다.
🧾 파일 저장 경로와 권한 설정
다운로드 경로를 지정할 때는 디렉터리가 존재하지 않아도 자동 생성되도록 설정하는 것이 좋습니다.
특히 pathlib.Path를 이용하면 간결하게 경로를 조작할 수 있습니다.
또한 파일 이름 충돌이 예상되는 경우, 기존 파일을 덮어쓰지 않고 별도 버전 이름으로 저장하거나 해시값으로 관리하는 방식도 유용합니다.
- 🧩임시 파일(.part)을 사용해 다운로드 도중 중단 시 손상 방지
- 🧰try-except로 ConnectionError와 HTTPError를 구분
- 💾성공 시 .part 파일을 rename 또는 replace로 완성본으로 이동
- 🧑💻Path 객체의 mkdir(parents=True, exist_ok=True)로 경로 자동 생성
⚠️ 주의: Windows 환경에서는 같은 이름의 파일이 열려 있을 경우 replace()가 실패할 수 있습니다.
이럴 땐 os.replace()나 shutil.move()로 대체하는 것이 안전합니다.
💡 TIP: 실패한 다운로드 로그를 남겨두면 재시도 자동화 스크립트와 연계하기 편리합니다.
예를 들어 실패한 URL을 별도 파일에 기록해, 다음 실행 시 자동 재시도하도록 구성할 수 있습니다.
🚀 대용량 다운로드와 성능 최적화 팁
대용량 파일 다운로드 시에는 단순한 요청 코드만으로는 충분하지 않습니다.
속도, 메모리, 안정성의 균형을 위해 스트리밍, 청크 크기, 세션 재사용, 병렬 처리 등을 전략적으로 조합해야 합니다.
특히 stream=True 옵션은 필수이며, 네트워크 상황에 따라 timeout을 적절히 조정해야 합니다.
여기에 캐시 제어, 파일 무결성 검증까지 더하면 신뢰도 높은 자동화 다운로드 환경을 구축할 수 있습니다.
⚙️ 병렬 처리로 속도 향상하기
동시에 여러 파일을 내려받을 때는 ThreadPoolExecutor를 사용하면 효과적입니다.
CPU 연산이 거의 없는 I/O 작업이므로 멀티스레딩으로 속도를 높일 수 있습니다.
아래 예시는 requests와 concurrent.futures를 결합한 병렬 다운로드 예시입니다.
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests, os
urls = [
"https://example.com/large1.iso",
"https://example.com/large2.iso",
"https://example.com/large3.iso",
]
def fetch(url):
local = os.path.basename(url)
with requests.Session() as s:
with s.get(url, stream=True, timeout=(3, 60)) as resp:
resp.raise_for_status()
with open(local, "wb") as f:
for chunk in resp.iter_content(1024 * 64):
f.write(chunk)
return local
with ThreadPoolExecutor(max_workers=3) as exe:
futures = [exe.submit(fetch, u) for u in urls]
for f in as_completed(futures):
print("✅ 완료:", f.result())
이 방식은 I/O 병목을 줄여 전체 다운로드 시간을 단축합니다.
단, 동시에 많은 스레드를 열면 서버의 제한이나 네트워크 포화로 실패할 수 있으므로, max_workers는 3~5개 정도가 적당합니다.
💡 캐시 및 무결성 검증
다운로드한 파일의 손상 여부를 판단하기 위해 해시 검증을 추가하는 것이 좋습니다.
서버에서 제공하는 ETag 또는 Content-MD5 헤더를 비교하거나, 수동으로 hashlib을 사용해 로컬 파일의 MD5, SHA256 값을 계산할 수 있습니다.
이 과정은 자동화 스크립트에서 파일 유효성을 보장하는 가장 확실한 방법입니다.
import hashlib
def file_md5(path, chunk=8192):
h = hashlib.md5()
with open(path, "rb") as f:
for data in iter(lambda: f.read(chunk), b""):
h.update(data)
return h.hexdigest()
파일 크기와 해시가 모두 일치하면 다운로드가 성공적으로 완료된 것으로 볼 수 있습니다.
대규모 배치 다운로드에서는 로그 파일에 해시값을 함께 기록해 두면, 추후 재검증이 용이합니다.
- 🚀ThreadPoolExecutor로 병렬 다운로드 처리
- 📊max_workers는 네트워크 상태에 따라 3~5개로 제한
- 🔍MD5, SHA256 등 해시 검증으로 파일 손상 여부 확인
- 🧰ETag, Content-Length 등 HTTP 헤더 기반 캐시 활용
💡 TIP: 파일 다운로드 완료 후에도 세션을 닫기 전에 잠시 대기(time.sleep(0.2))를 주면 서버가 연결 종료를 더 안정적으로 인식합니다.
🔐 보안 고려사항과 신뢰성 체크리스트
안전한 파일 다운로드의 출발점은 TLS 검증과 신뢰할 수 있는 출처입니다.
파이썬 requests는 기본적으로 SSL 인증서 검증을 수행하며, 임의로 검증을 끄면 중간자 공격이나 위조 파일 유포에 노출됩니다.
또한 무결성 확보를 위한 해시 검증, 크기 확인, 재시도와 지수 백오프, 그리고 부분 재개(HTTP Range)까지 갖추면 네트워크 변동에도 견고하게 동작합니다.
핵심 구성 예시는 반드시 다음 라인을 포함해야 합니다.
with s.get(u, stream=True, timeout=(3,30)) as resp: resp.raise_for_status();
여기서 stream=True와 timeout 튜플은 신뢰성과 자원 통제를 동시에 달성하는 기본 장치입니다.
🔒 SSL 검증과 인증서 체인
기본값으로 verify=True가 활성화되어 있으니 그대로 두는 것이 안전합니다.
사내 프록시나 자체 서명 인증서를 쓰는 환경이라면 신뢰할 수 있는 CA 번들을 지정해 검증을 유지하세요.
서버 이름 표시(SNI)가 필요한 호스트에서도 세션을 사용하면 안정적으로 처리할 수 있습니다.
import requests, hashlib, os
from pathlib import Path
def secure_download(u: str, dest: Path, expected_sha256: str | None = None):
dest.parent.mkdir(parents=True, exist_ok=True)
tmp = dest.with_suffix(dest.suffix + ".part")
with requests.Session() as s:
s.headers.update({"User-Agent": "secure-requests/1.0"})
# 사내 CA 번들이 있다면 경로 지정 (예: /etc/ssl/certs/ca-bundle.crt)
ca_bundle = os.getenv("REQUESTS_CA_BUNDLE", None)
with s.get(u, stream=True, timeout=(3,30), verify=ca_bundle or True) as resp:
resp.raise_for_status()
h = hashlib.sha256()
with tmp.open("wb") as f:
for chunk in resp.iter_content(1024 * 64):
if chunk:
f.write(chunk)
h.update(chunk)
if expected_sha256 and h.hexdigest().lower() != expected_sha256.lower():
tmp.unlink(missing_ok=True)
raise ValueError("무결성 검증 실패: SHA256 불일치")
tmp.replace(dest)
return dest
🧲 재시도, 백오프, 부분 재개(Range)
일시적인 네트워크 오류는 재시도와 지수 백오프로 극복할 수 있습니다.
또한 중단된 다운로드를 이어받기 위해 Range 헤더를 사용하면 이미 받은 바이트부터 이어서 요청할 수 있습니다.
서버가 Accept-Ranges: bytes를 지원하는지 확인하고, 응답 코드 206 Partial Content를 처리하세요.
import time, requests
def resume_download(url, path, backoff=0.6, max_retry=4):
downloaded = 0
if os.path.exists(path):
downloaded = os.path.getsize(path)
headers = {"Range": f"bytes={downloaded}-"} if downloaded else {}
for attempt in range(1, max_retry + 1):
try:
with requests.Session() as s:
with s.get(url, stream=True, timeout=(3,30), headers=headers) as resp:
resp.raise_for_status()
mode = "ab" if downloaded else "wb"
with open(path, mode) as f:
for chunk in resp.iter_content(1024 * 64):
if chunk:
f.write(chunk)
return path
except (requests.ConnectionError, requests.Timeout):
time.sleep(backoff ** attempt)
raise RuntimeError("재시도 초과로 다운로드 실패")
| 위험 요소 | 대응 방안 |
|---|---|
| SSL 검증 해제 | verify 유지, 신뢰할 수 있는 CA 번들 지정 |
| 파일 위변조 | SHA256 등 해시 검증, 서명 파일 확인 |
| 부분 손상/중단 | Range 재개, .part 임시 파일 사용 |
| 서버 과부하/차단 | 지수 백오프, 합리적 재시도, User-Agent 명시 |
💎 핵심 포인트:
SSL 검증을 해제하지 말고, 해시·크기·헤더 기반 검증을 습관화하세요.
그리고 재시도/백오프/재개 전략을 통해 네트워크 변동에도 복구 가능한 파이프라인을 구성하세요.
⚠️ 주의: 퍼블릭 네트워크에서 민감한 토큰을 쿼리스트링으로 전달하면 로그와 레퍼러에 노출될 수 있습니다.
헤더(Authorization: Bearer …)로 전달하고, 필요 시 짧은 만료 시간을 사용하세요.
- 🔐verify=True 유지, 커스텀 CA 번들 필요 시 환경변수 또는 경로 지정
- 🧾SHA256 해시 검증 및 Content-Length 확인으로 무결성·완전성 점검
- 📶지수 백오프 + 제한된 재시도 횟수로 서버 부담 최소화
- 📥Range 다운로드 지원 시 중단 복구 및 이어받기 구현
❓ 자주 묻는 질문 (FAQ)
파일 다운로드 중 연결이 자주 끊기는데 어떻게 해결할 수 있나요?
또한 Range 헤더를 이용한 부분 재개 기능을 구현하면 중간부터 이어서 다운로드할 수 있습니다.
stream=True를 반드시 써야 하나요?
timeout=(3,30)의 의미가 궁금합니다.
즉, 연결이 3초 내로 이루어지지 않으면 중단하고, 연결된 후 30초 동안 응답이 없으면 실패로 간주합니다.
raise_for_status()는 꼭 필요할까요?
HTTP 오류 코드(4xx, 5xx)가 발생했을 때 자동으로 예외를 발생시켜, 실패를 즉시 감지하고 처리할 수 있습니다.
requests 대신 urllib을 써도 되나요?
하지만 requests는 더 간결한 문법과 세션 재사용, 예외 처리 지원이 우수해 실무에서 훨씬 널리 사용됩니다.
파일 다운로드 중 강제 종료되면 임시 파일은 어떻게 되나요?
재시작 시 해당 크기만큼 이어받기 기능을 구현하면 손상 없이 복구할 수 있습니다.
SSL 검증을 끄면 어떤 문제가 생기나요?
검증은 항상 유지하고, 필요 시 신뢰할 수 있는 CA 인증서를 지정하세요.
requests.get 대신 세션을 써야 하는 이유는 뭔가요?
또한 인증, 쿠키, 헤더 등을 유지하기 때문에 여러 요청을 보낼 때 효율적입니다.
🧭 안정적인 requests 파일 다운로드를 위한 실전 정리
파이썬 requests로 파일을 다운로드할 때는 단순히 get() 요청만으로 끝내기보다 세션과 스트리밍, 예외 처리, 해시 검증 등 여러 안정 장치를 함께 사용하는 것이 핵심입니다.
기본 구문인 with s.get(u, stream=True, timeout=(3,30)) as resp: resp.raise_for_status()는 거의 모든 상황에서 안전하고 확장 가능한 패턴으로, 네트워크 불안정, 서버 오류, 대용량 데이터에도 견고하게 작동합니다.
이 글에서 다룬 구성 요소—세션 재사용, 청크 단위 쓰기, 임시 파일 처리, 재시도 및 백오프 전략, SSL 검증, 해시 무결성 점검—을 조합하면 클라우드 환경이나 CI 파이프라인에서도 손쉽게 자동화할 수 있습니다.
또한 로깅과 예외 복구 루틴을 체계화하면, 다운로드 실패 확률을 현저히 낮출 수 있습니다.
코드를 단순화하면서도 안정성을 놓치지 않기 위해, requests의 이 구성을 표준 패턴으로 기억해 두세요.
🏷️ 관련 태그 : 파이썬다운로드, requests모듈, stream옵션, timeout설정, 파일저장, 세션재사용, 예외처리, SSL검증, 무결성검사, 파이썬자동화