메뉴 닫기

파이썬 requests Range 헤더로 이어받기 구현 206 Partial Content 처리와 Content-Range 검증 가이드

파이썬 requests Range 헤더로 이어받기 구현 206 Partial Content 처리와 Content-Range 검증 가이드

🧩 중단된 다운로드를 안전하게 복구하고 무결성까지 확인하는 실전 패턴을 한 번에 정리합니다

대용량 파일을 내려받다 네트워크가 끊기면 처음부터 다시 시작해야 해서 속이 쓰린 경험이 한두 번이 아니죠.
현장에서 가장 간단하게 쓰이는 해결책이 바로 HTTP Range 헤더를 활용한 부분 요청입니다.
파이썬의 requests 라이브러리는 이 기능을 비교적 쉽게 다룰 수 있지만, 올바른 이어받기를 위해서는 서버의 206 Partial Content 응답을 정확히 처리하고, Content-Range 값이 요청한 범위와 일치하는지 세심하게 확인해야 합니다.
이 글은 그 핵심을 빠뜨리지 않고, 신뢰할 수 있는 재시도 전략과 파일 검증까지 곁들여 개발과 운영 모두에서 바로 적용할 수 있도록 도와드립니다.

특히 클라우드 스토리지나 CDN처럼 다양한 서버 구현을 상대할 때는 예외가 잦고, 잘못된 이어붙이기로 파일이 손상되는 일이 드물지 않습니다.
따라서 Range 헤더로 재개 요청을 보낼 때의 포맷, 서버가 206 상태코드와 함께 반환하는 Content-Range 헤더의 의미, 그리고 범위 불일치 시 어떻게 복구 루틴을 타야 하는지가 중요합니다.
여기서는 기본 원리부터 코드 구조, 검증 로직, 문제 상황별 대응 순서까지 실제 프로젝트에 가져다 쓰기 좋은 형태로 정리해 드립니다.



🔗 Range 헤더로 부분 요청의 원리

HTTP의 Range 메커니즘은 서버에 전체가 아닌 특정 바이트 구간만 달라고 요구하는 방식입니다.
네트워크가 끊겨도 이미 받은 구간을 유지한 채 남은 부분만 이어받을 수 있어, 대용량 파일 전송에서 낭비를 크게 줄여 줍니다.
요청은 클라이언트가 Range 헤더로 바이트 범위를 지정하고, 서버는 206 Partial Content와 함께 해당 범위를 반환하는 구조로 동작합니다.
이때 응답 헤더의 Content-Range는 실제로 전송된 구간과 전체 크기를 명시해 이어붙이기 무결성을 보장하는 핵심 근거가 됩니다.

CODE BLOCK
GET /file.iso HTTP/1.1
Host: example.com
Range: bytes=1048576-        # 1 MiB 이후부터 끝까지 요청

HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 1048576-3145727/3145728
Content-Length: 2097152
Content-Type: application/octet-stream

... <전송 바이트: 1048576~3145727> ...

Range의 단위는 보통 bytes이며, bytes=start-end 형식을 따릅니다.
end를 생략하면 끝까지 전송하고, bytes=-N처럼 뒤에서 N바이트를 요청할 수도 있습니다.
서버가 범위를 이해하고 지원하면 206 Partial Content로 응답하고, 지원하지 않으면 전체를 200 OK로 돌려주거나 범위가 유효하지 않을 때 416 Range Not Satisfiable을 보냅니다.
이어붙이기 구현에서는 단순히 206 여부만 보지 말고, Content-Range가 요청한 구간과 정확히 맞는지 반드시 대조해야 합니다.

항목1 항목2
Range 요청 헤더 Range: bytes=start-end (예: bytes=0-1023, bytes=1048576-)
필수 응답 요소 상태코드 206, Content-Range: bytes start-end/total, Content-Length
지원 신호 Accept-Ranges: bytes (지원 여부 힌트)
오류 시나리오 416 Range Not Satisfiable, 200 OK로 전체 재전송, Content-Range 불일치
  • 🛠️Accept-Ranges: bytes가 보이는지 확인해 부분 전송 지원 여부를 가늠합니다.
  • ⚙️이어받기 시작 오프셋은 이미 저장된 파일 크기(os.path.getsize)로 정합니다.
  • 🔌응답 상태가 206인지, Content-Range start가 요청한 오프셋과 같은지 대조합니다.
  • 🧾ETag/Last-Modified가 바뀌면 파일이 교체된 것이므로 이어받기 대신 전체 재다운로드로 전환합니다.

