6.2 Zenoh 세션 및 네트워크 초기화

6.2 Zenoh 세션 및 네트워크 초기화

zenoh-cpp 환경에서 가장 먼저 해야 할 일은 백그라운드 라우터(zenohd)와 연결되는 생명줄, 즉 Session(세션) 을 트는 것이다.
C++에서 네트워크 소켓을 연다는 것은 늘 지옥 같은 락(Lock)과 스레드 해제 미아 선언(Orphaned Thread)의 위험을 동반한다. 그러나 Zenoh의 세션 객체는 철저하게 캡슐화되어 있으며, RAII 패턴을 통해 소코트의 시작과 끝을 완벽하게 관장한다.

이 장에서는 단순히 Config 변수 몇 개를 던져서 세션을 여는 기본기를 넘어, C++ 소멸자(Destructor) 매커니즘을 이용한 우아한 커넥션 종료 전술, 그리고 멀티캐스트 스카우팅(Scouting)을 통해 IP 주소를 모르더라도 공장 내부의 다른 피어 노드를 귀신같이 찾아내는 제로 컨피겨레이션(Zero-Configuration) 스크립트를 파헤친다.
깡통 C++ 프로그램이 거대한 Zenoh 분산 군집의 일원으로 깨어나는 가장 경이로운 첫걸음이다.

1. 세션(Session) 객체의 생명주기 및 RAII 패턴 적용

기존 C 네트워크 프로그래밍에서는 socket() 함수로 디스크립터(fd)를 얻고, 온갖 에러 처리 끝에 프로그램 종료 시 close(fd) 를 반드시 호출해야 했다. 깜빡 잊고 안 닫으면 이른바 “Time Wait 지옥” 즉 좀비 포트가 발생한다.
C++ 언어를 쓰는 가장 강력한 무기 중 하나는 변수가 스코프 블록({})을 벗어날 때 소멸자(Destructor)가 알아서 호출되는 RAII(Resource Acquisition Is Initialization) 설계다.

1.0.1 [Runbook] RAII 기반 세션 록인(Lock-in) 전술

강력하게 추천하는 zenoh-cpp 의 베스트 프랙티스다.

#include "zenoh.hxx"
#include <iostream>

void run_edge_node() {
    // 1. Config 확보 (이 객체 역시 RAII로 관리됨)
    auto config = zenoh::Config::create_default();
    
    // 2. 세션 개방. C++17 의 auto 키워드와 결합되어 완벽한 클래스 인스턴스가 생성.
    // 연결이 성립되는 즉시 네트워크 백그라운드 I/O 스레드들이 솟아난다.
    auto session = zenoh::Session::open(std::move(config));
    
    std::cout << "Zenoh 라우터 접속 중... 세션 ID 획득 완료" << std::endl;
    
    // 비즈니스 로직 구동 (무한 루프 혹은 퍼블리싱 로직)
    // ...
    
    // 3. [핵심] 이 run_edge_node() 함수가 종료되는 순간, 
    // auto session 인스턴스의 소멸자(Destructor)가 자동으로 호출되며
    // 내부의 z_close() C 함수를 실행, 소켓을 우아하게 반환하고 이웃 라우터에게 CLOSE 패킷을 던진다!
}

