재개 가능한 업로드: 세션, Content-Range, S3 멀티파트
들어가며
5GB 영상을 올리다 90%에서 네트워크가 끊겼습니다. 처음부터 다시 보내면 4.5GB 전송이 통째로 사라지고, 불안정한 네트워크에서는 끝내 완료하지 못할 수도 있습니다.
이번 글에서는 끊긴 지점부터 이어 보내는 재개 가능한 업로드의 동작을 정리합니다. 세션과 받은 위치를 다루는 방법, 오류별 처리, S3 멀티파트 방식과의 차이까지 다룹니다.
1. 재개의 핵심은 서버가 위치를 기억하는 것
재개 가능한 업로드는 파일을 조각으로 보내고, 서버가 어디까지 받았는지 기억하게 하는 방식입니다. 연결이 끊기면 클라이언트는 받은 위치를 물어 그다음 바이트부터 이어 보냅니다.
재개 가능한 업로드는 긴 책을 쪽 번호를 확인하며 나눠 보내는 일이에요. 끊겨도 받은 쪽 다음부터 보내요.
- 4.5GB 전송 후 끊김
- 받은 데이터 폐기
- 다시 끊기면 또 처음부터
- 4.5GB 전송 후 끊김
- 받은 위치 확인
- 남은 0.5GB만 전송
대가도 있습니다. 조각마다 왕복이 생기고, 세션을 저장하는 서버 쪽 상태와 조각 크기, 세션 만료까지 다뤄야 합니다. 작은 파일이라면 한 번에 보내고 실패 시 다시 보내는 편이 단순합니다.
2. 세션과 Content-Range
Google Drive API 를 기준으로 흐름을 보겠습니다. 먼저 POST 로 세션을 열면 응답의 Location 헤더로 세션 URI 를 받습니다. 이후 조각과 위치 질의는 모두 이 주소로 보냅니다.
세션 URI는 택배 접수 번호예요. 번호만 대면 어디까지 받았는지 알려 줘요.
POST /upload/drive/v3/files?uploadType=resumable
→ 200 OK
Location: <세션 URI>
PUT <세션 URI>
Content-Range: bytes 0-8388607/52428800
→ 308 Resume Incomplete
Range: bytes=0-8388607
PUT <세션 URI> # 끊긴 뒤 위치 질의 (본문 없음)
Content-Range: bytes */52428800
→ 308 Resume Incomplete
Range: bytes=0-8388607여기서 308 은 표준 리다이렉트가 아니라, 아직 덜 받았다는 뜻으로 Google API 가 쓰는 응답입니다. Range 헤더는 지금까지 저장한 범위를 알려 주므로 위 예에서는 8388608 바이트부터 이어 보냅니다.
이때 보낸 만큼 받았다고 가정하면 안 됩니다. 끊긴 조각은 일부만 저장됐을 수 있으므로 위치는 항상 서버에 물어 정합니다.
def upload(path, uri, total):
pos = query_offset(uri, total) # 서버에 받은 위치를 묻는다
with open(path, "rb") as f:
while pos < total:
f.seek(pos)
chunk = f.read(CHUNK_SIZE)
try:
res = put_chunk(uri, chunk, pos, total)
except ConnectionError:
sleep_with_backoff()
pos = query_offset(uri, total)
continue
if res.status_code == 308:
pos = next_offset(res.headers.get("Range"))
else:
res.raise_for_status()
return res.json()Range 헤더가 없으면 아직 받은 것이 없다는 뜻이므로 next_offset 은 0을 돌려줘야 합니다. 조각 크기를 키우면 왕복은 줄지만 끊겼을 때 다시 보낼 양이 늘어납니다. Drive API 는 256KB 배수를 요구하므로 API 별 제약도 확인합니다.
3. 오류별 처리
오류가 났다고 모두 재시도하면 안 됩니다. 일시적 오류와 세션이 사라진 오류는 대응이 다릅니다.
끊기면 접수처에 먼저 전화해 확인하고, 접수 번호가 사라졌으면 새로 접수해요.
5xx 나 연결 끊김은 지수 백오프로 기다린 뒤 위치를 다시 묻습니다. 반면 세션이 만료돼 404 나 410 같은 응답이 오면(서비스마다 다릅니다) 같은 세션으로 재시도해도 소용없습니다. 새 세션을 열고 처음부터 보냅니다.
업로드 도중 원본 파일이 바뀌면 앞뒤 조각이 섞인 파일이 완성될 수 있습니다. 그래서 시작 전에 크기와 수정 시각을 기록해 두고, 완료 후에는 전체 해시를 대조합니다.
4. S3 멀티파트 업로드
S3 는 접근이 조금 다릅니다. 멀티파트 업로드는 번호를 붙인 조각을 각각 독립된 요청으로 올리고, 마지막에 완료 요청으로 합칩니다. 조각을 순서대로 보낼 필요가 없어 병렬 전송이 가능합니다.
멀티파트는 짐을 여러 트럭에 번호 붙여 나눠 싣고, 도착 뒤 번호순으로 조립하는 일이에요.
POST /video.mp4?uploads → UploadId
PUT /video.mp4?partNumber=1&uploadId=abc → ETag
PUT /video.mp4?partNumber=2&uploadId=abc → ETag
POST /video.mp4?uploadId=abc # 번호와 ETag 목록으로 완료끊기면 실패한 조각만 다시 올리고, 받은 조각 목록은 ListParts 로 확인합니다. 완료하지 않은 조각은 저장 비용으로 계속 남으므로 수명 주기 규칙의 AbortIncompleteMultipartUpload 로 치웁니다.
한눈에 비교
| 세션 방식 | 멀티파트 | |
|---|---|---|
| 대표 예 | Google Drive API, tus 프로토콜 | Amazon S3 |
| 전송 순서 | 순서대로 | 병렬 가능 |
| 재개 방법 | 받은 위치를 질의 | 실패한 조각만 재전송 |
| 주의할 점 | 세션 만료 | 미완료 조각 정리 |
헷갈리기 쉬운 점
- 보낸 바이트만큼 저장됐다는 오해. 마지막 조각은 일부만 저장됐을 수 있으므로 서버에 물어본 위치를 씁니다.
- 모든 오류를 같은 세션으로 재시도한다는 오해. 세션이 만료됐다면 새 세션을 열어야 합니다.
- 재개 가능한 업로드면 중복과 손상을 다 막는다는 오해. 이어 보내기는 전송만 다룹니다. 완료 뒤 파일 해시 대조와 요청 중복 처리는 따로 필요합니다.
면접에서 자주 받는 질문
세션이 만료되면 어떻게 하나요?
이어 보낼 곳이 사라졌으므로 새 세션을 열고 처음부터 보냅니다. 이 오류는 재시도해도 소용없으니 일시적 오류의 지수 백오프와 구분해 처리합니다.
세션 방식과 S3 멀티파트는 어떻게 고르나요?
쓰는 저장소가 제공하는 방식을 따르는 경우가 많습니다. 직접 설계한다면 순서 전송이 단순한 세션 방식이, 대역폭을 끌어 쓰려면 병렬이 가능한 멀티파트 구조가 잘 맞습니다.
조각 크기는 어떻게 정하나요?
왕복 비용과 재전송량을 맞바꾸는 값입니다. 안정적인 네트워크면 크게, 잘 끊기는 네트워크면 작게 잡습니다. API 의 최소 크기와 배수 제약이 먼저이고, 그 안에서 실측으로 조정합니다.
정리
- 재개 가능한 업로드는 파일을 조각으로 보내고 서버가 받은 위치를 기억하는 방식입니다.
- 이어 보낼 위치는 추측하지 않고 서버에 묻습니다. 일시적 오류와 세션 만료는 나눠 처리합니다.
- S3 멀티파트는 조각을 병렬로 올리는 대신 미완료 조각을 치워야 합니다.
끊겼을 때 다시 보낼 것이 아니라, 어디까지 받았는지부터 물으면 됩니다.