[WarpInfra] 파일 바이트는 Control을 지나지 않는다
그리드 시스템을 설계할 때 가장 먼저 결정해야 할 것은 "어떤 기능을 넣을까"가 아니라 "무엇이 어디를 지나가지 말아야 하나"라고 생각한다. 기능 목록은 나중에 얼마든지 늘릴 수 있지만, 데이터가 흐르는 경로는 나중에 바꾸기 어렵다. 경로는 모든 기능이 얹히는 뼈대이고, 뼈대를 뒤집으면 얹힌 것들이 전부 다시 검증돼야 하기 때문이다. WarpInfra에서 그 뼈대의 답은 한 문장이었다. "파일 payload는 Control이 아니라 Node 간 P2P로 이동한다." 이 편은 이 한 줄이 왜 그렇게 앞세워졌는지, 이 원칙이 깨졌을 때 무엇이 무너지는지, 그리고 구현에서 이걸 어떻게 강제했는지를 다룬다.
두 플레인의 책임을 먼저 나눈다
| 플레인 | 책임 | 금지 |
|---|---|---|
| Control(제어면) | 메타데이터, 스케줄, 정책, 인증, 조각 지도 | 파일 바이트 전송 |
| Data(데이터면) | Node↔Node 실제 바이트 이동, 무결성 검증 | 중앙 경유 |
표만 보면 당연해 보인다. 제어면은 상황실이고 데이터면은 도로망이다. 상황실이 화물까지 실어 나르는 교통 체계를 상상해 보면 왜 이상한지 감이 온다. 그런데 "당연한 원칙"일수록 깨졌을 때의 대가를 계산해 봐야 한다. 특히 그리드 구조에서는 이 원칙이 제품의 존재 이유와 직결된다 — 분산 수신으로 서버 대역폭을 대체하겠다는 것이 니치 정의인데, 바이트가 중앙을 경유하는 순간 그 약속 자체가 성립하지 않는다. 원칙은 아키텍처 취향의 문제가 아니라 니치 정의의 논리적 귀결이었다. 제어면이 파일 바이트까지 중계하도록 허용하면 — 즉 coordinator가 피어들 사이에서 바이트를 릴레이하도록 만들면 — 세 가지가 동시에 깨진다.
제어면이 바이트를 중계하면 깨지는 세 가지
첫째, 확장 불가. 모든 바이트가 한 지점을 지난다는 건 대역폭과 처리량의 상한이 coordinator의 하드웨어로 결정된다는 뜻이다. 그리드의 존재 이유는 "여러 대가 나눠 받아서 빠르다"인데, 중앙 경유를 허용하는 순간 그리드는 느린 프록시 서버로 퇴화한다. 피어가 열 대 늘어도 병목은 그대로 한 대다. 수신 노드가 많아질수록 중앙의 부하만 커지고, 그리드를 키울수록 시스템 전체가 느려지는 역설이 생긴다.
둘째, 장애 반경. coordinator가 죽었을 때 무엇이 죽어야 하는가. 올바른 답은 "새 전송의 시작이 막힌다"다. 이미 시작된 전송은 피어 사이에서 계속되고, 진행 중인 조각 분배도 영향이 없다. 반면 중계 구조에서는 coordinator 한 대가 죽는 순간 진행 중인 전송까지 전부 끊긴다. 장애 격리가 아예 설계에 없는 셈이다. 장애 격리는 운영에서 만들어지는 게 아니라 아키텍처에서 결정된다 — 코드를 아무리 고쳐도 중앙 경유 구조 자체에는 장애 격리가 생기지 않는다.
셋째, 감사. 언뜻 중계 구조가 감사에 더 유리해 보인다. 모든 바이트가 지나가니까 로그도 다 남을 것 같다. 그런데 실제로 남는 로그는 바이트 덤프일 뿐, 운영에 필요한 감사는 "누가 어느 조각을 받았고 무결성 검증을 통과했는지"라는 메타 정보다. 이건 오히려 제어면이 메타만 다룰 때 더 깨끗하게 남는다. 중계 구조의 바이트 로그는 감사에 도움이 되지 않으면서 디스크만 먹는다. "로그가 많다"와 "감사가 가능하다"는 다른 것이다.
구현에서 어떻게 강제했나
원칙은 문서에 쓰기 쉽고 코드에서 깨지기 쉽다. 급하면 "일단 coordinator 경유로 받고 나중에 P2P로 바꾸자"가 항상 제안되기 때문이다. 그래서 WarpInfra는 원칙을 구조로 강제했다.
ponswarp-grid의 coordinator는 방 생성·조회·입장, 멤버 관리, 조각 보유(Have) 정보 같은 메타만 다루는 라우트만 노출한다. 파일 바이트가 coordinator를 지나는 경로 자체가 코드에 없다. 실제 바이트는 WebRTC/QUIC 피어 경로가 직접 맡는다. "바이트를 coordinator로 보내지 말자"가 아니라 "보낼 API가 없다"로 구현한 것이다. API가 없으면 급할 때 우회로도 없다. 급할 때의 우회로가 나중에 기본 경로가 되는 사례는 인프라 코드에서 너무 흔하다. 이 라우트 허용/거부 표의 세부는 S3의 coordinator 편에서 다시 본다.
데스크톱 쪽도 같다. QUIC 피어 경로가 바이트를 실어 나르고, coordinator는 소개(피어가 서로를 알게 함)만 하고 물러난다. 전송이 터져도 제어면은 살아 있고, 제어면이 잠시 죽어도 이미 시작된 전송은 이어진다. 이 특성은 운영에서 생각보다 자주 쓰인다. 제어면 점검을 위해 coordinator를 내려도 진행 중인 배포가 멈추지 않는다 — 제어면 유지보수가 전송 중단 사유가 되지 않는 것이다.
구현 순서도 기록해 둘 만하다. 이 원칙은 구현을 대체하지 않는다 — 원칙이 있어도 "그럼 왜"를 코드로 증명하는 건 별도 작업이다. WarpInfra가 택한 증명 방식은 구현의 형태였다. 위의 세 가지 실패는 사고 실험으로만 남겨두면 언젠가 다시 논쟁되지만, coordinator 코드에 바이트 경로가 아예 없는 상태로 배포되면 "중계가 낫다"는 주장은 매번 처음부터 비용을 증명해야 하는 쪽으로 이동한다. 부담의 방향을 바꾸는 것 — 새로 넣으려는 쪽이 이점을 증명하게 만드는 것 — 이 원칙을 코드 구조로 강제한 실용적 이유다.
채택/기각
| 대안 | 채택? | 이유 |
|---|---|---|
| 모든 전송 coordinator 경유 | ❌ | 확장·장애·감사 삼중 실패 |
| payload P2P + control 최소화 | ✅ | 피어 소개만, 바이트는 직접 |
기각한 쪽도 완전히 무가치한 건 아니었다. 중계 구조는 구현이 훨씬 단순하다. 피어 사이의 NAT 너머 연결, 방화벽, 연결 실패 재시도 — 이런 어려운 문제를 coordinator가 대신 받아내면 피어 구현은 쉬워진다. 모든 피어는 coordinator하고만 통신하면 되니까. 그 단순함의 대가가 위의 삼중 실패라는 계산이 나왔기에 기각했다. 단순함은 유지했고, 그 대가로 피어 연결 성립의 복잡성을 구현이 감당하기로 했다. STUN/TURN, 연결 재시도, 연결 상태 이벤트 — 이 비용의 실제 청구서는 S2의 QUIC 전환 편에서 확인할 수 있다.
원칙을 잠깐 걷어내 보는 사고 실험
원칙의 가치는 가끔 반대 방향에서 확인하는 게 빠르다. 일주일만 걷어내 보자 — coordinator가 바이트 릴레이를 허용하도록 바꾸면 무엇이 달라지는가.
구현 편의성부터 좋아진다. NAT 뒤에 숨은 피어들이 서로 연결되지 않아도 coordinator만 통신하면 되니, 연결 성립 코드가 대폭 줄어든다. 데모도 잘 나온다. 어디서든 동작하는 것처럼 보인다. 그리고 첫 번째 문제는 데모가 끝난 뒤에 나온다. 열 대가 동시에 받는 릴리스 배포에서 coordinator의 업링크가 병목이 되고, "그리드니까 빠르다"는 제품의 첫인상이 정확히 반대로 각인된다. 두 번째는 운영이다. coordinator 프로세스가 OOM으로 죽으면 그날 배포 전체가 멈춘다. 세 번째는 보안이다. coordinator가 모든 바이트를 본다는 건, coordinator 침해가 곧 전사 파일 유출이라는 뜻이다. 메타만 다루는 coordinator의 침해 반경은 "가짜 조각 지도" 수준으로 제한할 수 있는데, 릴레이 구조에서는 그 선이 사라진다.
이 사고 실험의 결론이 원칙의 문장이다. 그리고 원칙이 걸러낸 제안들은 사라지지 않고 기각 기록으로 남는다 — 나중에 정책 기능(Control MVP)을 설계할 때 "어떤 것들은 중앙이 해도 되는가"의 판단 기준이 되기 때문이다.
이 원칙이 미리 정해 준 것들
플레인 분리 하나가 뒤따르는 결정들을 열거하면 다음과 같다.
coordinator의 라우트 표면은 메타로 제한된다(S3 coordinator 편). 조각 지도(Have 정보)는 메타이므로 coordinator가 다루지만, 조각 자체는 다루지 않는다. 스케줄링은 "누가 누구에게 무엇을 요청할지"의 판단이므로 제어면 성격이지만, 실행은 데이터면에서 일어난다. 무결성 검증은 데이터면의 책임이다 — coordinator가 해시를 검증하면 바이트를 본 것이 되니까, 대신 "조각 해시 목록"이라는 메타를 배포하고 각 피어가 스스로 검증한다. 전송 완료 판정도 중앙이 아니라 수신 노드의 SHA-256 비교로 내린다.
눈치챘겠지만 RC1 원칙 2("최종 성공은 UI 표시가 아니라 SHA-256 무결성으로 판정한다")와 원칙 1은 같은 뿌리에서 나온 쌍이다. 중앙이 바이트를 안 보면, 최종 판정도 중앙이 아니라 각 피어가 해야 한다. 원칙 하나가 이렇게 많은 것을 미리 결정해 주는 이유는, 원칙이 개별 기능보다 추상적이어서 모든 기능 위에 한 번에 적용되기 때문이다.
원칙의 대가와 남은 구멍
이 원칙에도 대가가 있다. 제어면이 바이트를 안 만지니까, Control 쪽에서 할 수 있는 일이 줄어든다. 전송 강제 중단, 대역폭 정책 적용 같은 운영 기능을 넣으려면 메타 채널로 간접적으로 해야 한다. 중계 구조였다면 "coordinator가 전송을 끊는다"로 끝날 일이, P2P 구조에서는 "피어에게 중단 명령을 전달하고 피어가 스스로 끊는" 프로토콜이 필요하다. 간접 제어는 직접 제어보다 설계가 어렵다.
그리고 그 정책 기능을 제대로 하려면 카탈로그·토큰·정책을 갖춘 온프레 Control MVP가 필요한데, 이건 아직 구현 전이다. 지금은 "control 최소" 상태로 Phase A를 통과했다. 이 구멍은 시리즈 마지막 편의 백로그에 P1으로 그대로 들어가 있다. 원칙 하나가 이렇게 많은 것을 결정한다. 다음 편은 이 원칙과 짝을 이루는 시간 축의 결정 — 무엇을 먼저 만들고 무엇을 일부러 뒤로 미뤘는지 — 를 다룬다.