
[Blueprint W2] PonsWarp Bulk Plane — 헥사고날로 찢어 전송을 다시 설계한 이유
원본 ponswarp를 헥사고날로 찢었다. hybridBulkTransport 듀얼 플레인, SpeedSender 프레임, contracts 고정, crypto-plane 분리로 속도를 측정 가능하게 만든 리디자인.

# [Blueprint W2] PonsWarp Bulk Plane — 헥사고날로 찢어 전송을 다시 설계한 이유
> Series: `blueprint-ponswarp` (2/5) · Category: `P2P/분산기술` + `개발 회고` · Slug: `blueprint-ponswarp-02-bulk-plane-hexagonal`
> Source: `blueprints/specs/P01_ponswarp_family.md` §02 + `blueprints/02-ponswarp-bulk-plane-vnext.png` + `ponswarp-bulk-plane-vnext/transfer/`
—
## 0. TL;DR
`ponswarp` 원본이 `SenderView → swarmManager → WASM` 일체형이었다면, `bulk-plane-vnext`는 `transfer/`를 헥사고날(Ports & Adapters)로 찢었다. `SpeedSender/SpeedReceiver` 프레임 프로토콜로 WebRTC와 LAN TCP를 듀얼 플레인으로 묶고, `contracts/protocol/v1/messages.json`에 클라 12종·서버 16종을 고정했다. `crypto-plane.worker`로 암호화를 분리하고, `verify` 체인 7단계로 wasm 빌드부터 벤치마크까지 증명한다. 속도를 위해 만든 게 아니라, 속도를 *측정 가능*하게 만들기 위해 리디자인했다.

