본문 바로가기
네트워크

대용량 파일 멀티파트 업로드, 정말 빠를까?

by 조민서 2026. 4. 17.

대용량 파일 업로드에서 "멀티파트로 쪼개면 빨라진다." 라고 가볍게 생각했다. 7GB 파일을 5가지 방식으로 실측한 결과, 네트워크가 병목인 환경에서는 모든 방식의 전송 속도가 동일했다.

이 글에서 전달하려는 메시지는 세 가지다.

  1. 멀티파트 청킹의 목적은 속도가 아니라 안정성과 서버 리소스 효율이다.
    (멀티파트로 청킹한 데이터를 병렬 업로드, 병렬 조회를 하면 물론 속도는 빨라진다.
    하지만 네트워크 대역폭에 제한을 받는다.)
  2. 병렬 업로드는 단일 TCP가 대역폭을 못 채울 때만 의미 있다.
  3. 파일 업로드 경로에서 가장 느린 구간이 전체 속도를 결정한다. 병목이 아닌 구간을 최적화해도 전체 속도는 변하지 않는다.

대용량 파일 업로드 파이프라인

파일이 클라이언트에서 스토리지까지 도달하는 전체 경로는 다음과 같다.

대용량 파일 업로드 파이프라인

이 파이프라인에서 ①~④가 전송 속도에 직접 영향을 주는 구간이다. ⑤⑥은 전송 완료 후 처리이므로 throughput과는 무관하다.
핵심 원리는 단순하다. ①②③④ 중 가장 느린 구간이 전체 throughput을 결정한다. 업로드 방식(멀티파트, 스트림, offset write 등)을 바꾸는 것은 주로 ③④에 영향을 주지만, ②가 병목이면 ③④를 아무리 최적화해도 전체 속도는 변하지 않는다.

"디스크에서 바로 네트워크로 보내면 ① 구간을 건너뛸 수 있지 않나?"

OS 레벨에서 sendfile() 같은 Zero-Copy를 쓰면 유저 스페이스 메모리 복사 없이 전송할 수 있다. 하지만 이 글에서 다루는 시나리오는 웹 브라우저에서 파일을 업로드하는 상황이다. 브라우저는 File API로 파일을 읽어 힙 메모리에 올린 뒤 HTTP 요청으로 전송하며, Zero-Copy를 직접 제어할 수 없다.

 

이 파이프라인에서 멀티파트란 무엇인가

멀티파트 업로드는 대용량 파일을 작은 청크(Chunk) 단위로 쪼개어 여러 번의 HTTP 요청으로 전송하는 방식이다.

파일 업로드 파이프라인

전체 파이프라인에서 멀티파트가 직접적인 영향을 주는 구간은 ①(클라이언트의 청크 분할)과 ③④(서버 측의 수신 및 스토리지 저장 전략)이다.
 
이 실험에서는 단일 스트림 대조군을 포함하여, ③④ 구간의 처리 전략이 다른 5가지 방식을 벤치마크했다.
 

1. Stream (단일 스트림 - 대조군)

멀티파트를 적용하지 않고 파일 전체를 한 번의 연결로 밀어 넣는 전통적인 방식이다.

  • 흐름: ③ [백엔드 수신] → ④ 단일 스트림으로 최종 파일에 기록
  • 특징: 청크 분할(스트림 청크 분할 X, 클라이언트가 청크함을 의미함)도, 병합 과정도 없어 단순하지만 전송 중 끊기면 처음부터 다시 올려야 한다.
  • 예시: 100GB 데이터를 1시간 동안 99GB 업로드 하다 실패하면 처음부터 다시 업로드.

2. Sequence (임시파일 순차 병합)

수신한 청크를 각각의 임시 파일로 저장한 뒤, 모든 전송이 끝나면 하나의 최종 파일로 합친다.

  • 흐름: ③ [백엔드 수신] → ④ 임시파일(1, 2, 3...) 생성 → 완료 시 최종 파일로 순차 복사(1+2+3+...)
  • 특징: 가장 구현이 쉽지만, 병합 과정에서 7GB를 디스크에서 한 번 더 읽고 써야 하는 디스크 I/O 페널티가 발생한다.
    예시: 클라이언트에서 서버 임시 저장소에 100GB 청크 복사 후, 원본 저장소에 100GB 청크 이어 붙임. 즉 disk write 2회

