본문으로 건너뛰기
Background Image
  1. 프로젝트/
  2. WindRPC/

WindRPC 개발기 2 - Proto 초기 설계와 C 서버 코드 자동화

·5 분· loading · loading ·
WindRPC - 이 글은 시리즈의 일부입니다.
부분 : 이 글

Proto 초기 설계와 C 서버 코드 자동화
#

마이크로시스템과 상위 애플리케이션 간의 원격 프로시저 호출(RPC)을 지원하기 위한 경량 프레임워크 WindRPC 개발 이야기의 두 번째 편입니다.


들어가며
#

이전 글에서는 전체적인 프로젝트의 이해를 돕기 위해 디스크립터(user_spec.yml) 명세 형식을 먼저 소개했습니다. 하지만 사실 개발의 극초기 단계에서는 디스크립터보다 표준 Protocol Buffers (.proto) 파일의 메시지 구조를 정의하는 일부터 시작했습니다.

gRPC를 사용할 수 없는 마이크로컨트롤러(MCU) 환경에서 단 하나의 Protobuf 바이너리 통화로 어떻게 다양한 서비스와 함수 프로시저를 구별하고 분기할 것인가에 대한 고민이 가장 먼저 해결되어야 했기 때문입니다.

초기 Proto 메시지 설계
#

초기에 고안했던 핵심 메커니즘은 ClientMessage와 ServerMessage라는 최상위 메시지를 크게 한 쌍으로 나누는 것이었습니다.

요청을 보내는 주체와 응답을 주는 주체에 따라 전송 방향을 먼저 결정하고, 메시지 내부에는 oneof 구문을 중첩으로 사용하여 서비스(Service)와 프로시저(Procedure)를 계층적으로 분류했습니다.

  • 전송 주체 구분: 최상위에 ClientMessage(클라이언트 요청용)와 ServerMessage(서버 응답 및 알림용)를 별도로 정의
  • 계층적 분기: 메시지 내부의 oneof 구문 하위에 동일한 서비스 명칭과 프로시저 명칭을 배치하고, 파라미터(입력 값)와 리턴 값(결과 값)을 속성으로 매핑

이 구조는 다이어그램이나 논리적인 개념상으로는 매우 체계적이고 깔끔해 보였습니다. 하나의 Protobuf 메시지 틀 안에서 모든 서비스와 프로시저를 깔끔하게 계층화할 수 있다고 생각했기 때문입니다.

[ 초기 중첩 oneof 메시지 계층 구조 ]

1. 클라이언트 요청 패킷 (ClientMessage)
ClientMessage (최상위)
  └── [oneof payload] ──► Request
                             ├── bytes request_id
                             └── [oneof service]
                                    ├── common.Request
                                    ├── led.Request ──► [oneof command] ──► display_pixels (PixelData)
                                    └── power.Request

2. 서버 응답 및 알림 패킷 (ServerMessage)
ServerMessage (최상위)
  └── [oneof payload]
        ├── Response (요청에 대한 응답)
        │     ├── bytes request_id
        │     └── [oneof service] ──► common.Response / power.Response
        └── Notification (비동기 이벤트 푸시)
              └── [oneof service] ──► power.Notification
// 당시 정의했던 초기 코어 proto 메시지 예시 (windrpc.proto)
syntax = "proto3";

package hlt.windrpc.core;

import "hlt/windrpc/service/common.proto";
import "hlt/windrpc/service/led.proto";
import "hlt/windrpc/service/power.proto";

message ClientMessage {
  oneof payload { Request request = 1; }
}

message Request {
  bytes request_id = 1;
  oneof service {
    hlt.windrpc.service.common.Request common = 6;
    hlt.windrpc.service.led.Request led = 7;
    hlt.windrpc.service.power.Request power = 8;
  }
}

message ServerMessage {
  oneof payload { 
    Response response = 1; 
    Notification notification = 2; 
  }
}

message Response {
  bytes request_id = 1;
  oneof service {
    hlt.windrpc.service.common.Response common = 6;
    hlt.windrpc.service.power.Response power = 8;
  }
}
// 개별 서비스 정의 예시 (led.proto)
package hlt.windrpc.service.led;

message PixelData {
  repeated fixed32 colors = 1;
}

message Request {
  oneof command {
    PixelData display_pixels = 1;
  }
}

NanoPB의 복잡성과 콜백의 늪
#

하지만 개념적으로 그럴듯 해보였던 설계는 MCU C 언어 환경에서 NanoPB 라이브러리를 통해 C 구조체로 변환되면서 큰 문제에 부딪히게 되었습니다.

C 구조체 중첩과 비대해진 타입 명칭
#

Protobuf 패키지명과 서비스명, 프로시저명이 중첩되다 보니, NanoPB가 생성해 낸 C 구조체 타입과 oneof 구분 태그(Tag) 이름이 극도로 길어졌습니다.

