8.8 구조화된 데이터 통신을 위한 직렬화(Serialization) 프로토콜 연동

8.8 구조화된 데이터 통신을 위한 직렬화(Serialization) 프로토콜 연동

대부분의 웹/JS 개발자는 데이터를 날릴 때 두통 없이 곧바로 JSON.stringify() 를 쓴다. 인간이 읽기 편하고 V8 엔진에 최적화되어 있기 때문이다.

하지만 당신의 시스템이 “초고속 C++ 자율주행 드론“과 통신한다면? “{“battery”: 100, “status”: “ok”}“ 라는 JSON 문자열은 그저 네트워크 대역폭을 낭비하는 거대한 쓰레기 더미다. 1바이트면 충분할 상태 값이 따옴표와 콜론(::), 공백 때문에 무려 30바이트로 뻥튀기된다. 게다가 양 끝단의 시스템이 서로 이 데이터를 String에서 Object로 파싱하느라 CPU 사이클을 흥청망청 소모한다.

진정한 분산 데이터 통신 프레임워크인 Zenoh 위에서 달리고 싶다면, JSON을 버리고 **Protocol Buffers(Protobuf)**와 FlatBuffers 라는 날카로운 이진(Binary) 검을 쥐어야 한다. 이 장에서는 TypeScript 환경에서 가장 효율적으로 C/Rust 로봇들과 메모리 구조체(Binary Struct)를 영거리에서 직결시키는 법을 파헤친다.

1. JSON의 성능 한계 분석 및 대안 탐색

왜 우리는 JSON을 P2P 고속망에서 버려야 하는가?

1.0.1 [아키텍처 인스펙션] JSON이 유발하는 3대 병목

  1. 오버헤드 팽창 (Payload Bloat)
    숫자 7 을 보내기 위해 JSON은 {"val": 7} 이라는 10바이트를 태운다. (1,000% 오버헤드). 로봇과 드론의 LTE/5G 통신망 요금제는 기가바이트 당 돈이 매겨진다.

  2. 파싱 지연 (Parsing Latency)
    V8 엔진의 JSON.parse() 는 아무리 빨라도 문자열 전체를 한 번 쭉 스캔하고, 힙(Heap) 메모리에 새로운 자바스크립트 객체(Object) 껍데기를 만들어야 한다. 초당 만 건의 데이터가 쏟아지면 브라우저는 메모리 파편화(Fragmentation)와 가비지 컬렉터(GC) 폭발로 굳어버린다.

  3. 강제적 스키마 부재 (Schema-less Danger)
    TypeScript 겉면에서 data as RobotStatus 로 아무리 타입을 단언(Casting)해 봤자, C++ 백엔드 개발자가 오타를 내서 {"battey": 50} (r 누락)을 날려버리면 브라우저는 이 에디터/런타임에서 알림 없이 undefined 를 화면에 띄워버린다.

해결책(The Alternatives)
이 재앙을 부수기 위해, 데이터의 뼈대(구조)를 파일(.proto 등)로 미리 양쪽 팀(C++ 팀과 웹 팀)이 합의해 두고, 실제 P2P 통신망에는 “데이터 값” 만 0과 1의 바이너리로 초압축해서 쏘아 보내는 ProtobufFlatBuffers 가 P2P 생태계의 표준으로 자리 잡았다. 웹 환경(TypeScript)은 이 무기들을 ArrayBuffer의 형태로 무리 없이 소화해낼 수 있다.

2. Protocol Buffers (protobufjs)와 Zenoh의 결합 및 타입 매칭

구글이 만든 Protobuf는 “작고 단단하게 압축(Packing)하는 기술“의 정석이다.
C++ 엔지니어가 작성한 .proto 파일을 가져와서 프론트엔드/백엔드 폴더에 넣고 TS 코드로 변환하는 것에서 시작한다.

2.0.1 [Runbook] Protobuf - Zenoh 결속 전술

1. 스키마 정의 (robot.proto)

syntax = "proto3";
package robot;

message Status {
  int32 id = 1;
  float battery = 2;
  bool is_moving = 3;
}