int main() {
    try {
        run_edge_node();
    } catch(const zenoh::ZException& e) {
        std::cerr << "치명적 장애: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

[아키텍처 인스펙션]
이 RAII 패턴은 예외(Exception)가 던져져서 프로그램이 급격히 터지면서 역추적(Stack Unwinding)을 할 때 진정한 위력을 발휘한다. 예외 상황에서도 session 객체의 소멸자는 100% 호출되도록 보장되므로, 시스템 크래시 후에도 포트가 먹통이 되는 재난을 막아준다.

2. 설정(Config) 객체를 이용한 네트워크 파라미터 세부 튜닝

Zenoh의 기본값(Default)은 텍스트 채팅이나 간단한 제어 신호를 보내기엔 적당하지만, 드론의 4K 카메라를 연동하거나 연결이 1초마다 뚝뚝 끊기는 지하 터널(LTE/5G)로 들어갈 땐 턱없이 부족하다.
이때 Config 객체를 JSON 포맷으로 후벼 파서 커널 파라미터를 교체하는 하드코어 튜닝이 요구된다.

2.0.1 [Runbook] 프로덕션 대용량/불안정 네트워크 튜닝

Config 클래스는 JSON5 방식과 완벽하게 호환되므로, 문자열로 된 키/값 설정 트리를 자유롭게 수정할 수 있다.

#include "zenoh.hxx"
#include <iostream>

int main() {
    auto config = zenoh::Config::create_default();
    
    // 전술 1. 명시적인 클라우드 엔드포인트 바인딩 (자동 탐색 스카우팅 우회)
    // "무조건 이 IP로만 붙어라"
    config.insert_json5("connect/endpoints", R"(["tcp/15.12.34.56:7447"])");

    // 전술 2. 대용량 버퍼 지연(Nagle/NoDelay) 설정 (TCP 레벨 제어)
    // 작은 패킷들의 엄청난 연사(Burst)가 필요한 HFT 트레이딩이나 제어 조이스틱의 경우:
    config.insert_json5("transport/tcp/nodelay", "true");

    // 전술 3. 고속 재연결 백오프(Backoff) 세팅 (무선망 대상)
    // 끊어지면 50ms 마다 미친듯이 두드려라. 라우터가 재부팅되면 가장 먼저 붙어야 하니까.
    config.insert_json5("connect/backoff/initial_delay", "50");
    config.insert_json5("connect/backoff/max_delay", "2000");

    try {
        auto session = zenoh::Session::open(std::move(config));
        std::cout << "[INFO] 커스텀 설정(Config) 기반 연결 성공." << std::endl;
    } catch(const zenoh::ZException& e) {
        std::cerr << "[FATAL] " << e.what() << std::endl;
    }
}

하드코딩된 이 설정들이 꺼림칙하다면, zenoh::Config::from_file("/etc/zenoh/client.json5") 를 써서 외부의 인프라스트럭처 설정 파일로 책임을 넘기는(Externalize) 데브옵스 아키텍처를 도입할 수 있다.

3. 스카우팅(Scouting)을 통한 로컬 및 원격 피어 자동 탐색

로봇 창고에 새로운 AGV(무인 운반차)가 투입되었는데, “라우터 IP를 192.168.0.100 번으로 고정해서 빌드해“라고 지시하는 것은 낡은 제조 철학이다.
IP가 DHCP에 의해 매번 바뀌더라도, 로봇은 깨어나자마자 스스로 반경 1km 이내의 다른 로봇이나 중앙 관제 라우터를 찾아내야 한다. 이를 가능케 하는 것이 멀티캐스트 스카우팅(Multicast Scouting) 이다.

3.0.1 [Runbook] 제로 설정(Zero-Configuration) 망 구축

기본적으로 zenoh::Config::create_default()를 치면 같은 로컬 네트워크 대역(LAN/WLAN) 안에서 핑거프린팅(7447 멀티캐스트)을 쏘며 라우터를 찾아 붙는다.
하지만 이 스카우팅 파편화를 명시적으로 통제해야 할 때가 있다.

스카우트 수색망(Scope) 튜닝 전술

#include "zenoh.hxx"
#include <iostream>

int main() {
    auto config = zenoh::Config::create_default();
    
    // 모드 1: 멀티캐스트 스카우팅 끄기 (보안 구역이나 망 통제가 엄격한 구역)
    config.scouting().multicast().set_enabled(false);
    
    // 모드 2: 멀티캐스트의 전파 도달 거리(TTL) 강제 축소
    // 라우터 1개만 건너뛰고, 무의미하게 망 전체를 폭격하는 캐스트(Broadcast Storm)를 막음
    config.insert_json5("scouting/multicast/ttl", "1");

    // [특수 전술] Zenoh 스카우트 유틸리티 직접 쏘기 (라우터 존재 검증용)
    auto config_check = zenoh::Config::create_default();
    std::cout << "망 내 피어/라우터 수색 개시..." << std::endl;
    
    // 내 코드가 세션을 맺기도 전에 망을 둘러보고, 발견되는 친구들 IP 리스트를 가져온다.
    zenoh::scout(std::move(config_check), [](const%20zenoh::Hello&%20hello) {
        std::cout << "발견된 노드 PID: " << hello.whatami() 
                  << " / IP Endpoint: " << hello.locators()[0] << std::endl;
    });

    return 0;
}

이 코드를 사용하면, 응용 프로그램 단에서 동적으로 검색된 이웃 라우터들의 핑(RTT) 길이를 재보고, 자기와 가장 물리적으로 가까운 라우터 IP를 강제 채택(Pick)하여 세션을 여는 지능적인 부하 분산(Load Balancing) 알고리즘도 C++ 로 구현이 가능하다.

4. 라우터 접속 및 클라이언트 모드(Client, Peer, Router) 설정

나는 이 C++ 프로그램에게 어떤 인격(What Am I)을 부여할 것인가?
Zenoh에서는 철저한 계급이 존재한다. 백본의 심장인 라우터(Router), 그와 대등하게 싸우는 군집의 중간 관리자(Peer), 그리고 라우팅 능력을 거세당한 철저한 이기주의자(Client)다.

4.0.1 [Runbook] 토폴로지 인격체 설계 전술

전술 1. 평민(Client) 임명 - IoT 및 말단 센서 최적화
자신은 남들의 데이터를 중계할 능력(CPU/RAM)이 없으니, 그냥 클라우드 라우터에 데이터만 던지고 배터리 절전 모드로 자버리는 기기 대상 모드.

auto config = zenoh::Config::create_default();

// 라우팅 검색 오버헤드 완벽 삭제
config.set_mode(zenoh::WhatAmI::Client); 
// 무조건 연결해야만 하는 주인(라우터) IP 지정
config.insert_json5("connect/endpoints", R"(["tcp/my.cloud.router:7447"])");

auto session = zenoh::Session::open(std::move(config));

전술 2. 중간 관리자(Peer) 임명 - V2X(차량 간 통신) 및 군집 드론
라우터라는 단일 장애점(SPOF)이 없이, 차(Car) 100대가 고속도로에 깔렸으면 지들끼리 서로 망(Mesh Mesh)을 짜고 중계해 주며 진격해야 한다.

auto config = zenoh::Config::create_default();

// 나도 라우팅을 도울 테니(Peer), 주변 놈들도 나한테 붙어라!
config.set_mode(zenoh::WhatAmI::Peer);

// 내가 남들의 원정지가 되도록 포트를 활짝 열어서 리슨(Listen) 모드로 대기한다!
config.insert_json5("listen/endpoints", R"(["tcp/0.0.0.0:7447"])");

auto session = zenoh::Session::open(std::move(config));
// 이 순간부터 이 C++ 프로세스는 서버이자 클라이언트가 된다.

WhatAmI::Peer 로 설정하는 순간 거대한 권력을 쥐게 되지만, 동시에 C++ 메모리가 수천 개의 라우팅 테이블(Routing Table)을 메모리에 올려야 하느라 살이 찌기(RAM 점유율 증가) 시작한다는 트레이드오프(Trade-off)를 명심하라.

5. 세션 수립 시의 에러 핸들링 및 예외 처리

C 언어 바인딩에서는 Z_OK, Z_ENOMEM, Z_EIO 따위의 번호표 코드를 가지고 switch 문을 수십 개씩 도배했다. 하지만 zenoh-cpp 환경에서는 이 에러들이 모던 C++의 std::exception 파생형 구조로 매끄럽게 번역되어 던져진다(Thrown).

5.0.1 [Runbook] 프로덕션 블록 디펜스 (Exception Handling)

Session::open() 은 가장 많은 네트워크 간섭을 받는 지점이라 예외가 난무한다.

에러 추적망(Try-Catch) 구축

#include "zenoh.hxx"
#include <iostream>

void initialize_system() {
    auto config = zenoh::Config::create_default();
    
    // 일부러 문법 오류를 넣어본다
    config.insert_json5("connect/backoff/initial_delay", "문자열넣으면에러남");

    try {
        // 이 한 줄에서 문제가 터지면 뒤는 얄짤없이 중단되고 catch 블록으로 점프!
        auto session = zenoh::Session::open(std::move(config));
        std::cout << "오픈 성공" << std::endl;
        
    } catch (const zenoh::ZException& ez) {
        // [Zenoh 전용 장애] 컴파일러는 Rust 엔진에서 떨어진 특정 원인을 C++ 에러 객체로 랩핑한다.
        std::cerr << "[네트워크 또는 설정 장애 발생]" << std::endl;
        std::cerr << "상세 원인: " << ez.what() << std::endl;
        
        // C++ 로그 시스템(spdlog 등)에 스택 덤프를 남기고 재시도 루프를 돌릴 수 있다.
        
    } catch (const std::exception& e) {
        // [표준 범용 장애] C++ 시스템 메모리 부족(OOM) 등 일반적인 오류 포착
        std::cerr << "[OS 레벨 크래시]: " << e.what() << std::endl;
        
    } catch (...) {
        // 알 수 없는 패닉 방어벽
        std::cerr << "[UNKNOWN FATAL ERROR]" << std::endl;
        std::exit(EXIT_FAILURE);
    }
}

[아키텍처 인스펙션]
만약 방화벽이 막혀서 라우터에 접속하지 못했을 경우 어떤 에러가 뜰까?
놀랍게도 Zenoh는 기본적으로 비동기 백그라운드 엔진이기 때문에, Session::open() 자체는 예외를 던지지 않고 무조건 열려서(Opened) 밑단에서 계속 백오프(재연결)를 시도한다.
즉, ZException이 터지는 경우는 방화벽 문제가 아니라 1. JSON 문법 에러 2. 라우터 IP를 비정상 자릿수(예: 256.256.256.256)로 넣었을 때, 혹은 3. 공유 메모리 영역(Shared Memory) 권한 부족과 같은 하드웨어적인 이슈에 국한된다. 이 차いを 아는 자만이 진정한 시니어다.