3. Offset (RandomAccessFile 기반)

디스크에 최종 파일 크기만큼 빈 공간을 미리 할당하고, 각 청크가 자신의 위치(Offset)를 찾아가 직접 덮어쓴다.

  • 흐름: ③ [백엔드 수신] → ④ 지정된 Offset 위치에 청크 직접 기록
  • 특징: 임시 파일 생성이나 병합(Merge) 과정이 생략되어 2번 방식(sequence)의 디스크 I/O 페널티를 완벽히 제거한다.
  • 예시:
    1. 클라이언트: "서버야 나 100GB인 a.txt 파일 10,000개 청크로 나눠서 전송할거야."
    2. 서버: "OK" 100GB a.txt 파일 미리 만들어둠.
    3. 클라이언트: 100GB을 청크로 쪼갠 후, 청크를 하나씩 보냄. "서버야 이 part_1.txt 파일은 a.txt의 0번째 offset에 넣으면 돼 크기는 100MB야.)
    4. 서버: "OK" a.txt 0번째 offset부터 100MB 그대로 복사.
    5. 1~4반복

4. Mmap (Memory-Mapped File 기반)

OS의 커널 수준 기능인 페이지 캐시(Page Cache)를 활용해 메모리에 쓰는 것처럼 디스크에 기록한다.

  • 흐름: ③ [백엔드 수신] → ④ OS 메모리 매핑(Offset) → 백그라운드에서 디스크로 Flush
  • 특징: 3번 방식인 Offset (RandomAccessFile 기반)과 마찬가지로 병합이 없으며, 애플리케이션 버퍼를 거치는 오버헤드를 줄여 시스템 리소스 활용도를 높인다.

5. S3-Multipart (Presigned URL 기반 스토리지 직결)

스토리지(SeaweedFS) 시스템의 멀티파트 기능을 직접 활용한다. 클라이언트가 백엔드를 거치지 않고 스토리지로 데이터를 쏜다.

  • 흐름: ① [클라이언트] → ② [네트워크] → ④ [SeaweedFS] 직접 PUT (③ 백엔드 완전 우회)
  • 특징: 트래픽을 스토리지로 완벽히 오프로딩한다. 백엔드 스레드를 전혀 점유하지 않아 대규모 트래픽에서 가장 강력한 아키텍처다.
  • 예시:
    1. 클라이언트: "서버야 나 100GB인 a.txt 파일 10,000개 청크로 나눠서 전송할거야."
    2. 서버-스토리지: "스토리지야 10,000개 업로드 URL(presigned URL) 줘.
    3. 서버: "OK" "10,000개의 스토리지 업로드 URL 줄게 여기에 업로드해. 대신 60분 후에 이 링크 접속 안된다."
    4. 클라이언트: "OK 각 청크마다 해당 URL로 업로드할게."
    5. 스토리지: "OK 10,000개 청크 다 업로드 하면 내가 10,000개 파일을 1개 파일로 병합할게."

5가지 업로드 전략 비교: 7GB 전송 63초의 수렴 

실험 환경

항목 스펙
네트워크 온프레미스 LAN 1Gbps (~120 MB/s 실측)
클라이언트 로컬 Windows
서버 Linux (별도 머신, LAN), Spring Boot (LAN), SeaweedFS (S3 호환, LAN)
테스트 파일 7GB

클라이언트 디스크 속도

Sequential Read   1,324 MB/s
Sequential Write    567 MB/s

서버 디스크 속도 

Sequential Read    1800 MB/s
Sequential Write    712 MB/s

 
각 구간의 속도를 비교하면:

 

② 네트워크가 제일 심한 병목이다.
①④ 모두 ②보다 빠르므로, ③④의 쓰기 전략을 바꿔도 전체 throughput은 ②에 묶인다.

결과 1: 전송 속도는 모든 방식에서 동일하다