2. CLI에서 TypeScript 클래스로 변환
프로젝트에 protobufjs 를 깔고 터미널을 열어 명령어를 친다.

## proto 파일로부터 순수 자바스크립트/TS 압축 코드를 뱉어낸다.
pnpm pbjs -t static-module -w commonjs -o compiled.js robot.proto
pnpm pbts -o compiled.d.ts compiled.js

3. TypeScript에서 패킷 직렬화 및 발사!

import * as zenoh from "@eclipse-zenoh/zenoh";
import { robot } from "./compiled.js"; // 방금 구워낸 Protobuf 모듈

async function pubProto(session: zenoh.Session) {
    const pub = await session.declarePublisher("robot/1/status");

    // 1. JS 객체를 만들고
    const payload = robot.Status.create({ id: 1, battery: 98.5, is_moving: true });
    
    // 2. 바이너리 버퍼로 압축! (JSON.stringify 대신 동작)
    // 이 buffer 변수는 순수 Uint8Array 덩어리다.
    const buffer = robot.Status.encode(payload).finish(); 

    // 3. 발사! (Zenoh는 바이트 덩어리를 제일 좋아한다)
    await pub.put(buffer);
}

// ---- 대시보드 쪽 (Subscribe) ----
async function subProto(session: zenoh.Session) {
    await session.declareSubscriber("robot/1/status", (sample) => {
        // 도착한 Uint8Array 를 다시 TS 객체로 부풀린다!
        // 속도는 JSON 파싱보다 대략 3배~5배 빠르다.
        const message = robot.Status.decode(sample.payload);
        
        // TS 타입이 완벽하게 보장된다.
        console.log(`로봇 ${message.id} 배터리: ${message.battery}%`); 
    });
}

Protobuf는 양 서버/클라이언트 간의 “타입 약속(Contract)“을 런타임이 아니라 빌드 타임 단계에서 강제함으로써 휴먼 에러를 0%로 만든다.

3. FlatBuffers를 이용한 제로 카피(Zero-copy) 데이터 파싱 기법

Protobuf가 “크기를 작게” 만드는 데 몰빵했다면, 게임 업계 출신의 FlatBuffers는 “압축을 푸는(Parsing) 시간 자체를 없애버리는 것“에 모든 것을 걸었다.

Protobuf조차 decode() 를 할 때 메모리에 새 객체를 복사하고 만든다. 하지만 Flatbuffers는 Zenoh가 던져준 원시 바이너리(Uint8Array) 메모리 덩어리의 주소값에 포인터(Pointer)만 턱 얹어서 “필요한 변수만 쏙 뽑아 읽어버리는(Zero-Copy)” 극악무도한 성능을 자랑한다. 초당 1만 건의 데이터가 쏟아져도 GC 프레임드랍이 없다!

3.0.1 [Runbook] Flatbuffers 무한 스피드 파싱 전술

스키마(.fbs)부터 작성 후 flatc 컴파일러로 TS 파일을 뽑는 원리는 거의 같다. 하지만 사용하는 방법론(접근법)이 완전히 다르다.

import * as zenoh from "@eclipse-zenoh/zenoh";
// flatc 가 만들어준 클래스와 flatbuffers 베이스 모듈
import { flatbuffers } from "flatbuffers";
import { RobotStatus } from "./schema_generated"; 

async function watchFlatbufferStream(session: zenoh.Session) {
    await session.declareSubscriber("robot/*/flat", (sample) => {
        // [핵심 1] 디코딩(Parsing/CopyING)이 전혀 없다!!! 
        // 그냥 메모리 덩어리 둘레에 울타리(ByteBuffer)만 쳤다. Time Complexity = O(1)
        const buf = new flatbuffers.ByteBuffer(sample.payload);
        
        // [핵심 2] 객체를 생성하지 않고, 버퍼 주소값을 씌우는 빈 껍데기만 얹음
        const statusObj = RobotStatus.getRootAsRobotStatus(buf);
        
        // [핵심 3] 배터리가 필요하면?
        // 브라우저는 정확히 배터리가 들어있는 4바이트 번지수(Offset)로 직행해서 그 숫자 1개만 꺼내온다.
        const batteryLvl = statusObj.battery();
        
        // 만약 statusObj 에 카메라 픽셀 이미지 데이터 백만 개가 동봉되어 있어도, 
        // 내가 읽지 않는 이상 메모리엔 1바이트도 구체화(Parsing) 되지 않으므로 CPU 사용량은 0이다.
        if (batteryLvl < 20.0) {
            console.warn(`배터리 부족! 좌표: ${statusObj.x()}, ${statusObj.y()}`);
        }
    });
}