💬 Range의 본질은 ‘필요한 구간만 안전하게 받기’입니다.
따라서 206 Partial Content와 Content-Range의 일치 여부를 검증하는 절차가 빠지면 이어붙이기 무결성이 깨질 수 있습니다.

💡 TIP: CDN이나 프록시 환경에서는 캐시 무효화로 인해 ETag가 바뀌는 경우가 있습니다.
이때는 이어받기 요청이 200 OK 전체 응답으로 바뀔 수 있으니, 파일 크기와 Content-Range를 함께 점검해 안전하게 분기하세요.

⚠️ 주의: 서버가 Range를 지원하지 않는데 부분 요청을 강행하면, 200 OK 전체 응답을 이어붙여 파일이 손상될 수 있습니다.
항상 상태코드와 Content-Range 유효성을 먼저 확인하세요.

🛠️ requests로 이어받기 구현 패턴

파이썬에서 파일 이어받기를 구현할 때 핵심은 requests.get() 요청에 Range 헤더를 직접 추가하고, 응답을 스트리밍으로 처리하는 것입니다.
이를 통해 메모리에 전부 올리지 않고도 파일을 순차적으로 쓸 수 있습니다.
먼저 파일의 현재 크기를 구한 뒤, 그 크기 이후의 바이트부터 요청을 시작하면 이어받기가 가능합니다.
다만, 서버가 206 Partial Content로 응답하지 않으면 전체 다운로드로 복구해야 하며, 파일이 이미 존재하는 경우에는 덮어쓰기나 오프셋 재검증 로직이 반드시 필요합니다.

CODE BLOCK
import os
import requests

url = "https://example.com/largefile.zip"
local_path = "largefile.zip"

# 이미 내려받은 파일 크기 확인
resume_byte_pos = os.path.getsize(local_path) if os.path.exists(local_path) else 0
headers = {"Range": f"bytes={resume_byte_pos}-"}

# 스트리밍 다운로드 시작
with requests.get(url, headers=headers, stream=True) as r:
    if r.status_code == 206:
        mode = "ab"   # 이어붙이기 모드
    else:
        mode = "wb"   # 전체 재다운로드

    with open(local_path, mode) as f:
        for chunk in r.iter_content(chunk_size=8192):
            if chunk:
                f.write(chunk)

위 코드에서 Range 헤더는 다운로드를 중단한 지점부터 다시 요청하도록 설정합니다.
서버가 206 Partial Content로 응답하면 append 모드(ab)로 이어붙이고, 그렇지 않으면 새로운 파일로 전부 덮어씁니다.
이때 iter_content()를 이용해 청크 단위로 데이터를 받아야 메모리 사용량을 최소화할 수 있습니다.

⚙️ 파일 이어붙이기 시점의 유효성 점검

이어붙이기를 실행하기 전에 반드시 기존 파일의 상태를 검증해야 합니다.
파일이 부분적으로 손상되거나 서버의 파일이 갱신된 경우, 단순 이어받기로는 오히려 파일이 깨질 수 있기 때문입니다.
이를 방지하려면 ETagLast-Modified 헤더를 확인해 이전에 저장한 메타데이터와 비교하는 것이 좋습니다.

CODE BLOCK
etag = r.headers.get("ETag")
last_modified = r.headers.get("Last-Modified")

# 저장된 정보와 비교
if etag and stored_etag and etag != stored_etag:
    print("서버 파일이 변경됨 → 전체 재다운로드 필요")
if last_modified and stored_modified and last_modified != stored_modified:
    print("파일 갱신 감지 → 이어받기 중단")

이러한 유효성 점검을 거쳐야 다운로드가 안전하게 이어지며, 요청한 구간이 올바르게 반영되었는지 Content-Range 헤더로 추가 검증하면 무결성을 확실히 보장할 수 있습니다.

💎 핵심 포인트:
Range 헤더는 단순히 이어받기 요청이 아니라, 서버와 파일 버전이 동일한지 확인한 후 신뢰할 수 있는 바이트 범위만 요청해야 완전한 복구가 가능합니다.



