3.10 첫 번째 Zenoh 애플리케이션 빌드 및 통신 검증
길고 지루했던 기초 공사가 끝났다. 시스템 요구사항을 검토하고, 컴파일러를 설치하고, 모니터링 대시보드를 세팅하며 Zenoh를 맞이할 모든 인프라를 빈틈없이 구축했다.
이제 이 장엄한 3장의 대미를 장식할 시간이다. 이 챕터에서는 앞서 구축한 환경 위에서 분산 시스템의 가장 기초적인 “Hello World“인 Pub/Sub 통신을 실제로 발생시켜 본다. 동일한 기기 내에서의 루프백(Loopback) 테스트부터 시작하여, 서로 다른 프로그래밍 언어(Rust와 TypeScript)로 작성된 애플리케이션 간의 벽을 허무는 이기종 교차 통신까지 단계적으로 검증한다.
마지막으로, 4장 이후 전개될 본격적인 실무 개발 토대를 닦기 위한 범용 템플릿(Template) 프로젝트를 생성하며, 완벽하게 조율된 오케스트라의 지휘봉을 쥘 준비를 마칠 것이다.
1. CLI 툴을 이용한 Pub/Sub 루프백(Loopback) 테스트 실행
복잡한 코드를 작성하기에 앞서, 우리가 설치한 Zenoh 프로토콜 스택이 운영체제의 네트워크 계층과 정상적으로 맞물려 돌아가는지 확인해야 한다. 가장 빠르고 확실한 검증 방법은 바이너리로 제공되는 CLI(Command Line Interface) 도구들을 이용해 자신의 로컬 호스트(Localhost) 내에서 매시지를 던지고 스스로 받는 루프백(Loopback) 테스트를 수행하는 것이다.
1.1 퍼블리셔(Publisher)와 서브스크라이버(Subscriber)의 이해
Zenoh의 데이터 분배 철학인 Publish-Subscribe (Pub/Sub) 모델에서, 퍼블리셔는 라우팅 키(예: demo/test)와 함께 데이터를 네트워크 공간에 흩뿌리는 역할을 하며, 서브스크라이버는 해당 키에 관심이 있다고 네트워크에 선언(Declaration)한 후 흘러들어오는 데이터 스트림을 낚아채는(Catch) 역할을 한다.
테스트를 위해 터미널 창을 두 개(Terminal A, B) 띄워보자.
1.2 서브스크라이버(Subscriber) 수신 대기
데이터의 흐름을 확인하기 위해서는 먼저 그물을 치고 대기하는 쪽이 필요하다. 터미널 A에서 z_sub 유틸리티를 실행한다.
## 터미널 A: "demo/test" 라는 키 경로로 들어오는 모든 데이터를 수신하겠다고 선언
z_sub "demo/test"
성공적으로 실행되었다면, 콘솔에는 아무런 출력 없이 커서만 깜빡이며 데이터가 유입되기를 조용히 기다릴 것이다. 백그라운드에서는 이 노드가 네트워크(멀티캐스트 망 또는 라우터)를 향해 “나는 demo/test 데이터에 관심이 있다“라는 구독 선언(Declare)을 전파한 상태다.
1.3 퍼블리셔(Publisher) 메시지 발송
이제 터미널 B로 이동하여, 대기하고 있는 서브스크라이버를 향해 의미 있는 문자열 데이터(Payload)를 던져보자.
## 터미널 B: "demo/test" 키 경로로 "Hello, Zenoh World!"라는 페이로드를 출판
z_put "demo/test" "Hello, Zenoh World!"
명령어를 실행하고 엔터를 누르는 즉시, 터미널 B의 프로세스는 종료되지만 터미널 A의 화면에는 다음과 같은 수신 로그가 강렬하게 찍힐 것이다.
>> [demo/test] ("Hello, Zenoh World!")
놀랍도록 단순해 보이지만, 이 한 줄의 출력 이면에는 엄청난 기술적 성취가 담겨 있다. 브로커(Broker)나 중앙 서버의 개입 없이, 두 독립적인 프로세스가 Zenoh의 동적 탐색(Discovery) 프로토콜을 통해 서로를 찾아내고, P2P(Peer-to-Peer) 메쉬망을 형성하여 데이터를 주고받은 것이다.
만약 이 테스트가 성공했다면, 여러분의 장비는 마이크로컨트롤러부터 클라우드까지 아우르는 거대한 Zenoh 글로벌 데이터 공간(Global Data Space)의 정식 노드로서 완벽하게 기능할 준비가 끝난 것이다.
2. 이기종 언어 간 교차 통신 확인 (Rust Publisher - TypeScript Subscriber)
CLI 툴을 벗어나, 드디어 진정한 개발자의 영역으로 진입한다. 현대의 분산 시스템은 단일 언어로 작성되지 않는다. 하드웨어 제어부가 C/C++나 Rust로 작성된 엣지 디바이스라면, 관제 대시보드 프론트엔드는 TypeScript나 JavaScript로 돌아가기 마련이다.
이 절에서는 시스템 엔지니어들이 가장 사랑하는 Rust를 데이터 생산자(Publisher)로, 웹 개발자들의 표준인 TypeScript를 데이터 소비자(Subscriber)로 설정하여, 두 이기종 언어(Heterogeneous Languages) 간의 장벽이 Zenoh 와이어 프로토콜(Wire Protocol) 위에서 어떻게 조용히 허물어지는지 라이브 코딩을 통해 검증한다.
2.1 데이터 소스: Rust Publisher 구성
먼저, 센서 데이터를 1초마다 지속적으로 생성하여 네트워크에 흩뿌리는 Rust 애플리케이션을 작성한다. 앞서 세팅한 Rust 작업 환경에서 카고(Cargo) 프로젝트를 초기화한다.
## Cargo.toml
[dependencies]
zenoh = "0.10.1"
tokio = { version = "1", features = ["full"] }
// src/main.rs (Publisher)
use std::time::Duration;
use tokio::time::sleep;
use zenoh::prelude::r#async::*;
#[tokio::main]
async fn main() {
// 1. Zenoh 세션 열기
let session = zenoh::open(config::peer()).res().await.unwrap();
// 2. 메시지를 발행할 키(Topic) 공간 선언
let publisher = session.declare_publisher("sensor/temperature").res().await.unwrap();
let mut temp = 20.0;
println!("🔥 Rust Publisher 가동: 'sensor/temperature' 채널로 데이터 송출 시작...");
// 3. 1초마다 온도 데이터를 변경하며 무한 서빙
loop {
temp += 0.5;
let payload = format!("{{ 'celsius': {} }}", temp);
publisher.put(payload.clone()).res().await.unwrap();
println!(" -> Sent: {}", payload);
sleep(Duration::from_secs(1)).await;
}
}
이 코드를 cargo run으로 실행하면, Rust 세계에서 만들어진 바이트 조각들이 sensor/temperature라는 이름을 달고 허공(Network)으로 던져지기 시작한다.
2.2 데이터 컨슈머: TypeScript Subscriber 구성
이제, 웹 기반 대시보드에서 이 온도를 수신한다고 가정하고 Node.js 기반의 TypeScript 코드를 작성한다.
// package.json 기본 구성 후 의존성 설치
// npm install @eclipse-zenoh/zenoh-ts typescript ts-node
// app.ts (Subscriber)
import { open, Config } from "@eclipse-zenoh/zenoh-ts";
async function runSubscriber() {
// 1. Zenoh 세션 열기
console.log("📡 TypeScript Subscriber 가동 중...");
const session = await open(new Config());
console.log(" 'sensor/temperature' 채널 구독 대기...");
// 2. 비동기 콜백을 통한 데이터 스트림 수신 대기
const subscriber = await session.declare_subscriber("sensor/temperature", (sample) => {
// Rust에서 보낸 바이트 배열을 버퍼를 통해 문자열로 디코딩
const payloadStr = Buffer.from(sample.payload).toString('utf-8');
console.log(`[수신됨] 시간: ${new Date().toISOString()} | 데이터: ${payloadStr}`);
});
// 프로세스가 종료되지 않고 계속 수신하도록 대기
await new Promise(() => {});
}
runSubscriber().catch(console.error);
이 TypeScript 스크립트를 npx ts-node app.ts로 실행해 보자.
2.3 크로스 플랫폼 통신 결과의 통찰
양쪽 애플리케이션이 동시에 돌아갈 때, Rust 터미널에서 송출한 로그가 정확하게 1초 간격으로 TypeScript 터미널 화면에 나타날 것이다.
여기서 우리가 주목해야 할 기술적 경이로움은 다음과 같다:
- 중개자 부재 (No Broker): MQTT와 다르게 중앙의 서버 주소를 설정하거나
connect()하는 과정이 전혀 없었다. 두 프로세스는 단지 세션을 열었을 뿐인데, 멀티캐스트 탐색을 통해 서로를 찾아내어 직결(P2P) 세션을 형성했다. - 타입의 투명화 (Type Transparency): Rust에서 조립된 UTF-8 바이트 배열은 Zenoh 프레이밍을 거쳐 TypeScript 환경의 메모리로 자연스럽게 흘러 들어와
Buffer객체로 매끄럽게 변환되었다. - 플랫폼 독립성 (Platform Agnosticism): 코드 레벨에서 상대방이 어떤 언어로 작성되었는지, 어떤 운영체제인지 알 필요가 없다.
이 작은 교차 통신 성공은 거대한 이기종 분산 시스템 통합이라는, 이 책이 목표로 하는 최종 아키텍처 혁신의 첫 단추가 완벽하게 꿰어졌음을 의미한다.
3. 다음 챕터를 위한 기본 템플릿 프로젝트 생성
이제 여러분은 Zenoh의 기본 원리를 깨달았고, 실제로 코드를 컴파일하여 통신망을 개통시켰다. 하지만 앞으로 전개될 4장 이후의 험난한 심화 과정(분산 라우팅, 백엔드 스토리지 연동, RPC 설계 등)에서 매번 제로(Zero) 베이스에서 프로젝트를 셋업하는 것은 심각한 시간 낭비이자 생산성 저하를 초래한다.
이 절에서는 향후 모든 실습 챕터에서 공통적으로 복제(Clone)하여 사용할 수 있는, 이른바 **‘스위스 아미 나이프(Swiss Army Knife)’ 수준의 뼈대 프로젝트(Boilerplate)**를 생성하고 구조화한다.
3.1 다중 언어 모노레포(Monorepo) 작업 공간 구조화
IoT 시스템 개발의 특성상 하나의 레포지토리 안에서 로우레벨(C/Rust)과 하이레벨(TS/Python) 코드가 공존하는 모노레포 형태를 구성하는 것이 버전 관리와 테스트에 압도적으로 유리하다.
개발 머신의 루트 디렉터리에 zenoh-handbook-workspace 라는 최상위 디렉터리를 만들고 아래와 같은 표준 구조를 잡아라.
zenoh-handbook-workspace/
├── docs/ # 설계 문서 및 다이어그램
├── infra/ # 도커 컴포즈 및 라우터 설정 파일
│ ├── docker-compose.yml
│ └── zenohd_router.json5
├── edge_node_rust/ # Rust 기반 센서/퍼블리셔 애플리케이션 폴더
│ ├── Cargo.toml
│ └── src/
├── control_dashboard_ts/ # TypeScript 기반 관제/서브스크라이버 폴더
│ ├── package.json
│ ├── tsconfig.json
│ └── src/
└── scripts/ # 트러블슈팅 및 헬스체크 쉘 스크립트 모음
3.2 Docker Compose를 활용한 기초 인프라 선언
Zenoh 라우터(Router)와 3.7~3.8장에서 다루었던 Prometheus, Grafana 등의 모니터링 스택을 한 방에 띄워주는 인프라 구성 코드를 infra/docker-compose.yml 리소스에 영구히 저장해 둔다.
## infra/docker-compose.yml
version: '3.8'
services:
zenoh-router:
image: eclipse/zenoh:latest
container_name: handbook-zenoh-router
# Zenoh 기본 통신 포트 및 관리용 플러그인(REST API 등) 포트를 개방
ports:
- "7447:7447/tcp"
- "7447:7447/udp"
- "8000:8000/tcp"
command: ["--rest-http-port", "8000"]
restart: unless-stopped
이렇게 세팅해 두면, 다음장부터는 터미널에서 docker-compose up -d 명령어 단 한 줄로 분산 네트워크의 글로벌 데이터 척추(Backbone)를 즉각 기립시킬 수 있다.
3.3 모듈형 컴포넌트 설계 철학의 습관화
이 뼈대 프로젝트 안에서 코드를 작성할 때 반드시 지켜야 할 철학이 있다. 통신 로직(Zenoh API 호출부)과 비즈니스 로직(센서 데이터 파싱, UI 로직 등)을 엄격하게 **분리(Decoupling)**하라.
// [나쁜 예] 비즈니스 코드 안에 Zenoh가 강하게 결합된 하드코딩
session.put("robot1/sensor", "25.0").await;
// [좋은 예] 통신 계층의 추상화를 통한 모듈형 접근
let network_adapter = ZenohNetworkStrategy::new(session);
robot_controller.broadcast_telemetry(&network_adapter).await;
이렇게 템플릿 환경과 코딩 철학이 갖춰지면, 우리는 더 이상 zenohd를 띄우고 라이브러리 버전을 맞추는 따위의 지루한 소모전에 체력을 낭비하지 않아도 된다.
축하한다. 이로써 여러분은 이 책의 거대한 도입부이자 환경설정 파트인 3장을 완벽하게 마스터했다. 이제 숨을 깊게 들이쉬고, 분산 시스템의 아키텍처를 근본부터 설계하고 지휘하는 Chapter 4. Zenoh 라우터 구성 및 네트워크 토폴로지의 격전지로 당당히 진군하라.