—
## 1. 문제 정의 — 왜 원본을 찢어야 했나
원본 `ponswarp`는 프로덕션이 먼저였다. 10GB 전송은 되지만, 세 가지를 답할 수 없었다.
1. **어디서 느린가:** `swarmManager`와 `WASM`과 `directFileWriter`가 한 덩어리라, WebRTC가 느린지 WASM이 느린지 LAN이 가능한지 구분 불가.
2. **프로토콜이 흔들린다:** `PARTITION_ACK`에 `offset/runId` 추가, `PAUSE/RESUME` 추가할 때마다 클라/서버 양쪽을 동시에 고쳐야 했다. 계약 없이.
3. **암호화가 메인 스레드를 잡아먹는다:** AES-GCM을 메인 워커에서 돌리면 500MB 이상에서 프레임 드랍.
`vnext`의 답은 “전송 코어를 도메인으로 격리하고, 계약을 먼저 고정한다” 다.
—
## 2. 설계도 한눈에 — 헥사고날과 두 개의 플레인
중앙에 큰 `transfer` 코어 박스: `ports.ts → state.ts → events.ts → useCases.ts` (application). 그 바깥을 `adapters/`가 감싼다.
* **위:** `hybridBulkTransport`가 두 화살표를 동시에 쏜다 — ① `WebRTC DataChannel` 플레인 ② `LAN TCP 보조` 플레인 (`lanTcpAssist.ts → Python TCP helper`). 어느 쪽이든 먼저 도착한 조각을 `SpeedReceiver`가 합친다.
* **옆:** `cryptoPlaneClient + crypto-plane.worker` — 암호화 평면 분리. 메인 전송 워커는 바이트 이동만, 암호화는 별도 워커에서.
* **아래:** `contracts/protocol/v1/messages.json` 점선 — 송수신 양단이 같은 파일을 참조. 클라 12종, 서버 16종, `CONTROL PAUSE/RESUME`, `PARTITION_ACK(offset/runId)`, `RESUME_REQUEST`, `DOWNLOAD_COMPLETE`.
* **우측:** `verify` 파이프라인 화살표 — `wasm test → build → provenance → benchmark → type-check → frontend → backend cargo` 7단계.
기존 UI를 깨지 않기 위해 `adapters/senderCompatibilityFacade.ts`, `receiverCompatibilityFacade.ts`가 구 UI와 새 코어 사이를 중개한다.
—
## 3. 아키텍처 해부 — 헥사고날 4층
“`
transfer/
├── ports.ts # 인터페이스: BulkTransport, CryptoPlane, EvidenceSink
├── state.ts # 순수 상태머신: pending → sending → paused → verifying → completed
├── events.ts # 도메인 이벤트: PieceAck, PartitionAck, ResumeRequested
├── useCases.ts # 유스케이스: startBulkSend, handleAck, pause, resume
└── adapters/
├── hybridBulkTransport.ts
├── lanTcpAssist.ts
├── cryptoPlaneClient.ts
└── *CompatibilityFacade.ts
“`
* **Ports:** 전송을 `interface`로 정의. WebRTC든 LAN TCP든 같은 `sendPiece(piece)`를 구현하면 된다. 테스트에서 `FakeTransport`로 교체해 10Mbps 시나리오를 재현.
* **State:** `useCases`가 `state`를 직접 mutate하지 않는다. `events`를 `reduce`해 다음 상태를 만든다. 이른바 Redux식 상태머신이지만, 의존성 없이 순수 함수.
* **Adapters:** `hybridBulkTransport`는 두 transport를 `Promise.race`처럼 다룬다. WebRTC가 막히면 TCP가 메꾸고, TCP가 느리면 WebRTC가 앞선다. `networkAdaptiveController`가 RTT를 보고 비율을 조절한다.
—
## 4. 로직 딥다이브 — 3개만 깊게
### 4.1 SpeedSender/SpeedReceiver 프레임 프로토콜 (speedFrames)
“`
[Header: runId(16) | seq(32) | offset(64) | length(32) | flags(8)] [Payload: chunk] [Trailer: CRC32]
“`
* `runId`로 재전송 세션 구분 — 오래된 seq는 버린다.
* `flags`에 `PAUSE/RESUME` 비트. 컨트롤 메시지도 같은 프레임으로 가서 순서 보장.
* `CRC32`는 무결성 1차 필터. 최종은 Merkle 루트.
### 4.2 LAN TCP Assist 폴백
`lanTcpAssist.ts`는 같은 방(roomCode)에 있는 피어에게 `Python TCP helper`를 띄우라고 제안한다. `mDNS`로 `192.168.x.x:8787`을 찾으면, `WebRTC ICE` 실패 전에 TCP를 먼저 시도. 사무실·강의장처럼 같은 LAN에 있으면 WebRTC 시그널링만 하고 바이트는 TCP로 간다. `benchmarks/v1`에서 LAN은 WebRTC보다 1.8배 빨랐다.
### 4.3 컨트랙트 고정과 Compatibility Facade
`contracts/protocol/v1/messages.json`이 진실. 예:
“`json
{“type”:”PARTITION_ACK”,”offset”:1048576,”runId”:”a3f1″,”crc”:”9e0a”}
“`
클라/서버 양쪽이 `zod`로 검증. 필드 하나 추가하려면 JSON을 먼저 바꾸고, 양쪽 테스트가 깨지는 걸 확인한 뒤 코드를 고친다. `CompatibilityFacade`는 구 UI의 `onProgress(percent)` 호출을 새 코어의 `onPieceAck(seq, offset)`로 변환. UI를 안 고치고 코어만 갈아끼운 이유다.
—
## 5. 왜 이 선택인가 — Rejected
| 선택 | 채택 | 버린 대안 | 버린 이유 |
|—|—|—|—|
| 코어 구조 | 헥사고날 Ports & Adapters | 기존 일체형에 메서드 추가 | 테스트 불가, LAN TCP를 붙일 자리 없음 |
| 전송 | 듀얼 플레인 (WebRTC+TCP) | WebRTC 단일 플레인 | LAN에서 TURN 경유 불필요, TCP가 더 빠름 |
| 계약 | messages.json 고정 | 코드에 타입만 정의 | 클라/서버 드리프트, 조용한 필드 누락 |
| 암호화 | crypto-plane.worker 분리 | 메인 워커에서 AES-GCM | 1GB 이상에서 메인 루프 블로킹 |
—
## 6. 실패와 복구
* **실패 1:** 처음엔 LAN TCP를 `WebRTC DataChannel` 위에서 에뮬레이션하려 했다. 그러나 DataChannel의 `ordered:false`가 TCP의 순서 보장과 충돌. 결국 별도 플레인으로 분리.
* **실패 2:** `verify` 체인 없이 `wasm-pack build`만 돌리다 `provenance` 해시가 달라져 배포 롤백. 이후 `wasm test → build → provenance → benchmark → type-check → cargo` 7단계를 `pnpm verify` 한 방으로 묶음. 하나라도 깨지면 머지 불가.
—
## 7. 운영 증거
* `contracts/protocol/v1/messages.json` — 클라 12종·서버 16종, `CONTROL PAUSE/RESUME`, `PARTITION_ACK offset/runId`, `RESUME_REQUEST`, `DOWNLOAD_COMPLETE`
* `transfer/` 4파일 + `adapters/` 4파일 — 의존성 방향: `adapters → ports`, 역방향 금지
* `benchmarks/v1` — LAN TCP 1.8배, `Playwright` 측정, `wasm-provenance` 해시 고정
—
## 8. 다음 편 예고
**W3: `ponswarp-lan-10mbps` — 10Mbps에서 증명한 것.** `networkAdaptiveController` 피드백 루프와 `lanEvidenceAdapter`가 10Mbps 제약을 어떻게 측정 가능한 증거로 바꿨는지, `scripts/lan-evidence.mjs`가 무엇을 재는지 다룬다.
*부록: `blueprints/02-ponswarp-bulk-plane-vnext.png` 프롬프트 `prompts.part1.json#02`, 코드 진입점 `ponswarp-bulk-plane-vnext/transfer/useCases.ts`, `hybridBulkTransport.ts`*

댓글