파일 크기: 7GB 파일
커넥션 수: 동시 4연결 (4개의 청크를 동시에 전송 )
청크 크기: 32/64/100MB.

방식 청크(MB) 전송 시간(초) 병합속도(초) 속도 합계 (초) 전송속도(MB/s)
Sequence 32 63.2 8.8 72.4 111.6
Sequence 64 63.3 10.8 74.5 111.4
Sequence 100 63.5 9.9 73.9 111.1
Stream 62.5 0.0 62.7 113.0
Offset 32 63.4 0.0 63.6 111.3
Offset 64 63.4 0.0 63.6 111.2
Mmap 32 63.3 0.0 63.3 111.4
Mmap 64 63.2 0.0 63.2 111.5
S3-Multipart 32 63.2 0.04 63.2 111.6
S3-Multipart 64 63.0 0.02 63.1 111.9
S3-Multipart 100 63.0 0.03 63.0 112.0

전송 시간(초) 열이 핵심이다. 모든 방식이 62.5~63.5초로 수렴한다.
③④의 쓰기 전략이 달라도 ②가 병목이므로 전체 throughput은 변하지 않는다.

전송 시간만 보면 안 된다: 병합 시간

차이가 발생하는 건 전송 후 병합 구간이다.

  • Seuence: 임시파일 → 최종파일 복사에 8~11초 추가. ④에서 7GB를 한 번 더 읽고 쓴다.
    (서버 7GB 디스크 I/O가 8~11초가 걸림 → 디스크 Write 속도는 7GB/10s = 약 700MB/s로 실제 측정한 서버 디스크 속도와 비슷함을 확인)
  • S3-Multipart: 메타데이터만 연결하므로 병합 0.04초.
  • Offset / Mmap: 최종 위치에 직접 기록하므로 병합 0초.
  • Stream: 청킹이 없으므로 병합 0초.

전송 속도(MB/s)만 비교하면 모든 방식이 동일해 보이지만, 합계 시간으로 보면 임시 파일로 저장 후 병합하는 sequence 방식은 10초 이상 느리다.
멀티파트 구현 시 병합 전략을 반드시 고려해야 하는 이유다.


그러면 멀티파트는 왜 쓸까? 안정성 + 서버 리소스 효율

벤치마크가 보여주듯, 네트워크가 속도가 제한된 환경에서는 멀티파트로 전송 속도를 높일 수는 없다.
그럼에도 멀티파트는 안정성과 서버 리소스 효율에서 이점이 있다.

1. 안정성

  • 재전송 비용 절감: 100GB를 단일 스트림으로 보내다 99%에서 끊기면 전체를 다시 보내야 한다. 64MB 청크로 나누면 실패한 청크 하나만 재전송하면 된다.
  • 이어올리기(resume). 서버가 각 청크의 완료 여부를 추적하면, 장애 후 미완료 청크부터 재개할 수 있다. 단일 스트림에서는 불가능하다.
  • 서버 측 요청 크기 제한 우회:
    • Nginx: 기본 client_max_body_size 설정은 1MB. 대용량 파일을 보내려면 이 설정을 수 GB로 늘려야 하는데, 이는 서버를 DOS 공격에 취약하게 만들 수 있다.
    • Spring Boot (Embedded Tomcat): 스프링 부트 역시 기본적으로 업로드 가능한 파일 크기를 제한한다. (spring.servlet.multipart.max-file-size 기본값은 보통 1MB).

2. 서버 리소스 효율

② 네트워크가 병목인 환경에서, 서버는 대부분의 시간을 "클라이언트가 데이터를 보내올 때까지 기다리는 것"에 쓴다.
②가 111 MB/s인데 ③④가 그보다 빠르면, 서버 스레드는 대부분 네트워크 대기(recv) 상태로 블로킹된다.

방식 백엔드 커넥션 점유 (7GB 기준, 120 MB/s 기준) 데이터 경로
Stream ~63초 (전체 전송) 클라이언트 → 백엔드 → 디스크
Sequence / Offset / Mmap ~63초 (총합 동일) 클라이언트 → 백엔드 → 디스크
S3-Multipart (Presigned URL) ~0.1초 클라이언트 → 스토리지 직접