이 “지연 평가(Lazy Evaluation) + 제로 카피(Zero-Copy)” 기법은 3D 시뮬레이터(Three.js)를 웹 브라우저 안에서 구동하거나, WebGL 그래픽 카드 버퍼로 데이터를 직결시킬 때 프론트엔드 엔지니어의 최종 테크 트리가 된다.

4. CDDL 및 TypeScript 인터페이스(Interface)를 통한 스키마 검증

하지만 중소규모 프로젝트에서 당장 Protobuf나 Flatbuffers 컴파일러 툴체인을 붙이기는 번거로울 수 있다. 여전히 보편적인 개발 속도를 위해 JSON 베이스(혹은 CBOR 베이스)를 원하지만, TypeScript 컴파일 단에서라도 “가짜 데이터“를 막아내는 퓨어(Pure)한 방법이 필요할 때 CDDL (Concise Data Definition Language) + TS Run-time 형변환 검증기를 쓴다.

4.0.1 [Runbook] TS 런타임 타입-가드(Type-Guard) 전술

우리가 TypeScript에서 선언한 interface는 컴파일(tsc)이 끝나고 순수 JavaScript가 되는 순간 흔적도 없이 증발한다! 즉, 런타임에 Zenoh에서 날아오는 악의적 데이터 구조를 막아낼 방어막이 풀려버린다는 뜻이다.
이를 막기 위해 Zod 같은 런타임 스키마 검증 라이브러리를 동원하여 강력한 수문장을 만든다.

import * as zenoh from "@eclipse-zenoh/zenoh";
// 런타임 스키마 파괴자 Zod
import { z } from "zod"; 

// 1. CDDL 스펙을 본따 런타임에도 살아 숨쉬는 모델을 짠다.
const RobotStatusSchema = z.object({
    id: z.string(),
    battery: z.number().min(0).max(100),
    is_armed: z.boolean(),
    coords: z.tuple([z.number(), z.number()]).optional() // 위도,경도는 선택적
});

// Zod 의 타입 추론을 통해 TS 용 일반 Interface 하나를 날먹 추출한다.
type RobotStatusType = z.infer<typeof RobotStatusSchema>;

async function secureJsonDashboard(session: zenoh.Session) {
    await session.declareSubscriber("fleet/*/status", (sample) => {
        try {
            // 바이트 -> 문자열 -> JSON 객체 변환 (아직 이건 그냥 any 타입이다)
            const rawJson = JSON.parse(new TextDecoder().decode(sample.payload));
            
            // 2. 방호벽 스캐너 가동! (Zod Parse)
            // 만약 C++ 놈이 문서를 엉터리로 짜서 battery 에 "Full" 이라는 글자(String)를 넣었거나
            // 150 이라는 100 초과 숫자를 보냈다면, 이 구문에서 즉각 무자비한 ZodError가 폭발하여 잡힌다!
            const safeData: RobotStatusType = RobotStatusSchema.parse(rawJson);
            
            // [통과] 이제 절대 안심하고 TS 컴포넌트에 넘길 수 있다.
            updateReact(safeData);
            
        } catch (err) {
            // ZodError 등 검증 실패 패킷 파기 (화면이 터지는 것을 막음)
            if (err instanceof z.ZodError) {
                console.error(`[보안 경보] 스키마(형식)에 맞지 않는 불량 패킷 파기당함!`, err.issues);
            }
        }
    });
}

이 방식은 웹/앱 개발자에게 아주 편안한 생태계를 제공한다. 성능 최적화(Protobuf/FlatBuffers)와, 개발 생산성 보장 + 방탄(Zod)의 밸런스 사이트에서 프로젝트에 맞는 아키텍처를 골라 쥐어야 한다.