⚙️ 206 Partial Content 응답 처리

서버가 Range 요청을 정상적으로 처리했다면 응답 코드는 206 Partial Content가 됩니다.
이 상태코드는 이어받기 동작의 핵심 신호이며, Content-Range 헤더를 통해 실제 전송된 범위를 명확하게 나타냅니다.
파일 이어받기 코드를 작성할 때는 200, 206, 416 등 세 가지 상태에 따른 분기 처리가 필요합니다.
특히 206이 아닐 경우, 기존 파일을 덮어쓰거나 삭제 후 재시도하는 로직이 반드시 포함되어야 합니다.

CODE BLOCK
if r.status_code == 206:
    print("부분 다운로드 성공: 이어붙이기 진행")
elif r.status_code == 200:
    print("Range 미지원 → 전체 다운로드 수행")
elif r.status_code == 416:
    print("요청한 범위 초과 → 파일 크기 불일치, 전체 재다운로드 필요")
else:
    print(f"예상치 못한 상태코드: {r.status_code}")

서버가 Range 요청을 지원하지 않으면 200 OK로 전체 응답을 반환합니다.
이 경우 이어붙이기를 시도하면 파일이 손상되므로, 반드시 r.status_code == 206 조건을 만족할 때만 이어받기를 허용해야 합니다.
또한 416 오류가 발생할 때는 로컬 파일 크기와 서버 파일의 전체 크기(Content-Range의 total 값)가 다르기 때문에 파일이 변조되었거나 서버에서 교체된 것으로 간주하고, 즉시 새로 내려받아야 합니다.

📡 응답 헤더를 통한 구간 검증

206 응답을 받은 뒤에는 반드시 Content-Range 헤더의 값이 요청한 바이트 구간과 일치하는지 점검해야 합니다.
이 정보가 틀리면 이어붙인 파일의 경계가 어긋나 데이터가 손상될 수 있습니다.
헤더 포맷은 일반적으로 다음과 같습니다.

CODE BLOCK
Content-Range: bytes 1048576-2097151/4194304

위 예시에서 start=1048576, end=2097151, total=4194304로 해석됩니다.
요청한 Range 값과 비교해 start가 동일한지, total이 전체 크기와 일관된지 확인하면 됩니다.

💡 TIP: r.headers[‘Content-Range’]에서 total 크기를 파싱해 전체 파일 크기를 미리 예측할 수 있습니다.
이를 활용하면 진행률 표시나 남은 다운로드 크기 계산도 간단히 구현할 수 있습니다.

  • 📦206 상태코드를 받았는지 확인하고 나서 이어붙이기를 실행합니다.
  • 🧩Content-Range의 시작 바이트가 요청 구간과 일치하는지 검증합니다.
  • 📊전체 크기(total)가 이전에 저장한 크기와 다르다면 파일 변경으로 판단합니다.
  • 🧱416 오류는 잘못된 Range 요청을 의미하므로 전체 재다운로드로 전환합니다.

💬 206 Partial Content는 단순한 성공 응답이 아닙니다.
이 신호를 기준으로 정확한 이어받기 검증을 수행해야 비로소 ‘신뢰 가능한 다운로드’가 완성됩니다.

🧪 Content-Range 검증과 예외 대응

206 응답이 왔다고 해서 무조건 안전한 이어받기가 가능한 것은 아닙니다.
서버가 반환한 Content-Range 값이 요청한 구간과 다르거나, 전체 파일 크기(total)가 바뀌었다면 이어붙이기 결과가 틀어질 수 있습니다.
이 경우 파일이 손상되거나 중복 저장이 발생할 수 있기 때문에, 응답의 Content-Range를 직접 파싱해 검증하는 절차가 필요합니다.

CODE BLOCK
import re

content_range = r.headers.get("Content-Range")  # 예: 'bytes 1048576-2097151/4194304'

if content_range:
    m = re.match(r"bytes (\d+)-(\d+)/(\d+)", content_range)
    if m:
        start, end, total = map(int, m.groups())
        if start != resume_byte_pos:
            print("⚠️ 요청 구간과 Content-Range 불일치 → 전체 재다운로드 권장")
        else:
            print(f"부분 다운로드 확인: {start}-{end}/{total}")
    else:
        print("⚠️ Content-Range 형식 오류 → 검증 실패")

