개발 회고

[PonsWarp] - zero-copy와 메모리 경계 문제

AC 2026. 6. 24. 00:24

PonsWarp의 성능 회고에서 zero-copy라는 말은 꽤 매력적으로 보입니다.

대용량 파일을 브라우저에서 직접 보내고, worker와 WASM을 붙이고, WebRTC DataChannel로 밀어 넣는 제품이라면 복사를 없애는 것이 정답처럼 들립니다. 하지만 실제 기록을 따라가면 결론은 더 조심스럽습니다. PonsWarp에서 zero-copy는 “복사가 전혀 없다”가 아니라 “복사가 가장 위험하고 자주 일어나는 경계를 줄이고 통제한다”에 가까웠습니다.

브라우저 파일 전송에서는 파일 조각을 읽는 순간, worker로 넘기는 순간, WASM memory에서 packet view를 얻는 순간, WebRTC가 ArrayBuffer를 소유하는 순간, receiver가 payload를 검증하고 저장하는 순간마다 경계가 생깁니다. 그 경계는 성능 문제이면서 동시에 신뢰 문제였습니다.

PonsWarp copy boundaries map
zero-copy는 전체 경로의 복사 삭제가 아니라 반복 할당과 복사 경계를 통제하는 문제였습니다.

🧩 첫 번째 오해: 청크 풀링은 zero-copy 완성형이 아니었다

💡 이 글은 zero-copy를 마케팅 단어로 쓰지 않습니다. 확인 가능한 PonsWarp 기록 안에서, 무엇이 실제로 줄어들었고 무엇이 여전히 남았는지 정리합니다.

post-15에서 다룬 949d921은 Phase 2 파이프라인 병렬화, 이중 버퍼링, 청크 풀링을 붙인 커밋입니다. file.slice(...).arrayBuffer()로 파일 조각을 얻고, 그 위에 헤더를 붙인 새 패킷 배열을 계속 만들면 객체 생성과 GC 압력이 반복됩니다. 그래서 ChunkPool은 제한된 Uint8Array 풀을 재사용해 “메모리 재사용으로 GC 압박 감소”를 노렸습니다.

하지만 이 단계는 완전한 zero-copy가 아니었습니다. 949d921에서도 실제 반환 직전에는 18 + dataSize 크기의 새 ArrayBuffer를 만들고 필요한 범위만 복사합니다. WebRTC로 넘길 정확한 크기의 transferable buffer가 필요했기 때문입니다.

복사를 없앴다고 믿으면 남은 경계를 보지 못합니다. 복사 지점을 줄였다고 보면 아직 남은 복사는 어디이며, 그 복사는 왜 남아야 했는지 묻게 됩니다.

🦀 WASM ZeroCopyPacketPool이 한 일

현재 sender worker는 pons-core-wasmZeroCopyPacketPool을 사용합니다. WASM 초기화 시 new ZeroCopyPacketPool()을 만들고, WASM memory 참조를 잡은 뒤 acquire_slot(), commit_slot(), get_packet_view(), release_slot() 흐름으로 packet을 만듭니다.

pons-core-wasm/src/zero_copy_pool.rs의 설명도 같은 방향입니다. WASM 선형 메모리 안에 사전 할당된 버퍼를 두고 JS와 WASM 경계에서의 메모리 복사를 최소화합니다. 슬롯은 기본 64개이고, 각 슬롯은 64KB 데이터와 header, 인증 tag 여유 공간을 포함합니다.

WASM zero copy packet pool slots
WASM pool은 packet 생성과 암호화의 반복 메모리 사용을 슬롯 수명으로 통제합니다.

다만 이 이름도 과장해서 읽으면 안 됩니다. 현재 sender worker에는 “WebRTC는 ArrayBuffer를 전송 후 detach하므로 복사 필요”라는 현실적인 주석이 남아 있습니다. WASM memory의 packet view를 그대로 DataChannel에 넘기는 것이 아니라, 전송 경계에서 새 ArrayBuffer로 복사합니다.

⚠️ 그래서 zero-copy가 의미하지 않았던 것

