제1부 계산 그래프의 코어
1.3 타입 지원과 직렬화 — rosidl·CDR·type hash
1.3.1 이 장에서 배우는 것 / 선수 지식
publish(msg) 한 줄이 네트워크에 나가는 바이트열이 되기까지 무슨 일이 벌어지는가. 이 장은 그 변환의 기계 장치를 연다. 여기를 이해하면 "왜 이 메시지는 예상보다 3배 크지", "왜 필드 순서만 바꿨는데 통신이 끊기지", "왜 rclpy가 rclcpp보다 이렇게 느리지" 같은 질문이 한꺼번에 풀린다.
배우는 것:
- 코드 생성 파이프라인 —
.msg하나가 만들어 내는 파일들의 정체와 역할. - typesupport의 정체 — "타입 지원"이라는 모호한 이름 뒤에 있는 실제 자료구조(함수 포인터 테이블)와, 왜 종류가 여럿인가.
- CDR 인코딩 — 정렬(alignment)·패딩·엔디언, 그리고 메시지 실제 크기를 직접 계산하는 법.
- type hash(RIHS01) — Jazzy에서 노출된 타입 지문. 무엇을 보장하고 무엇을 보장하지 않는가.
- 직렬화를 우회하는 경로 — intra-process·loaned message가 이 그림에서 어디를 건너뛰는가.
- rclpy의 비용 구조 — Python 메시지가 왜 비용이 큰지 계층으로 설명.
- 직렬화 메시지 직접 다루기 — 제네릭 브리지·기록 도구를 만들 때.
- 타입 적응(
rclcpp::TypeAdapter) — 애플리케이션 자료구조를 ROS 메시지처럼 발행·구독하기, 그리고 그 이득이 성립하는 조건(1.3.12). ~/get_type_description— 노드에게 타입 정의를 런타임에 물어보는 서비스. Jazzy에서 기본으로 켜져 있다(1.3.13).
선수 지식: 1.2(인터페이스 정의), 0.6(빌드 구조). DDS·RTPS는 아직 몰라도 되지만 이 장 끝에서 그 경계에 닿는다(3.1·3.2).
정직: 이 장은
rosidl_*저장소의 생성기 구조와 OMG CDR / DDS-XTypes 명세, 그리고 ROS 2 설계 문서(type hash·type description)에 근거한다. 이 환경에서 빌드·패킷 캡처로 검증하지 않았다(목차 3). 특히 XCDR1/XCDR2 중 어느 인코딩이 Jazzy 기본으로 쓰이는지는 RMW 벤더·설정에 따라 다를 수 있어 해당 절에(확인 필요)로 표시했다. 정확한 값이 필요하면tcpdump·Wireshark(RTPS 디섹터)로 직접 확인하는 것이 정본이다. 예외: 1.3.5(type hash)·1.3.12(타입 적응)·1.3.13(타입 서술 서비스)은 2026-08-16에 Jazzy 컨테이너에서 컴파일·실행 확인했다(목차 3의 3차 검증).
1.3.2 .msg 하나가 만들어 내는 것
빌드 시 rosidl_generate_interfaces가 실행되는 결과는 헤더 하나가 아니다. 여러 생성기가 각각의 산출물을 만든다.
| 산출물 | 무엇 | 언제 쓰이나 |
|---|---|---|
| C 구조체 | motor_state__struct.h 등 | rcl·C 노드·다른 언어 바인딩의 기반 |
| C++ 구조체 | motor_state.hpp | rclcpp 사용자 코드 |
| Python 클래스 + C 확장 | _motor_state.py, _motor_state_s.c | rclpy — Python 객체 ↔ C 구조체 변환기 포함 |
| introspection typesupport | 필드 이름·타입·오프셋 테이블 | 런타임에 타입을 모르고 다뤄야 하는 도구(rosbag2·CLI·rqt) |
| 벤더 typesupport | MotorState 전용 직렬화 함수 | 해당 RMW가 실제 전송할 때 |
핵심 통찰: 하나의 메시지 타입에 대해 직렬화 코드가 여러 벌 존재한다. 벤더별(Fast DDS용·Cyclone용)로 따로 생성되고, 여기에 introspection 기반의 범용 경로가 더해진다. 그래서 RMW_IMPLEMENTATION을 바꿔도 재빌드 없이 동작하는 것이다 — 필요한 typesupport 라이브러리가 런타임에 동적으로 선택되기 때문이다(3.5).
1.3.3 typesupport — 함수 포인터 테이블
"타입 지원"은 추상적으로 들리지만 실체는 단순하다. 타입마다 하나씩 존재하는, 그 타입을 다루는 함수들의 묶음이다.
/* 개념적 형태 — 실제 필드 구성은 생성기·버전에 따라 다름 (확인 필요) */
typedef struct rosidl_message_type_support_t {
const char * typesupport_identifier; /* "rosidl_typesupport_fastrtps_c" 등 */
const void * data; /* 구현별 데이터(직렬화 함수·필드 테이블) */
rosidl_message_typesupport_handle_function func; /* 다른 구현으로 넘겨주는 핸들러 */
rosidl_type_hash_t * type_hash; /* Jazzy: 타입 해시 */
} rosidl_message_type_support_t;
사용자 코드에서 이 핸들을 얻는 지점이 rosidl_typesupport_cpp::get_message_type_support_handle<T>()이며, create_publisher<T>() 안에서 자동으로 호출된다. 그래서 rcl과 rmw는 메시지 타입을 컴파일 시점에 몰라도 된다 — 그들은 void*와 typesupport 핸들만 받는다.
두 갈래의 typesupport:
| 종류 | 방식 | 장점 | 단점 |
|---|---|---|---|
벤더 직결(typesupport_fastrtps_c/cpp) | 타입별로 생성된 전용 직렬화 코드 | 빠르다. 인라인·최적화 가능 | 벤더마다 별도 생성 필요 |
introspection(typesupport_introspection_c/cpp) | 필드 메타데이터 테이블을 런타임에 순회 | 타입을 모르고도 다룰 수 있다 | 느리다(간접 참조·분기) |
introspection이 없으면 rosbag2가 임의의 타입을 기록할 수 없고, ros2 topic echo가 임의의 메시지를 출력할 수 없다. CLI가 느린 이유의 일부가 여기 있다(0.7.2) — Python + introspection 경로의 이중 비용.
함정: 커스텀 메시지를 만들었는데
ros2 topic echo에서 "타입을 찾을 수 없다"가 나오면, 대개 CLI를 실행한 셸에 그 인터페이스 패키지가 소싱되지 않은 것이다. 발행 노드와 CLI는 별개 프로세스이고, 각자 자기 환경에서 typesupport 라이브러리를 dlopen한다(0.5).
1.3.4 CDR — 바이트열의 규칙
DDS의 표준 직렬화 형식은 CDR(Common Data Representation) 이다. 규칙은 세 가지로 요약된다.
- 필드는 선언 순서대로 나열된다.
- 각 기본 타입은 자기 크기에 맞춰 정렬(align) 된다 —
uint32는 4의 배수 위치,float64는 8의 배수 위치. 맞추기 위해 패딩 바이트가 들어간다. - 가변 길이(문자열·시퀀스)는 앞에
uint32길이가 붙는다. 문자열은 널 종료 문자를 포함해 센다.
앞에 인코딩 헤더 4바이트(representation identifier + options)가 붙어 엔디언과 인코딩 버전을 알린다. 그래서 빅엔디언·리틀엔디언 기계가 섞여 있어도 동작한다 — 수신 측이 필요하면 바이트를 뒤집는다.
직접 계산해 보기:
# Sample.msg
uint8 flag
float64 value
uint32 count
string name # "abc" 라고 하자
| 오프셋 | 내용 | 크기 |
|---|---|---|
| 0 | 인코딩 헤더 | 4 |
| 4 | flag (uint8) | 1 |
| 5 | 패딩 (8 정렬 맞춤) | 3 |
| 8 | value (float64) | 8 |
| 16 | count (uint32) | 4 |
| 20 | name 길이 (uint32, "abc\0" = 4) | 4 |
| 24 | name 내용 a b c \0 | 4 |
| 합 | 28 |
여기서 나오는 실무적 결론:
- 필드 순서가 크기를 바꾼다. 위 예에서
float64를 맨 앞에 두면 패딩 3바이트가 사라진다. 작은 메시지를 초당 수천 개 보내는 경로에서는 무시할 수 없다. 큰 타입부터 선언하는 것이 일반적인 최적화다. - 필드 순서는 계약의 일부다. 순서를 바꾸면 와이어 포맷이 바뀐다 — "필드를 재배치만 했다"는 호환 가능한 변경이 아니다(1.2.8).
- 문자열은 생각보다 비용이 크다. 길이 4바이트 + 널 종료 + 정렬 패딩. 짧은 문자열 필드가 여럿이면 오버헤드가 실제 데이터를 넘어선다. 고빈도 경로에서는 문자열 대신 정수 ID·상수를 쓴다.
sensor_msgs/msg/Image가 큰 이유는 명확하다 —data가uint8[]이므로 폭×높이×채널 바이트가 그대로 실린다. 1920×1080 RGB면 약 6.2 MB. 30 Hz면 초당 186 MB — 기가비트 이더넷을 넘는다. 이것이image_transport압축이 존재하는 이유다(7.2).
XCDR1 vs XCDR2: DDS-XTypes는 확장 가능한 타입을 위해 두 버전의 인코딩을 정의한다. XCDR2는 정렬 규칙과 확장 타입 처리를 개선했다. Jazzy에서 실제로 어떤 인코딩이 기본으로 쓰이는지는 RMW 벤더와 타입의 확장성(extensibility) 표시에 따라 다르다(확인 필요). 실무에서 이 차이가 드러나는 지점은 서로 다른 벤더 간 통신과 비ROS DDS 애플리케이션과의 연동이며, 문제가 생기면 벤더 문서의 데이터 표현 설정을 확인해야 한다(3.6·3.7).
1.3.5 type hash — Jazzy의 타입 지문
ROS 1은 메시지 정의의 MD5 체크섬으로 타입 일치를 확인했다. ROS 2는 오랫동안 타입 이름 문자열에 의존했고, 이는 취약했다 — 같은 이름의 다른 정의를 가진 두 노드가 연결되어 쓰레기 데이터를 읽는 사고가 가능했다.
Jazzy에는 type hash가 도입·노출되어 있다. 형태는 다음과 같다.
RIHS01_<64자리 16진수>
RIHS= ROS Interface Hash Standard,01= 버전 1.- 값은 타입 설명(type description)의 정규화된 표현에 대한 SHA-256이다. 필드 이름·타입·순서·중첩 타입이 모두 입력에 들어간다.
어디서 보이나:
ros2 topic info -v /motor_state
# 각 엔드포인트마다 "Topic type hash: RIHS01_..." 줄이 나온다 (실행 확인)
무엇을 보장하는가:
| 보장한다 | 보장하지 않는다 |
|---|---|
| 두 해시가 다르면 정의가 다르다 | 해시가 같아도 의미(semantics)가 같다는 보장은 없다 |
| 필드 추가·삭제·타입 변경·순서 변경을 감지 | 단위(deg vs rad)·좌표 규약 차이는 감지 못 함 |
| bag에 기록해 두면 나중에 정의를 특정 가능 | 자동 변환·마이그레이션을 해 주지는 않음 |
즉 type hash는 "같은 것을 말하고 있는가"가 아니라 "같은 형식인가"만 답한다. 단위 혼동 사고(1.2.11의 12번)는 여전히 사람이 막아야 한다.
정직: 해시 불일치 시 연결이 거부되는지, 경고만 나오는지는 RMW·설정에 따라 다를 수 있다
(확인 필요). 실무 관점에서 안전한 가정은 "해시가 다르면 통신이 안 되거나, 되더라도 믿을 수 없다"이며, 시스템 전체를 같은 인터페이스 버전으로 맞추는 것이 정답이다(1.2.8).
연관 기능 — 타입 설명 배포: 해시만으로는 "그래서 그 정의가 뭐였나"를 알 수 없으므로, 노드가 자신의 타입 설명을 서비스로 제공하는 메커니즘이 함께 설계되었다. Jazzy에서 이 서비스는 이미 활성화돼 있으며 기본으로 켜져 있다 — 실행 확인했다. 자세한 것은 1.3.13.
1.3.6 직렬화를 건너뛰는 경로
지금까지의 그림은 "직렬화해서 네트워크로 보낸다"였다. 그런데 같은 프로세스 안이면 이 전부가 낭비다.
| 경로 | 직렬화 | 복사 | 조건 |
|---|---|---|---|
| intra-process | 없음 | 구독자 1개 + unique_ptr 발행이면 0회 | 같은 프로세스, use_intra_process_comms(true)(2.7) |
| 공유 메모리(loaned) | 있음(또는 최소) | 최소 | 같은 호스트, RMW·타입 조건(2.6·2.7) |
| 네트워크 | 있음 | 있음 | 그 외 전부 |
이것이 컴포지션(1.13)의 성능 논거다. 카메라 드라이버와 영상 처리 노드를 같은 프로세스에 넣고 intra-process를 켜면, 6 MB 크기의 이미지의 직렬화·복사·네트워크 스택 통과가 전부 사라지고 포인터 하나가 오간다. 대가는 격리 상실이다 — 한쪽이 종료되면 둘 다 종료된다.
함정: intra-process가 켜져 있어도 모든 발행이 직렬화를 건너뛰는 것은 아니다. 같은 토픽에 프로세스 밖 구독자가 하나라도 있으면 그쪽 몫은 직렬화된다. 그리고
unique_ptr가 아닌const&로 발행하면 복사가 남는다. 정확한 조건은 2.7에서 표로 정리한다.
1.3.7 rclpy의 비용 — 왜 Python이 비용이 큰가
같은 메시지를 rclpy로 발행하면 rclcpp보다 훨씬 비용이 크다. 계층으로 보면 이유가 분명하다.
rclcpp는 A·B 단계가 없다. C++ 구조체가 곧 C 구조체와 호환되는 레이아웃이므로 바로 직렬화로 간다. rclpy는 매 발행마다 Python 객체 순회 → C 구조체 구축이라는 단계를 추가로 지불한다.
| 상황 | 영향 |
|---|---|
| 작은 메시지, 저빈도(1~10 Hz) | 무시 가능. Python이 편한 만큼 이득 |
| 큰 배열(이미지·포인트클라우드) | 심각. 요소별 변환 비용이 크기에 비례 |
| 고빈도(수백 Hz 이상) | GIL 경합까지 겹쳐 지연·지터 급증(2.8) |
완화책:
- 큰 수치 배열은 numpy로 다루고, 메시지 필드에 넣을 때 변환을 한 번만 한다.
sensor_msgs_py·ros2_numpy계열 유틸이 이 변환을 최적화한다(확인 필요: Jazzy에서의 패키지명·가용성). - 경로 자체를 옮긴다 — 데이터 평면(고빈도·대용량)은 C++, 제어 평면(설정·조정·상태 기계)은 Python. 실무에서 가장 효과가 큰 선택이다.
- 필요하면 직렬화 메시지를 직접 다뤄 Python 객체화를 건너뛴다(다음 절).
1.3.8 직렬화된 메시지 직접 다루기
타입을 컴파일 시점에 모르면서 데이터를 옮겨야 하는 도구 — 브리지·기록기·중계기 — 를 만들 때 쓴다.
#include "rclcpp/serialization.hpp"
#include "rclcpp/serialized_message.hpp"
// 타입을 아는 경우: 수동 직렬화·역직렬화
rclcpp::Serialization<sensor_msgs::msg::Imu> ser;
rclcpp::SerializedMessage serialized;
ser.serialize_message(&imu_msg, &serialized);
ser.deserialize_message(&serialized, &out_msg);
// 타입을 모르는 경우: 제네릭 구독 (문자열로 타입 지정)
auto sub = node->create_generic_subscription(
"/some_topic", "sensor_msgs/msg/Imu", rclcpp::QoS(10),
[](std::shared_ptr<rclcpp::SerializedMessage> msg) {
// 역직렬화하지 않고 그대로 전달·저장 가능
});
auto pub = node->create_generic_publisher(
"/mirror", "sensor_msgs/msg/Imu", rclcpp::QoS(10));
쓰임새:
| 용도 | 왜 제네릭인가 |
|---|---|
| rosbag2 기록·재생 | 임의 타입을 다뤄야 한다(11.2) |
| 도메인 브리지·중계 | 내용을 볼 필요 없이 옮기기만 |
| 토픽 미러·필터 | 역직렬화 비용 절약 |
| 계측·크기 측정 | 바이트 수를 직접 측정한다 |
함정: 제네릭 경로는 타입 이름 문자열을 신뢰한다. 상대의 실제 타입이 다르면 그대로 잘못된 바이트를 옮긴다. type hash(1.3.5)를 함께 확인하는 것이 안전하며, 그것이 type hash가 도입된 이유 중 하나다.
1.3.9 메시지 크기·비용을 실제로 측정하는 법
이론 계산은 출발점이고, 실측이 정본이다.
ros2 topic bw /camera/image_raw # 대역폭 (전송 기준)
ros2 topic hz /camera/image_raw # 주기
bw는 CLI가 실제로 받은 양을 재므로 네트워크·RTPS 헤더 오버헤드를 포함하지 않는다. 와이어 상의 실제 부하를 보려면 인터페이스 수준에서 봐야 한다.
# 인터페이스 총 트래픽 (ROS 외 트래픽 포함)
ip -s link show eth0
# RTPS 패킷만 (Wireshark의 rtps 디섹터가 가장 유용)
sudo tcpdump -i eth0 -nn udp portrange 7400-7500
tcpdump로 캡처한 패킷을 Wireshark로 열면 RTPS 디섹터가 서브메시지·데이터 페이로드를 해석해 준다. 디스커버리 트래픽(3.3)과 데이터 트래픽을 구분해 보는 데 특히 유용하며, "대역폭이 왜 이렇게 높나"의 답이 실은 디스커버리인 경우가 드물지 않다.
계산으로 상한을 설정하는 습관:
$$B_{\text{wire}} \approx f \cdot \left( S_{\text{CDR}} + H_{\text{RTPS}} \right) \cdot N_{\text{sub,network}}$$
여기서 $f$는 발행 주파수, $S_{\text{CDR}}$은 직렬화 크기, $H_{\text{RTPS}}$는 패킷당 헤더 오버헤드(수십 바이트 수준이나 조각화되면 조각마다 붙는다), $N_{\text{sub,network}}$는 프로세스 밖 구독자 수다. 멀티캐스트가 실제로 쓰이면 $N$ 배 증가가 완화되지만, 무선·컨테이너 환경에서는 멀티캐스트가 막혀 유니캐스트로 떨어지는 경우가 흔하다(3.9) — 그때 이 식이 그대로 부하가 된다.
견해: 시스템 설계 초기에 주요 토픽의 $S_{\text{CDR}} \times f$ 표를 한 장 만들어 두는 것이 나중의 대역·CPU 사고를 크게 줄인다. 특히 이미지·포인트클라우드는 이 표에서 즉시 눈에 띈다(7.8).
1.3.12 타입 적응 — 사용자 타입을 ROS 메시지처럼 쓰기
번호에 대하여: 이 절과 다음 절(1.3.13)은 나중에 추가되었다. 기존 번호를 밀지 않는다는 규약(CLD-03)에 따라 번호는 끝에 붙이고, 읽는 흐름을 위해 함정 카탈로그 앞에 놓았다.
지금까지 본 그림에는 조용한 세금이 하나 있다. 애플리케이션의 자료구조와 ROS 메시지 타입이 다르면, 발행할 때마다 변환 복사가 일어난다.
cv::Mat frame = grab(); // 애플리케이션 표현
sensor_msgs::msg::Image msg;
convert(frame, msg); // ← 복사 1회, 매 프레임
pub_->publish(msg);
6 MB 이미지를 30 Hz로 다루면 이 한 줄이 초당 180 MB의 memcpy다. 그런데 이 변환은 논리적으로 노드의 관심사가 아니다 — 노드는 cv::Mat을 다루고 싶고, ROS는 sensor_msgs/msg/Image를 요구할 뿐이다.
타입 적응(type adaptation)은 이 변환을 미들웨어 경계로 밀어 넣는다. 사용자는 자기 타입으로 발행·구독하고, 변환 규칙은 한 번만 선언한다.
1.3.12.1 적응기 선언
rclcpp::TypeAdapter를 특수화한다. 요구 사항은 네 가지다 — 실행 확인(rclcpp/type_adapter.hpp 원문과 컴파일).
#include "rclcpp/type_adapter.hpp"
template<>
struct rclcpp::TypeAdapter<std::string, std_msgs::msg::String>
{
using is_specialized = std::true_type; // ① 특수화했음을 표시
using custom_type = std::string; // ② 내가 쓰는 타입
using ros_message_type = std_msgs::msg::String; // ③ 와이어에 나가는 타입
static void convert_to_ros_message( // ④ 양방향 변환
const custom_type & source, ros_message_type & destination)
{ destination.data = source; }
static void convert_to_custom(
const ros_message_type & source, custom_type & destination)
{ destination = source.data; }
};
선언한 쌍은 두 가지 방법으로 쓴다.
| 방법 | 형태 | 성격 |
|---|---|---|
| 명시적 | using Adapted = rclcpp::adapt_type<std::string>::as<std_msgs::msg::String>; | 쌍을 그때그때 지정. 한 사용자 타입을 여러 ROS 타입에 대응시킬 수 있다 |
| 암묵적 | RCLCPP_USING_CUSTOM_TYPE_AS_ROS_MESSAGE_TYPE(std::string, std_msgs::msg::String); | 이후 create_publisher<std::string>(…)처럼 사용자 타입만 써도 된다 |
using Adapted = rclcpp::adapt_type<std::string>::as<std_msgs::msg::String>;
pub_ = create_publisher<Adapted>("chat", 10);
sub_ = create_subscription<Adapted>("chat", 10,
[this](const std::string & s) { // ← 콜백이 사용자 타입을 받는다
RCLCPP_INFO(get_logger(), "%s", s.c_str());
});
pub_->publish(std::string("adapted-hello")); // ← 사용자 타입을 그대로 발행
실행 확인(2026-08-16): 위 구조를 Jazzy 컨테이너에서 컴파일·실행해 발행한
std::string이 구독 콜백에const std::string &로 도착하는 것을 확인했다.rclcpp::adapt_type<T>::as<M>·RCLCPP_USING_CUSTOM_TYPE_AS_ROS_MESSAGE_TYPE둘 다 헤더에 실재한다.
1.3.12.2 무엇이 실제로 절약되는가 — 그리고 무엇은 아닌가
여기서 오해가 갈린다. 타입 적응은 변환을 없애는 것이 아니라 변환을 옮기는 것이다.
| 경로 | 변환이 일어나나 | 이득 |
|---|---|---|
| 프로세스 밖 구독자 | 일어난다 — 어차피 CDR로 나가야 한다 | 코드에서 변환 코드가 사라진다(가독성). 성능 이득은 없다 |
| intra-process + 사용자 타입 구독자 | 건너뛸 수 있다 | 변환·직렬화·복사가 모두 빠지고 사용자 타입 객체가 그대로 전달된다 |
| intra-process + ROS 타입 구독자 | 일어난다 | 한쪽만 적응 타입일 때는 경계에서 변환된다 |
즉 타입 적응의 성능 논거는 intra-process(2.7)와 짝일 때 성립한다. 컴포지션으로 같은 프로세스에 묶고(1.13), intra-process를 켜고, 양쪽이 같은 적응 타입을 쓸 때 "애플리케이션 표현 → 애플리케이션 표현"이 복사 없이 이어진다. 프로세스가 갈려 있으면 얻는 것은 코드 구조이지 대역·CPU가 아니다.
견해: 먼저 배치를 정하고 나서 타입 적응을 도입하라. 노드를 분리해 둔 채 타입 적응만 넣으면 "복잡도는 늘었는데 프로파일은 그대로"인 결과가 나온다. 2.7.9의 판단 절차를 먼저 통과시키는 것이 순서다.
함정: 변환 함수는 아무 데서나 불린다. 발행 경로·수신 경로 어느 쪽에서 불릴지, 어느 스레드에서 불릴지는 배치와 경로에 달렸다. 변환 함수 안에서 락을 획득하거나 할당을 크게 하면 그 비용이 실시간 경로로 새어 들어간다(12.3). 변환은 순수·저비용으로 유지하라.
함정: 적응 타입은 CLI에서 그대로 보이지 않는다. 와이어에 나가는 것은
ros_message_type이므로ros2 topic echo는 ROS 타입으로 보여 준다. 이것은 정상이며, 오히려 이 성질 덕에 기존 도구·bag·브리지가 그대로 동작한다.
1.3.12.3 어디에 쓰나
| 상황 | 적응 쌍의 예 |
|---|---|
| 영상 처리 | cv::Mat ↔ sensor_msgs/msg/Image |
| 포인트클라우드 | 라이브러리 고유 클라우드 타입 ↔ sensor_msgs/msg/PointCloud2(7.4) |
| GPU·가속기 버퍼 | 디바이스 메모리 핸들 ↔ 대응 메시지 — 자매서 nvidia NV-108(NITROS: 노드 사이에서 GPU 버퍼를 복사 없이 넘긴다)이 이 문제를 정면으로 다룬다 |
| 수치 코드 | Eigen 타입 ↔ geometry_msgs 계열 |
정직: REP 번호는 확인하지 못했다
(확인 필요). 이 기능은 흔히 "type adaptation(REP 2007)"으로 불리나, 이 저장소에서는 REP 원문에 접근할 수 없었다(목차 3·A.3). 위 서술은 Jazzy에 설치된rclcpp헤더와 실행 결과에 근거한다 — 근거가 문서가 아니라 실물이라는 뜻이다.
1.3.13 ~/get_type_description — 정의를 런타임에 물어보기
type hash는 "정의가 다르다"까지만 말한다(1.3.5). 그래서 그 정의가 무엇이었는지를 얻으려면 정의 자체를 배포해야 한다. Jazzy의 모든 노드는 이를 위한 서비스를 기본으로 생성한다.
$ ros2 service list | grep type_desc
/probe/get_type_description
실행 확인(2026-08-16): rclpy 노드를 하나 실행하자 별도 설정 없이 위 서비스가 나타났다. 켜고 끄는 것은 읽기 전용 노드 파라미터다.
$ ros2 param get /probe start_type_description_service
Boolean value is: True # ← Jazzy 기본값
1.3.13.1 호출과 응답
# 1) 해시를 먼저 얻는다
$ ros2 topic info -v /chatter | grep "type hash"
Topic type hash: RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18
# 2) 그 해시로 정의를 요청한다
$ ros2 service call /probe/get_type_description \
type_description_interfaces/srv/GetTypeDescription \
"{type_name: std_msgs/msg/String, type_hash: RIHS01_df66…1a18, include_type_sources: true}"
응답에는 두 가지가 들어온다 — 구조화된 필드 목록과, 요청하면 원본 .msg 텍스트까지.
| 응답 필드 | 내용 |
|---|---|
successful · failure_reason | 성공 여부와 실패 이유 |
type_description | IndividualTypeDescription — 필드 이름·type_id·capacity·중첩 타입 이름, 그리고 참조된 타입들 |
type_sources | include_type_sources: true일 때 원본 정의 텍스트 그대로 |
실제 응답에서 std_msgs/msg/String의 원본 주석("This was originally provided as an example message…")까지 그대로 돌아왔다 — 정의를 소스 없이 복원할 수 있다는 뜻이다.
1.3.13.2 함정 — 해시가 정확해야 한다
$ ros2 service call … "{type_name: std_msgs/msg/String, type_hash: '', …}"
response: successful=False, failure_reason='Failed to parse type hash'
[ERROR] [rcl]: Failed to parse type hash ''
빈 해시나 형식이 틀린 해시는 거절된다(실행 확인). 이 서비스는 "이름으로 아무거나 찾아 주는" 조회 창구가 아니라 "이 정확한 해시의 정의를 다오" 라는 질의다. 이름은 같고 정의가 다른 두 타입이 공존할 수 있으므로 — 애초에 그 상황을 풀려고 만든 기능이므로 — 해시가 키가 되는 것이 당연하다.
1.3.13.3 이것이 열어 주는 것
| 쓰임 | 설명 |
|---|---|
| 소스 없는 도구 | 원격 관측 도구가 커스텀 메시지 패키지를 설치하지 않고도 필드를 해석할 수 있다(11.5) |
| bag의 자체 기술성 | 기록 시점의 정의를 함께 남기면, 정의가 바뀐 뒤에도 해석 가능한 bag이 된다(11.2·11.3) |
| 버전 불일치 진단 | "해시가 다르다"에서 멈추지 않고 무엇이 다른지를 양쪽에서 받아 비교할 수 있다(1.2.8) |
정직: 위 세 가지는 이 서비스가 가능하게 하는 것이지, Jazzy의 도구들이 이미 그렇게 동작한다는 뜻이 아니다
(확인 필요). 확인한 것은 서비스의 존재·기본 활성화·요청/응답 동작까지다.rosbag2가 이 정보를 기록에 넣는지, RViz2·외부 도구가 이를 활용하는지는 확인하지 않았다.
정직:
rcl의 type hash 헤더는 설계 근거로 REP-2011을 가리키고,GetTypeDescription.srv의 주석도 해시를 "REP-2011 RIHS hash string"이라 부른다(원문 확인). 0.3에서 이 서비스를 "REP 2016 구현"이라 적었으나 REP 원문을 대조하지 못했다(확인 필요)— 번호가 필요하면 REP 원문을 확인하기 바란다.
1.3.10 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | 커스텀 타입 패키지 미소싱 | CLI가 타입을 못 찾음 | 해당 워크스페이스 소싱(0.5) |
| 2 | 필드 순서 재배치를 무해하다고 판단 | 통신 두절·해시 변경 | 순서는 계약(1.2.8) |
| 3 | 짧은 문자열 필드 남발 | 오버헤드가 데이터를 초과 | 정수 ID·상수 사용 |
| 4 | 큰 타입을 뒤에 선언 | 패딩 증가 | 큰 타입부터 선언 |
| 5 | Python으로 고빈도 대용량 처리 | CPU·지터 폭증 | 데이터 평면은 C++(2.8) |
| 6 | intra-process를 켜고 성능이 그대로 | 조건 미충족 | unique_ptr 발행·프로세스 배치 확인(2.7) |
| 7 | 벤더 혼용 시 타입 불일치 | 연결은 되는데 값이 깨짐 | 데이터 표현 설정·XCDR 버전 확인(3.6) |
| 8 | ros2 topic bw를 와이어 부하로 오해 | 실제 대역 과소 추정 | 인터페이스·패킷 수준 측정 |
| 9 | 제네릭 브리지가 타입 이름만 신뢰 | 잘못된 데이터 중계 | type hash 확인 |
| 10 | 이미지를 원본으로 무선 전송 | 링크 포화 | 압축 전송(7.2) |
| 11 | 프로세스를 갈라 둔 채 타입 적응 도입 | 복잡도만 증가 | intra-process와 짝지어야 이득(1.3.12.2) |
| 12 | 타입 적응 변환 함수에서 락·큰 할당 | 실시간 경로에 지터 유입 | 변환은 순수·저비용으로(12.3) |
| 13 | get_type_description에 빈 해시 전달 | Failed to parse type hash | topic info -v로 해시부터 얻기(1.3.13.2) |
1.3.11 체크포인트
.msg하나에서 생성되는 산출물 다섯 종류와 각각의 소비자를 말할 수 있는가.- typesupport가 함수 포인터 테이블이라는 사실이,
RMW_IMPLEMENTATION교체에 재빌드가 필요 없는 이유와 어떻게 연결되는가. - introspection typesupport가 없으면 못 하게 되는 일을 두 가지 이상 들 수 있는가.
- 임의의
.msg를 보고 CDR 직렬화 크기를 정렬·패딩까지 포함해 계산할 수 있는가. - 필드 순서를 바꾸면 무엇이 달라지는가 — 크기, 와이어 포맷, type hash 각각에 대해.
- type hash가 보장하는 것과 보장하지 않는 것을 구분할 수 있는가. 단위 혼동은 어느 쪽인가.
- 직렬화를 건너뛰는 두 경로와 각각의 조건을 말할 수 있는가.
- rclpy가 rclcpp보다 비용이 큰 이유를 계층 그림으로 설명하고, 완화책 세 가지를 들 수 있는가.
- 제네릭 발행·구독이 필요한 상황 세 가지와, 그 경로의 위험을 말할 수 있는가.
- 주요 토픽의 와이어 부하 상한을 식으로 추정하고, 멀티캐스트가 막힌 환경에서 그 값이 어떻게 변하는지 설명할 수 있는가.
- 타입 적응이 변환을 없애는 것이 아니라 옮기는 것이라는 말의 뜻을 배치(같은 프로세스/다른 프로세스)로 나누어 설명할 수 있는가.
- 적응 타입으로 발행한 토픽을
ros2 topic echo로 보면 무엇이 보이는가, 그리고 왜 그것이 오히려 이점인가. - type hash가 "다르다"만 알려 줄 때, 무엇이 다른지 알아내는 경로는 무엇인가.
다음 장 1.4에서는 이 바이트열이 실제로 오가는 토픽의 전 경로 — publish() 호출부터 구독 콜백 실행까지 — 를 계층별로 따라간다.