서버가 잘못된 범위를 반환하는 경우는 의외로 자주 발생합니다.
특히 프록시나 CDN 캐시가 동작할 때 Range 헤더가 무시되는 사례가 있으며, 클라우드 저장소(Google Drive, AWS S3 등)에서도 범위 시작점이 강제로 0으로 맞춰지는 버그가 보고된 바 있습니다.
이럴 때는 단순히 이어받기를 멈추는 것이 아니라, Content-Range 불일치 여부를 로그로 남기고 전체 재요청으로 복구하는 것이 좋습니다.

🧰 예외 상황별 대응 전략

상황 처리 방법
Content-Range 없음 206 응답이라도 무효, 전체 재다운로드로 전환
start 오프셋 불일치 기존 파일 삭제 후 새로 다운로드
total 크기 변경 파일 버전 변경으로 판단, 이어받기 중단
206이 아닌 200 OK Range 미지원 서버, 전체 덮어쓰기
416 Range Not Satisfiable 파일 크기 초과, resume 오프셋 조정 또는 재시작

이 검증 절차를 거치면 예상치 못한 오류나 손상된 데이터를 방지할 수 있습니다.
특히 자동화된 다운로드 프로그램에서는 예외 발생 시 즉시 전체 재시작 루틴으로 전환해야 안정성이 유지됩니다.

💎 핵심 포인트:
Content-Range 헤더는 이어붙이기의 ‘진실의 기준’입니다.
이 값이 요청 범위와 맞지 않으면 절대 이어받기를 시도하지 말고, 즉시 전체 재다운로드로 전환해야 합니다.

⚠️ 주의: 파일 이어붙이기 중 예외 처리를 생략하면, 재시작 시점이 엉켜서 파일이 중복되거나 손상될 수 있습니다.
항상 Range 요청 검증 후에만 append 모드를 사용하세요.



🚀 대용량 파일·멀티스레드 다운로드 팁

수백 MB에서 수 GB를 넘는 파일은 단일 연결보다 여러 Range 요청을 병렬로 보내는 방식이 더 빠른 경우가 많습니다.
단, 스레드를 늘리기 전에 서버가 바이트 범위 전송을 안정적으로 지원하는지 확인하고, 각 청크의 206 Partial ContentContent-Range를 검증해야 무결성을 지킬 수 있습니다.
또한 너무 작은 조각은 오버헤드를 키우고, 너무 큰 조각은 실패 시 재시도 비용이 커집니다.
네트워크 품질과 서버 특성에 맞춘 적절한 청크 크기와 동시성 수준을 찾는 것이 핵심입니다.

🧵 멀티스레드 Range 다운로드 기본 구조

CODE BLOCK
import os, math, re, requests
from concurrent.futures import ThreadPoolExecutor, as_completed

URL = "https://example.com/big.bin"
OUT = "big.bin"
CHUNK = 8 * 1024 * 1024   # 8 MiB 권장 시작점
MAX_WORKERS = 6

# 전체 크기 확인 (헤더만)
h = requests.head(URL, allow_redirects=True)
total = int(h.headers.get("Content-Length", "0"))
assert total > 0, "총 파일 크기를 알 수 없습니다."

# 작업 범위 생성
jobs = []
for i in range(0, total, CHUNK):
    start = i
    end = min(i + CHUNK - 1, total - 1)
    jobs.append((start, end))

def fetch_part(idx, start, end, retry=3):
    headers = {"Range": f"bytes={start}-{end}"}
    for attempt in range(1, retry + 1):
        r = requests.get(URL, headers=headers, stream=True, timeout=30)
        if r.status_code != 206:
            # 200/416 등: 이어받기 불가 → 실패 처리
            r.close()
            continue
        cr = r.headers.get("Content-Range", "")
        m = re.match(r"bytes (\d+)-(\d+)/(\d+)", cr or "")
        if not m:
            r.close(); continue
        s, e, t = map(int, m.groups())
        if s != start or e != end or t != total:
            r.close(); continue
        # 임시 파일로 저장
        part_name = f"{OUT}.part{idx}"
        with open(part_name, "wb") as f:
            for chunk in r.iter_content(1024 * 64):
                if chunk:
                    f.write(chunk)
        return part_name
    raise RuntimeError(f"청크 다운로드 실패: {start}-{end}")