예를 들어 특정 프로시저를 호출하기 위한 태그나 구조체에 접근하려면 다음과 같이 길고 복잡한 C 멤버 변수 경로를 거쳐야 했습니다.

// 초기 구조체 접근 및 태그 설정 예시
hlt_windrpc_core_ClientMessage msg = hlt_windrpc_core_ClientMessage_init_zero;
msg.which_payload = hlt_windrpc_core_ClientMessage_request_tag;

hlt_windrpc_core_Request *req = &msg.payload.request;
req->which_service = hlt_windrpc_core_Request_led_tag;
req->service.led.which_command = hlt_windrpc_service_led_Request_display_pixels_tag;

단순히 메시지의 종류를 지정하는 것만으로도 C 코드가 너무 길어지고 가독성이 급격히 떨어졌습니다.

NanoPB 디코딩 콜백(Callback) 작성의 복잡성
#

더 큰 문제는 수신받은 패킷을 디코딩하는 서버 측 C 코드를 작성할 때 발생했습니다.

NanoPB는 가변 길이 문자열(string)이나 가변 배열(repeated) 필드를 처리하기 위해 pb_callback_t라는 콜백 함수 방식을 사용합니다. 중첩된 oneof 메시지 구조를 디코딩하기 위해서는 각 단계마다 수많은 콜백 함수를 직접 구현해야 했습니다.

  • 필드 단위 디코딩 콜백 함수 작성 (decode_string_callback, decode_request_id 등)
  • 각 oneof 태그 분기문마다 수동으로 케이스 처리 및 바이트 스트림 읽기
  • 메모리 오버플로우 방지를 위한 버퍼 및 컨텍스트 관리

이 모든 과정을 C 서버 코드에서 사람이 손으로 일일이 작성하다 보니 코드의 양이 폭발적으로 증가했습니다. C 언어 특성상 디코딩 실수가 잦았고, 중첩 구조체 조작 중 오탈자나 태그 매칭 오류로 인한 빌드 에러 및 런타임 수신 오류가 비일비재하게 발생했습니다.

C 서버 코드 자동화의 결단
#

원래는 디스크립터 파싱 오류나 복잡한 검증 규칙보다는 일단 .proto 파일이 잘 만들어지고, 이를 통해 C 코드와 C# 클라이언트 간 패킷이 주고받아지는지 검증하는 것이 목적이었습니다.

그러나 서버 측 패킷 해석 및 분기 로직을 C 언어로 수동 작성하는 과정에서의 고통을 겪고 나니, 사람이 이 디코딩 코드와 핸들러 분기 로직을 직접 만드는 것은 개발 생산성과 안정성 측면에서 도저히 불가능하다는 결론에 도달했습니다.

결국 스펙 디스크립터(user_spec.yml)에서 표준 .proto 파일을 생성하는 것에 그치지 않고, MCU에서 동작할 C 서버 프레임워크 엔진 코드와 핸들러 콜백 스텁(Stub)까지 통째로 자동 생성하기로 결단했습니다.

개발자는 NanoPB의 내부 콜백이나 태그 분기 메커니즘을 전혀 알 필요 없이, 자동 생성된 C 함수 핸들러 내부에서 비즈니스 로직(예: LED 제어, 센서 값 반환)만 채워 넣으면 되는 구조를 지향하게 되었습니다.


정리하며
#

처음에는 표준 .proto의 중첩 oneof 구조로 계층화를 시도했지만, 임베디드 C/NanoPB 환경에서는 복잡한 구조체와 디코딩 콜백의 늪에 빠지는 시행착오를 겪었습니다. 이후에 서버 코드를 생성을 고려하게 되었고, 이에 대한 생성기에 대한 작업 내용을 정리 해보겠습니다.

WindRPC - 이 글은 시리즈의 일부입니다.
부분 : 이 글

관련 글

WindRPC 개발기 1 - 프로젝트의 시작
·8 분· loading · loading
프로젝트의 시작 # 마이크로시스템과 상위 애플리케이션 간의 원격 프로시저 호출(RPC)을 지원하기 위한 경량 프레임워크 WindRPC (Micro Interconnect & Network Dispatch)의 개발 이야기입니다.
[빛나리] 주간 작업 일지
·15 분· loading · loading
주차별 진행했던 작업 또는 고찰에 대해서 간략하게 정리 합니다. 처음부터 보시면 의식의 흐름대로 정처없이 흘러가는 작업 내용을 보실 수 있습니다.
[빛나리] 하드웨어 설계 고려사항
·4 분· loading · loading
하드웨어 설계 고려사항 # 앰비언트 라이트(Ambient Light) 시스템 개발을 위한 하드웨어 설계 시 고려해야 할 핵심 요소들을 다뤄보겠습니다. 프로젝트의 기능적 요구사항을 충족시키기 위해 MCU(Micro Controller Unit), LED 스트립, 그리고 전력 관리의 세 가지 주요 하드웨어 구성 요소에 대한 기술적 검토 및 결정 과정을 정리합니다.