S3-Multipart의 Presigned URL 방식에서 데이터는 클라이언트 → 스토리지(SeaweedFS)로 직접 전송된다. 백엔드는 URL 발급(수 ms)과 청크 파일 병합 처리만 담당한다. 7GB 업로드 동안 백엔드 스레드 점유가 거의 0이다.
 
100명이 동시에 7GB를 업로드하는 상황을 생각하면: Stream 방식은 백엔드 스레드 100개가 각 63초씩 블로킹된다.
S3-Multipart는 100명이 동시에 업로드해도 백엔드 스레드는 presignedURL을 획득하는 순간에만 사용한다.


병렬 업로드는 언제 의미가 있는가 (LAN vs WAN)

네트워크 환경에 따른 병렬 업로드 효용성 비교

LAN 환경 (이번 실험)
네트워크가 1Gbps이고, TCP 연결 하나만으로도 이 통신로를 거의 가득 채울 수 있다. TCP 연결을 2개, 4개, 8개로 늘려봤자 통신로 자체의 용량(1Gbps)은 변하지 않으므로 총 전송량은 같다. 이것이 LAN에서 동시 연결 수를 늘려도 속도가 변하지 않는 이유다.
 
WAN 환경 (인터넷)
같은 네트워크 통신로지만, 물리적 거리와 네트워크 홉이 길게 연결되어 있다. 통신로가 길면(Round Trip Time이 크면) TCP 연결 하나로는 통신로 전체를 채울 수 없다. TCP는 상대방의 응답(ACK)을 기다리면서 전송량을 조절하는데, RTT가 크면 이 대기 시간이 길어져서 한 연결의 throughput이 대역폭보다 훨씬 낮아진다. 이때 TCP 연결을 3~4개로 늘리면 각 연결의 전송량이 합산되어 통신로 전체를 활용할 수 있다.
 
이번 실험에서는 청크 크기(1~64MB)와 동시 연결 수(1~8)를 다양하게 조합했지만, LAN 환경이라 TCP 연결 하나로 이미 대역폭이 포화 상태였다. 어떤 조합을 써도 ~111 MB/s로 동일했다.


방식 선택 가이드

상황 추천 이유
실무 (재시도 + 서버 리소스 절약) S3-Multipart 안정성 + 백엔드 커넥션 점유 ~0
단순 로컬 저장 Stream 가장 단순, 오버헤드 없음
로컬 저장 + 병렬 청크 Offset 병합 없음
디스크 I/O 실험 Mmap OS 페이지 캐시 활용
임시파일 방식 Sequence 비추천 — 병합 시간 걸림

정리

멀티파트 청킹은 속도 최적화 기법이 아니다. 네트워크가 병목인 환경에서는 어떤 방식으로 보내든 전송 속도가 동일하다. 그리고 전송 속도만 보면 안 된다 — 병합 시간까지 포함해야 실제 업로드 완료 시간이다.
 
멀티파트는 안정성(실패 시 청크 단위 재전송, 이어올리기)에 더 의미있고,
파일 저장소를 클라우드로 가져가면 서버 리소스 효율(Presigned URL로 백엔드 커넥션 점유 제거)이점도 취할 수 있다.
 
그리고 이는 모두 ② 네트워크가 병목일 때를 가정한 상황이다.
병목이 ③ 서버 처리나 ④ 스토리지 write로 이동하면, 업로드 방식과 동시성이 속도에 직접 영향을 준다.

파일 업로드 상황에서 네트워크 병목을 SW에서 줄이려면 데이터 페이로드를 줄여야한다. 대표적으로 압축해서 전송하는 방식이 있다.
 
파일 업로드에서 병목이 발생하면 "지금 ①②③④ 중 어디가 병목인가?"를 고민해보자.
 
소스코드: large_data_uploader

'네트워크' 카테고리의 다른 글

신뢰성을 위한 Timeout 처리와 멱등성 설계 전략  (1) 2025.12.02