# 병렬 다운로드
parts = [None] * len(jobs)
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as ex:
    futs = {ex.submit(fetch_part, i, s, e): i for i, (s, e) in enumerate(jobs)}
    for fut in as_completed(futs):
        i = futs[fut]
        parts[i] = fut.result()

# 합치기 (순서 보장)
with open(OUT, "wb") as out:
    for p in parts:
        with open(p, "rb") as part:
            out.write(part.read())
        os.remove(p)

print("다운로드 완료:", OUT, total, "bytes")

이 구조의 포인트는 각 파트가 요청 범위와 동일한 Content-Range를 돌려주는지 검증한 뒤, 임시 파일로 안전하게 저장하고 마지막에 순서대로 머지하는 것입니다.
한 번의 실패로 전체를 다시 받지 않도록 파트 단위 재시도를 적용하고, 최종 머지 전에 누락된 파트가 없는지 검사하면 안정성이 크게 올라갑니다.

⚖️ 청크 크기와 동시성 튜닝

일반적으로 4~16 MiB 범위에서 시작해 네트워크 대역, 서버의 단일 연결 제한, 지연시간을 고려해 조절합니다.
청크가 작을수록 실패 시 재시도 비용이 낮아지지만, HTTP 오버헤드와 스레드 스케줄링 비용이 증가합니다.
반대로 청크가 너무 크면 부분 실패 시 재다운로드 낭비가 커집니다.
동시성은 4~8개에서 시작해 서버의 스로틀링(429/503)을 관찰하며 조정하는 방법이 안전합니다.

  • 📐모든 파트에서 206과 올바른 Content-Range를 확인합니다.
  • 🔁파트 단위 재시도와 지수 백오프를 적용합니다(예: 0.5s, 1s, 2s).
  • 🧭ETag/Last-Modified를 병행 점검해 서버 파일 교체 여부를 감지합니다.
  • 🧱429/503 응답 시 즉시 동시성 내려 재시도합니다.
  • 🧮합치기 전에 모든 파트 파일 크기의 합이 Content-Length와 일치하는지 확인합니다.

⚠️ 주의: 일부 서버나 CDN은 과도한 병렬 Range 요청을 차단하거나 속도를 제한합니다.
비정상 응답(예: 200 OK로 회귀, Content-Range 누락)이 관측되면 즉시 단일 스트림이나 낮은 동시성으로 전환하세요.

💎 핵심 포인트:
멀티스레드의 이득은 ‘안전한 검증’ 위에서만 성립합니다.
각 파트의 Content-Range를 확인하고, 실패는 파트 단위로 격리·재시도하는 구조가 대용량에서도 흔들리지 않는 이어받기를 보장합니다.

자주 묻는 질문 (FAQ)

