9.6 실전 구현 시나리오: C와 Go 혼합 환경
지금까지 익힌 C의 생존술과 Go의 스케일링 전술을 하나의 실전 프로젝트로 결합한다.
시나리오는 명확하다. 수십 년간 가동된 낡은 공장의 산업용 제어기(C) 와, 최신 쿠버네티스 기법으로 구축된 클라우드 관제 대시보드(Go) 를 이어붙이는 작업이다.
중간에 라우터를 끼워 넣기만 하면, 두 언어는 서로의 존재조차 알 필요가 없다. C 프로그램은 1초에 만 번 센서값을 내뱉고, Go 백엔드는 이를 우아하게 흡수하여 데이터베이스에 밀어 넣는다.
이 장은 바로 그 거대한 시스템의 전체적인 설계도이자, 두 이기종 간의 용접(Bridging)을 책임지는 런북이다.
1. 레거시 C 시스템(산업용 제어기)에 Zenoh 통신 모듈 이식하기
공장의 PLC 모터 제어기 안에 20년 전 쓰인 싱글 스레드 무한 루프 펌웨어가 돌고 있다 치자.
이 녀석은 while(1) 안에서 GPIO 센서를 읽어 제어만 할 뿐, 바깥으로 데이터를 쏠 방법을 모른다. 여기에 zenoh-c 를 주사기처럼 꽂아 넣을 것이다.
1.0.1 [Runbook] 비침투적(Non-intrusive) 텔레메트리 이식 전술
핵심은 기존 메인 루프에 절대 블로킹(Blocking) 영향을 미쳐서는 안 된다는 것이다.
1. 초기화 구역에 세션 몰래 삽입
z_owned_session_t z_sess;
z_owned_publisher_t z_pub;
void init_legacy_system() {
// ... 원래 있던 센서 초기화 로직 ...
// Zenoh 통신망 백그라운드 기동
z_owned_config_t config = z_config_default();
zc_config_insert_json(z_loan(config), "mode", "\"client\"");
z_sess = z_open(z_move(config));
z_owned_keyexpr_t key = z_keyexpr_new("factory/cnc/01/sensor");
z_pub = z_declare_publisher(z_loan(z_sess), z_loan(key), NULL);
z_drop(z_move(key));
}
2. 메인 루프 중간에 발사 코드 슬쩍(Fire & Forget) 끼워 넣기
void legacy_main_loop() {
int sensor_val;
while(1) {
sensor_val = read_gpio_hardware();
// ... 원래 있던 복잡한 제어 로직 ...
// [Zenoh 이식 부위]
// z_publisher_put 은 1ms 도 안걸리고 엔진 큐(Queue)로 던진뒤 바로 리턴하므로
// 원래의 루프 속도를 저하시키지 않는다!!
struct { int v; } payload = { sensor_val };
// Endianness 패딩 주의 (9.4장 참조)
z_publisher_put(z_loan(z_pub), (const uint8_t*)&payload, sizeof(payload), NULL);
}
}
제어기의 CPU 가 90% 차는 극한 상황에서도, 이 몇 줄의 코드는 공장 밖으로 무지막지한 트래픽 빔을 은밀하게 발사(Publish)한다.
2. Go 언어를 이용한 마이크로서비스 기반 Zenoh 데이터 게이트웨이 구현
공장(Edge)에서 1만 대의 센서가 C 언어로 쏜 데이터가 구름(Cloud)으로 밀려 들어온다.
중간에 위치한 Zenoh Router 가 이 트래픽을 중계해 줄 것이고, 우리의 Go 백엔드(Gateway)는 거대한 우산을 펼쳐 이 소나기를 모조리 받아야 한다.
2.0.1 [Runbook] K8s 통합 Fan-in 게이트웨이 전술
package main
import (
"encoding/binary"
"fmt"
"bytes"
"github.com/eclipse-zenoh/zenoh-go"
)
// C 에서 쏜 4바이트 덩어리와 규격을 맞춘다.
type SensorPayload struct {
V int32
}
func startDataGateway() {
conf := zenoh.DefaultConfig()
// 라우터(Zenoh Router Pod) IP 타겟팅
conf.Insert("connect/endpoints", `["tcp/zenoh-router.default.svc.cluster.local:7447"]`)
session, _ := zenoh.Open(conf)
defer session.Close()
// 공장 내 모든 CNC 센서를 광역 수신한다.
sub, _ := session.DeclareSubscriber("factory/cnc/**/sensor", func(sample zenoh.Sample) {
var raw SensorPayload
buf := bytes.NewReader(sample.Payload())
// C 가 보낸 LittleEndian 바이트를 Go 구조체로 빙의!
if err := binary.Read(buf, binary.LittleEndian, &raw); err != nil {
// 에러가 나면 이 패킷은 Drop 하고 다음으로 넘어간다(Non-blocking)
return
}
keyPos := sample.KeyExpr().String() // 어느 CNC 기계인가?
// 모인 데이터를 Kafka 로 쏘거나, 시계열 DB(InfluxDB) 에 비동기로 INSERT 한다.
go saveToInfluxDB(keyPos, raw.V)
})
defer sub.Undeclare()
fmt.Println(">> 광역 데이터 우산(Gateway) 전개 완료.")
select {}
}
func saveToInfluxDB(tag string, val int32) {
// (비동기 DB 적재 로직)
}
이 게이트웨이는 매우 심플하다. 데이터가 들어오고, 타입 캐스팅을 한 뒤, 고루틴(go)을 열어 뒷단의 DB로 넘겨주는 단순 파이프 역할이다. 이 코드 하나를 컴파일해서 쿠버네티스 파드로 10개를 띄우면(Scale-out), 알아서 Zenoh 라우터가 10대의 게이트웨이에 트래픽을 분산 전송해 준다! 무중단, 무제한 스케일링의 완성이다.
3. 센서/제어 하드웨어 인터페이스(C)와 클라우드 비즈니스 로직(Go)의 분산 연동 아키텍처
마지막, 양방향(Bidirectional) 통신이다. 센서만 모으는 것은 반쪽짜리 아키텍처다. 클라우드의 Go 서버가 공장 안의 C 로봇에게 명령(Command) 을 내릴 수 있어야 진정한 폐쇄 루프(Closed-loop) 백엔드가 탄생한다.
3.0.1 [Runbook] RPC(Remote Procedure Call) 분산 제어 아키텍처
1. 로봇(C)의 대기조 (Queryable)
명령을 기다리는 자는 C로 짠 로봇이다. 백엔드에서 PUT 을 쏘는 대신, 정밀 제어를 위해 GET(질의응답) 방식을 쓴다.
void on_motor_command(const z_query_t* query, void* ctx) {
// Go 백엔드에서 "파라미터" 를 붙여서 날렸다고 가정
z_str_t payload = z_query_value(query).payload; // 예: "SPEED=500"
// (이 곳에서 로봇 하드웨어 모터를 500으로 돌린다!)
move_motor(payload.start, payload.len);
// 명령수행 결과를 Go 백엔드로 리턴해준다.
z_owned_keyexpr_t reply_key = z_keyexpr_clone(z_query_keyexpr(query));
z_query_reply_options_t opts = z_query_reply_options_default();
z_query_reply(query, z_loan(reply_key), (const uint8_t*)"OK", 2, &opts);
z_drop(z_move(reply_key));
}
// 메인 루프 진입 전
z_owned_keyexpr_t cmd_key = z_keyexpr_new("robot/01/cmd");
z_owned_closure_query_t cq;
z_closure(&cq, on_motor_command, NULL, NULL);
z_declare_queryable(z_loan(z_sess), z_loan(cmd_key), z_move(cq), NULL);
2. 통제실(Go)의 질의 격발 (Querier)
클라우드 대시보드에서 엔지니어가 [비상 정지] 버튼을 눌렀을 때 Go 서버가 치는 코드다.
func sendHaltCommand(session *zenoh.Session, robotID string) error {
getOpts := zenoh.GetOptions{}
// 비상 명령에 파라미터(Value)를 붙인다.
// (Zenoh-Go 최신버전 옵션 로직 기준)
getOpts.Value = zenoh.NewValue([]byte("SPEED=0"))
// 1초 타임아웃 쿼리 격발
target := fmt.Sprintf("robot/%s/cmd", robotID)
replies := session.Get(target, zenoh.QueueingDefault(), &getOpts)
// 응답 대기
for reply := range replies {
if reply.IsOk() {
if string(reply.GetOk().Payload()) == "OK" {
fmt.Println("[성공] 로봇이 비상 정지 명령을 확인했습니다.")
return nil
}
}
}
// 파이프가 깨지고 응답이 없다면?
return fmt.Errorf("!! 로봇 명령 수신 실패 (오프라인 상태 의심)")
}
이 C 와 Go 를 잇는 명령 파이프라인(Command Pipeline)은 그 이면에 복잡한 NAT 방화벽이나 포트 포워딩 뚫기 같은 고행이 필요하지 않다. 그저 양 끝단이 Zenoh 라우터에 연결되어 있다는 조건 하나만으로, 클라우드의 Go 코드가 공장의 C 함수 포인터를 원격으로 격발(Targeting)시키는 극강의 마이크로서비스 융합이 달성된 것이다.