멀티 클라이언트 자동화와 Flat 아키텍처 완성#
마이크로시스템과 상위 애플리케이션 간의 원격 프로시저 호출(RPC)을 지원하기 위한 경량 프레임워크 WindRPC 개발 이야기의 네 번째 편입니다.
육아와 업무, 그리고 AI 에이전트의 큰 발전#
작년 겨울부터 육아와 바쁜 회사 업무가 겹치면서, 빛나리 프로젝트와 WindRPC 개발은 오랫동안 손을 대지 못하고 장기 계류 상태로 멈추어 있었습니다.
그렇게 시간이 흐르는 사이 기술 생태계에는 커다란 변화가 일어났습니다. 바로 AI 코딩 에이전트 성능의 비약적인 발전이었습니다.
프로젝트 재개를 결심하고 다시 코드를 잡았을 때 놀라운 경험을 하게 되었습니다. 과거에는 부분적으로 AI의 도움을 받으려고 해도 제대로 해결되지 않던 복잡한 템플릿 처리나 엣지 케이스 디버깅 문제들이, 최신 AI 에이전트와의 협업을 통해서는 너무나 매끄럽고 원활하게 해결되기 시작했습니다.
로직 최적화와 멀티 클라이언트 SDK 자동 생성#
에이전트의 도움을 받으며 기존 WindRPC의 C 서버 로직을 대폭 최적화하고, 통합 단위 테스트 코드를 하나씩 구축해 나가기 시작했습니다.
기존에는 데스크톱 클라이언트를 C# (WinForms) 기반으로만 우선 개발하고 있었습니다. 따라서 Protobuf 라이브러리가 만들어준 C# 클래스 메시지만 가져다가 시리얼 포트로 전송하고 응답을 받는 수준의 단순한 방식만 고려했었습니다.
그러나 AI 에이전트의 발전으로 인해 UI/UX 개발 속도가 비약적으로 올라가면서, 데스크톱 클라이언트를 C#뿐만 아니라 웹 기술 기반의 일렉트론(Electron JS/TS)으로 전환하는 방안을 적극적으로 검토하게 되었습니다.
일렉트론 환경을 도입하려면 JavaScript/TypeScript 기반의 클라이언트 통신 라이브러리가 필수적이었습니다. C 서버 코드뿐만 아니라 C#과 JS/TS 환경의 클라이언트 SDK까지 통째로 자동 생성하는 것이 맞겠다는 결론을 내렸고, 디스크립터(user_spec.yml) 하나로부터 양쪽 언어의 클라이언트 통신 SDK(WindRpcClient.cs, WindRpcClient.js)를 일괄 생성하는 자동화 도구를 확장했습니다.
// 생성된 Electron JS/TS 클라이언트 SDK 사용 예시
import { WindRpcClient, decodePowerManagerPowerStatus } from './windrpc/WindRpcClient.js';
const client = new WindRpcClient();
// 1. 시리얼/바이너리 수신 바인딩
serialPort.on('data', (chunk) => {
client.receiveBytes(chunk, (notification) => {
// 서버 비동기 푸시 알림 처리
if (notification.rpcId === 0x0882) {
const alert = decodePowerManagerPowerStatus(notification.payload);
console.log('서버 푸시 알림 수신:', alert);
}
});
});
// 2. RPC 요청-응답 호출 (비동기 async/await)
async function fetchPowerStatus() {
const responseFrame = await client.sendRequest(
0x0801, // (Service ID 8, RPC ID 1)
new Uint8Array(0),
(frame) => serialPort.write(frame),
3000 // 타임아웃 (ms)
);
return decodePowerManagerPowerStatus(responseFrame.payload);
}
풀 코드 생성과 아키텍처 재고#
서버 코드에 이어 C# 및 일렉트론(JS/TS) 클라이언트 SDK 초안까지 자동 생성하는 데 성공하면서, 통신 전 과정의 풀 코드 생성(Full-Code Generation)이 가능해졌습니다.
전 과정 자동 생성이 가능해진 순간, 초안을 만든 후 최적화를 고민하면서 아키텍처에 대해 근본적인 질문을 던지게 되었습니다.
“어차피 모든 코드를 도구가 자동으로 만들어낸다면, 기존에 사람이 읽기 편하도록 유지했던 중첩(Nested) Protobuf 메시지 구조를 고집할 필요가 과연 있을까?”
초기에는 사람이 .proto 파일이나 메시지 구조를 읽고 이해하기 쉽도록 ClientMessage 하위에 oneof 구문을 겹겹이 쌓은 중첩 구조를 만들었습니다. 하지만 모든 코드가 자동 생성되는 방향으로 정해졌다면, 사람이 읽기 쉬운 메시지 구조를 유지할 필요성이 전혀 없다는 사실을 깨닫게 되었습니다.
궁극적으로 내부 로직이나 API가 어떻게 돌아가던지(블랙박스) 결국 유저 입장에서는 생성된 함수(또는 메서드)에 대한 기대 동작과 역할을 알고, 이를 호출하고 응답을 기다리면 되는 것만 수행하면 되었기 때문입니다.
하이브리드 패킷 포맷 (6바이트 헤더 + Protobuf 페이로드)#
최적화를 위해 메시지 구조를 훨씬 간결하게 단순화하고, 마이크로컨트롤러(MCU) 서버에서도 NanoPB 디코딩 콜백이 발생하지 않도록 정적 메모리 크기를 가정한 방식을 고민했습니다.
그 결과 중첩 메시지를 피하고 ‘하이브리드(Hybrid) 패킷 구조’와 Flat 아키텍처를 고안하게 되었습니다.
[ WindRPC 6바이트 바이너리 헤더 + Protobuf 페이로드 구조 ]
+-------------------+-------------------+-------------------+-----------------------+
| RPC ID (2 Byte) | Seq ID (2 Byte) | Payload Len (2B) | Protobuf Payload |
+-------------------+-------------------+-------------------+-----------------------+
| (Service << 8) | Transaction ID | Raw Payload Bytes | Serialized Data Bytes |
| | RPC ID | | Length | |
+-------------------+-------------------+-------------------+-----------------------+
패킷 전체를 Protobuf 메시지로 감싸는 대신, 패킷 전면부는 6바이트 고정 크기의 일반 바이너리 헤더로 구성하고 뒤따르는 페이로드 영역만 단순한 Protobuf 메시지로 처리하는 하이브리드 형태입니다.
- RPC ID (2 Byte): 서비스 ID와 RPC ID를 조합한 16비트 고유 숫자 식별자 ((service_id « 8) | rpc_id)
- Sequence ID (2 Byte): 요청과 응답을 짝지어 비동기 매칭하기 위한 트랜잭션 시퀀스 번호
- Payload Length (2 Byte): 후속으로 이어지는 Protobuf 바이너리 데이터의 길이
- Protobuf Payload: 개별 프로시저의 파라미터나 리턴 값만 담고 있는 단순한 Flat Protobuf 메시지
Flat 아키텍처의 장점과 단일 표준으로의 정립#
이 하이브리드 Flat 구조는 기존 중첩 방식 대비 커다란 성능적 이점을 제공해 주었습니다.
O(1) 룩업 테이블 기반의 초고속 분기
- 더 이상 프로시저 분류를 위해 메시지를 콜백으로 순회하거나 태그를 일일이 비교할 필요가 없어졌습니다.
- MCU는 수신된 6바이트 헤더의 RPC ID 2바이트만 읽어 C 언어 룩업 테이블(Lookup Table)에서 O(1) 시간 복잡도로 즉시 해당 함수를 디스패치합니다.
NanoPB 디코딩 콜백 배제와 정적 메모리(Zero-Heap) 고정
- 파라미터와 리턴 메시지만 생성하되 내부적으로 정적 메모리 제약(nanopb: max_count / max_length)을 강제했습니다.
- NanoPB에 의해 페이로드 부분만 한 번에 디코딩 및 인코딩하면 되므로, 복잡한 콜백 없이 동적 메모리 할당(malloc/free)이 전혀 없는 정적 구조체를 활용하게 되어 전반적인 연산량이 크게 줄어들었습니다.
초기에는 기존 중첩 방식인 Nested 버전과 새로운 하이브리드 방식인 Flat 버전을 사용자가 선택할 수 있도록 두 가지 생성 모드를 모두 지원했습니다.
그러나 실제 빛나리(Bitnari) 프로젝트에 Flat 버전을 기본으로 채용하여 테스트하고 운영해 본 결과, 훨씬 안정적이고 깔끔한 코드 생성과 운영이 가능함을 확인했습니다.
어느 정도 안정적인 코드 생성과 운영이 가능하다고 판단된 시점에서, 과거의 유산이었던 Nested 방식을 과감하게 코드 베이스에서 완전히 없애버리고 Flat 하이브리드 아키텍처만을 단일 표준으로 정립하여 WindRPC 초기 버전을 정식 완성했습니다.
정리하며#
육아와 업무로 오랜 기간 멈춰 있었지만, 그 사이 비약적으로 발전한 AI 에이전트 덕분에 프로젝트를 재개하고 서버 및 멀티 클라이언트 SDK(C#, JS/TS) 풀 코드 자동화를 완성할 수 있었습니다.
그리고 최적화를 통해 6바이트 고정 바이너리 헤더와 Flat Protobuf 페이로드를 결합한 하이브리드 아키텍처를 수립함으로써 마이크로컨트롤러 환경에서 사용할 수 있는 경량 RPC 프레임워크를 완성했습니다.
실제로는 장시간 개발 및 사용해보면서 미쳐 발견하지 못한 이슈 또는 제약사항에 대한 추가적인 확인이 필요하겠지만, 적어도 빛나리 프로젝트에서는 이 RPC를 통해서 개발과 유지보수를 할 생각입니다. 그리고 이 후 제가 진행하는 모든 마이크로시스템 프로젝트에서는 별도의 프로토콜을 만들지 않고 WindRPC에 의한 자동생성 즉, 휴먼 에러를 최소화하여 안정성과 신뢰성을 높이는 방식으로 진행할 예정입니다.
이상으로 WindRPC 탄생 비화와 작업 변화 과정에 대한 개발기를 마칩니다.
다음 포스팅에서는 완성된 WindRPC를 사용함에 있어 필요한 내용이나 빠진 내용들을 작성해볼까 합니다.