requests에서 Range 헤더를 설정했는데 200 OK가 왔다면 실패인가요?
이어받기로 간주할 수 없습니다.
200 OK는 서버가 범위 전송을 적용하지 않고 전체를 반환하겠다는 의미입니다.
이 응답을 기존 파일 뒤에 붙이면 손상이 발생합니다.
이 경우에는 기존 파일을 새로 만들고 전체 재다운로드로 전환하는 분기 처리가 필요합니다.
206 Partial Content면 항상 안전하게 이어붙일 수 있나요?
아닙니다.
206이더라도 Content-Range가 요청한 시작 오프셋과 정확히 일치하는지 반드시 확인해야 합니다.
시작점이나 total 값이 다르면 파일 버전이 바뀌었거나 캐시가 잘못된 범위를 반환한 가능성이 있으므로 전체 재다운로드로 복구하는 편이 안전합니다.
Accept-Ranges 헤더가 없으면 Range를 지원하지 않는 건가요?
힌트일 뿐 절대적 기준은 아닙니다.
일부 서버는 Accept-Ranges를 노출하지 않아도 Range 요청을 처리하기도 합니다.
가장 확실한 방법은 실제로 Range를 포함해 요청을 보내고 206과 올바른 Content-Range가 오는지 확인하는 것입니다.
ETag나 Last-Modified는 왜 이어받기에서 중요하죠?
이어받기는 같은 파일의 연속 바이트를 합치는 작업입니다.
다운로드 도중 서버의 파일이 바뀌면 이어붙인 결과가 섞여 손상됩니다.
ETag 또는 Last-Modified를 이전 요청과 비교하면 파일 교체 여부를 쉽게 감지할 수 있고, 바뀐 경우 전체 재다운로드로 전환해 무결성을 지킬 수 있습니다.
416 Range Not Satisfiable은 어떤 때 발생하고 어떻게 처리하나요?
주로 로컬 파일 크기가 서버의 실제 total보다 크거나, 요청 시작 오프셋이 total을 초과했을 때 발생합니다.
이어받기 대상이 달라졌을 가능성이 높으므로 로컬 파일을 삭제하거나 이름을 바꾼 뒤 0부터 전체 다운로드를 다시 수행하는 것이 안전합니다.
gzip 압축 전송(Content-Encoding)이 켜져 있으면 Range 이어받기에 문제가 되나요?
가능합니다.
전송 중 압축이 적용되면 바이트 범위가 원본 파일 기준이 아니라 전송된 스트림 기준으로 해석될 수 있어 이어붙이기가 깨질 수 있습니다.
대용량 바이너리 다운로드에서는 Accept-Encoding을 identity로 제한하거나, 서버가 바이트 범위 전송에서 압축을 비활성화하도록 구성하는 것이 좋습니다.
멀티스레드 Range 다운로드 시 파트 합치기 전에 무엇을 검증해야 하나요?
각 파트에서 206 여부와 Content-Range의 start·end·total 일치성을 확인해야 합니다.
또한 모든 파트 파일 크기의 합이 전체 크기와 일치하는지, 누락된 인덱스가 없는지 점검합니다.
해시 검증(SHA-256 등) 값이 제공된다면 최종 병합본에 대해 체크섬을 계산해 비교하면 무결성을 확실히 보장할 수 있습니다.
프록시나 CDN 뒤의 서버에서 Range가 가끔 0부터 시작하는 문제가 있어요. 우회 방법이 있을까요?
첫째, 요청마다 Cache-Control: no-cache 또는 revalidate 지시자를 추가해 오래된 캐시를 피합니다.
둘째, 잘못된 Content-Range가 오면 즉시 실패로 간주하고 단일 스트림으로 폴백합니다.
셋째, 가능한 경우 원본 오리진 도메인으로 직접 요청하거나 서밋 헤더(If-Range)에 ETag를 포함해 일관성을 강화합니다.

📘 파일 이어받기 구현 핵심 정리

Range 헤더는 HTTP가 제공하는 가장 단순하면서도 강력한 복구 메커니즘입니다.
파이썬 requests로 이를 구현할 때는 206 Partial Content 응답을 기준으로 이어받기를 판단하고, Content-Range를 반드시 검증해야 합니다.
파일 이어붙이기 전 ETag나 Last-Modified를 비교하고, 불일치 시 전체 재다운로드로 전환하는 습관이 중요합니다.
멀티스레드 환경에서는 각 파트를 독립적으로 검증해 합치는 구조가 필수이며, 실패한 파트는 개별 재시도로 안정성을 확보합니다.

서버가 Range 요청을 완벽히 지원하지 않거나 CDN이 캐시 문제를 일으킬 때는, 단일 스트림으로 폴백하거나 If-Range 헤더를 추가해 무결성을 강화하세요.
결국 이어받기의 핵심은 “속도보다 정확성”이며, 검증을 생략한 빠른 다운로드보다 완전한 데이터가 훨씬 더 값집니다.

💎 핵심 포인트:
206 상태와 Content-Range 일치 여부를 검증하고, 이어붙이기는 항상 신뢰 가능한 구간에서만 수행해야 합니다.
이 원칙만 지켜도 어떤 네트워크 환경에서도 안전하게 이어받기를 구현할 수 있습니다.


🏷️ 관련 태그 :
파이썬requests, Range헤더, 206PartialContent, ContentRange검증, 파일이어받기, HTTP다운로드, CDN캐시, 네트워크복구, 대용량파일다운로드, 멀티스레드다운로드