PonsWarp 맥락에서 zero-copy는 파일에서 네트워크까지 한 번도 복사하지 않는다는 뜻이 아니었습니다. 브라우저의 File API, worker 메시지, WASM linear memory, WebRTC DataChannel은 서로 다른 소유권 규칙을 가집니다.

receiver 쪽 검증과 저장 경계를 건너뛴다는 뜻도 아니었습니다. db175bf, 8aff234, 4650d02 맥락에는 Uint8Array view의 byteOffset, packet size 검증, header parsing 오류 차단, 파일 깨짐 문제가 같이 남아 있습니다.

backpressure가 필요 없어졌다는 뜻도 아니었습니다. sender가 메모리 재사용을 잘해도 receiver의 DirectFileWriter에는 write buffer watermark와 pending bytes가 남습니다. 현재 constants.ts도 DataChannel의 bufferedAmount가 sender 로컬 큐일 뿐 receiver 저장 완료를 의미하지 않는다고 못 박습니다.

🧱 메모리 경계는 신뢰 경계였다

이 주제에서 가장 위험한 표현은 “복사 비용을 줄였다”만 말하고 끝내는 것입니다. PonsWarp에서는 메모리 경계가 곧 신뢰 경계였습니다.

Uint8Array view의 byteOffset 문제는 단순한 최적화 버그가 아닙니다. 같은 backing buffer를 보더라도 view가 가리키는 시작점과 길이를 잘못 해석하면 header와 payload 경계가 틀어집니다. packet length 검증과 CRC32 fallback은 이 문제를 저장 전에 잡기 위한 장치였습니다.

Memory boundary and trust boundary
메모리 view, packet 길이, 순서, 저장 크기는 모두 사용자 신뢰를 닫는 경계입니다.

검증하려면 길이를 읽고, checksum을 계산하고, 때로는 안전한 buffer로 복사해야 합니다. 하지만 파일 전송 제품에서는 이것이 비용이 아니라 계약입니다. 사용자는 내부 복사 횟수를 보지 못합니다. 사용자가 보는 것은 완료 후 파일이 열리는지, 실패하면 partial file을 성공처럼 남기지 않는지, 다시 받을 수 있는지입니다.

🧭 지금 돌아보는 판단

PonsWarp에서 zero-copy는 “복사 없음”이 아니라 “복사의 위치를 설계한다”는 문제였습니다. JS 객체를 계속 만들던 압력은 줄여야 했고, WASM memory의 pool로 내려보낼 수 있는 책임은 내려보내야 했습니다. 그러나 WebRTC 전송 경계와 receiver 검증 경계, 저장 완료 경계는 없어지지 않았습니다.

복사가 줄어들면 빠르고 부드러워집니다. 하지만 어떤 복사를 남길지, 어떤 buffer를 소유할지, 어떤 view를 믿을지, 어떤 크기를 검증할지를 정하지 못하면 빠른 전송은 깨진 파일을 더 빨리 배달할 뿐입니다.

📚 읽은 코드
PonsWarp commit 949d921 — Phase 2 파이프라인 병렬화, 이중 버퍼링, 청크 풀링
PonsWarp/workers/file-sender.worker.v2.ts at 949d921 — ChunkPool, DoubleBuffer, BUFFER_SIZE, POOL_SIZE, PREFETCH_BATCH
PonsWarp/src/workers/file-sender.worker.ts:159-188 — ZeroCopyPacketPool 초기화와 WASM memory 참조
PonsWarp/src/workers/file-sender.worker.ts:801-865 — zero-copy packet 생성 경로와 WebRTC detach 때문에 남는 복사
pons-core-wasm/src/zero_copy_pool.rs:24-34 — WASM 선형 메모리 기반 pool 사용 흐름
PonsWarp commits db175bf / 8aff234 / 4650d02 — byteOffset, packet size, header parsing, 파일 깨짐 수정 맥락
PonsWarp/src/workers/file-receiver.worker.ts:220-250 — packet length, WASM verification, fallback CRC32 mismatch 처리
PonsWarp/src/services/directFileWriter.ts:47-104, 1297-1490 — write buffer watermark와 finalize guard
PonsWarp/src/utils/constants.ts:18-29 — DataChannel queue를 4MB로 보수화한 현재 판단
LIST