용량이 큰 오프라인 리소스를 내려받아야 하는 iOS App을 원격으로 개발할 때, 네트워크가 끊긴 뒤 진행 막대가 이전 위치에서 다시 움직인다고 해서 클라이언트가 이미 받은 바이트를 실제로 재사용했다고 볼 수는 없습니다. 화면에만 기존 진행률을 유지한 채 처음부터 다시 내려받았을 수도 있습니다. 클라우드 Mac에서는 시뮬레이터가 같은 Mac의 로컬 서버에 접속하도록 구성하고, 서버 기록과 파일 검증으로 재현 가능한 테스트를 할 수 있습니다. 이 과정에는 공개 인터넷의 파일 서비스가 필요하지 않습니다.
먼저 성공 조건 정의하기
이번 테스트는 파일 하나와 단일 바이트 범위만 다루며, 병렬 분할 다운로드는 포함하지 않습니다. 민감한 데이터가 없고 다운로드 도중 취소할 수 있을 만큼 충분히 큰 파일을 준비합니다. 첫 요청에는 200이 반환되어야 합니다. 클라이언트가 유효한 재개 정보를 보관했다면 이후 요청에 Range: bytes=N-가 포함될 수 있으며, 서버는 206과 정확한 Content-Range를 반환합니다. 최종 파일의 길이와 SHA-256은 원본 파일과 일치해야 합니다.
resumeData는 클라이언트가 다운로드 재개를 시도하는 데 필요한 데이터이지, 서버가 이미 부분 응답을 제공했다는 증거가 아닙니다. 취소했다고 해서 항상 사용할 수 있는resumeData가 생성되는 것도 아닙니다.
먼저 전용 테스트 디렉터리에서 파일을 만들고 비교 기준을 기록합니다. 파일 크기는 수동으로 취소할 시간을 확보하기 위한 것일 뿐, 제품 사양이나 성능 지표가 아닙니다.
mkdir -p ~/range-lab && cd ~/range-lab
python3 - <<'PY'
with open("fixture.bin", "wb") as out:
for _ in range(64):
out.write(b"XCZone-range-check" * 65536)
PY
shasum -a 256 fixture.bin
stat -f '%z bytes' fixture.bin
로컬에서만 접속할 수 있는 Range 서버 실행하기
아래의 최소 서버는 현재 디렉터리의 fixture.bin만 제공합니다. 파일의 ETag를 생성하고 지정한 오프셋부터 파일 끝까지 읽는 요청을 지원하며, 지원하지 않는 Range 요청에는 416을 반환합니다. 시뮬레이터에서 다운로드를 중단하기 쉽도록 각 블록을 쓴 뒤 잠시 멈춥니다. 공개 배포용 파일 서버는 아닙니다.
from hashlib import sha256
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from time import sleep
import re
path = Path("fixture.bin")
size = path.stat().st_size
etag = '"' + sha256(path.read_bytes()).hexdigest() + '"'
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != "/fixture.bin":
self.send_error(404)
return
requested = self.headers.get("Range")
if self.headers.get("If-Range") not in (None, etag):
requested = None
start = 0
if requested:
match = re.fullmatch(r"bytes=(\d+)-", requested)
if not match or int(match.group(1)) >= size:
self.send_response(416)
self.send_header("Content-Range", f"bytes */{size}")
self.end_headers()
return
start = int(match.group(1))
self.send_response(206 if requested else 200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Accept-Ranges", "bytes")
self.send_header("ETag", etag)
self.send_header("Content-Length", str(size - start))
if requested:
self.send_header("Content-Range", f"bytes {start}-{size - 1}/{size}")
self.end_headers()
with path.open("rb") as source:
source.seek(start)
while chunk := source.read(65536):
try:
self.wfile.write(chunk)
self.wfile.flush()
except (BrokenPipeError, ConnectionResetError):
break
sleep(0.01)
HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()
range_server.py로 저장하고 해당 디렉터리에서 python3 range_server.py를 실행합니다. 다른 터미널을 열어 먼저 curl -i -r 1024-2047 http://127.0.0.1:8765/fixture.bin -o /dev/null로 상태 코드와 응답 헤더를 확인합니다. 이 명령의 출력에 전송 진행률이 표시될 수 있지만, 그것만으로 응답 내용을 판단하지 마세요. curl -sD - -r 1024-2047 -o /dev/null http://127.0.0.1:8765/fixture.bin을 사용하면 206, Content-Range: bytes 1024-2047/…, Content-Length를 명확히 확인할 수 있습니다. 테스트가 끝나면 키보드 인터럽트로 서버를 종료하고, 외부에서 접근할 수 있는 네트워크 인터페이스로 수신 주소를 변경하지 마세요.
시뮬레이터에서 중단하고 재개하기
테스트용 App에서 URLSessionDownloadTask로 http://127.0.0.1:8765/fixture.bin을 다운로드하도록 설정합니다. 시뮬레이터는 실행 중인 Mac과 이 로컬 테스트 경로를 공유합니다. 실제 기기로 바꾸면 127.0.0.1은 휴대전화 자신을 가리키므로 같은 주소를 그대로 사용할 수 없습니다. 평문 HTTP에 대한 예외가 필요하다면 별도의 테스트 구성에서만 네트워크 보안 권한을 설정하고, 정식 빌드에는 해당 예외가 없는지 확인합니다.
다운로드가 시작되면 cancel(byProducingResumeData:)를 호출하고 콜백에서 받은 데이터를 저장한 다음, downloadTask(withResumeData:)로 새 작업을 만듭니다. 서버 터미널 출력을 보관하면서 요청별 Range, 응답 상태, 최종 파일 크기를 기록합니다. 먼저 일반 다운로드가 완료되는지 확인한 뒤 중단 테스트를 진행하세요. 그렇지 않으면 연결 문제와 재개 문제가 뒤섞일 수 있습니다.
재개와 처음부터 다시 다운로드하기 구분하기
두 번째 요청에 Range가 없고 서버가 200을 반환했다면, 클라이언트가 전체 다운로드를 다시 시작했을 수 있습니다. 이는 부분 다운로드 재개에 성공한 것이 아닙니다. 요청에 Range가 있더라도 파일의 ETag가 바뀌어 전체 응답을 받았다면 파일을 처음부터 써야 합니다. 어느 경우든 전체 응답을 기존 조각 끝에 이어 붙여서는 안 됩니다. 테스트 코드에서는 취소 후 resumeData를 얻지 못하는 경우도 다뤄야 합니다. 작업을 ‘재개 대기’ 상태에 방치하지 말고 명시적으로 처음부터 다시 시작할 수 있어야 합니다.
파일 검증으로 결과 확정하기
다운로드 작업이 완료되면 임시 파일을 테스트 디렉터리에 복사하고 shasum -a 256 下载后的文件路径를 실행해 원본 파일의 해시와 정확히 비교합니다. 파일 길이도 함께 확인합니다. 206은 서버가 일부 데이터를 반환했다는 뜻일 뿐입니다. 해시가 일치하지 않는다면 오프셋과 파일 쓰기 방식, 두 요청 사이에 파일이 변경되었는지를 점검해야 합니다.
마지막으로 반례도 테스트합니다. 다운로드를 중단한 뒤 원본 파일을 수정하고 테스트 서버를 다시 시작해 ETag를 바꿉니다. 클라이언트가 이전 파일의 조각을 새 파일의 앞부분으로 취급해서는 안 됩니다. 요청 기록, 상태 코드, 파일 길이, 해시를 하나의 테스트 기록에 모아 두면 진행 막대 화면만 캡처했을 때보다 회귀 문제를 파악하기 쉽습니다.
자주 묻는 질문
206 응답만 받으면 재개가 성공한 건가요?
아닙니다. Range 시작 위치와 최종 파일 길이, SHA-256까지 일치해야 합니다.
재개 요청에 서버가 200을 반환하면 어떻게 하나요?
전체 응답으로 취급해 처음부터 저장하거나 다시 다운로드해야 합니다. 기존 조각에 이어 붙이면 안 됩니다.
원격으로 접속할 독점 물리 Mac이 필요하신가요?
M4 Start, M4 Work, M4 Pro의 사양과 노드, 결제 주기를 비교하고 작업에 맞는 플랜을 선택하세요.