AEGLOSLAB ROS 2 Jazzy 대전

제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가 실행되는 결과는 헤더 하나가 아니다. 여러 생성기가 각각의 산출물을 만든다.

MotorState.msg rosidl_adapter MotorState.idl rosidl_generator_c C 구조체 rosidl_generator_cpp C++ 구조체 rosidl_generator_py Python 클래스 + C 확장 typesupport_introspection 필드 메타데이터 typesupport_fastrtps 벤더별 직렬화
산출물무엇언제 쓰이나
C 구조체motor_state__struct.h 등rcl·C 노드·다른 언어 바인딩의 기반
C++ 구조체motor_state.hpprclcpp 사용자 코드
Python 클래스 + C 확장_motor_state.py, _motor_state_s.crclpy — Python 객체 ↔ C 구조체 변환기 포함
introspection typesupport필드 이름·타입·오프셋 테이블런타임에 타입을 모르고 다뤄야 하는 도구(rosbag2·CLI·rqt)
벤더 typesupportMotorState 전용 직렬화 함수해당 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 핸들만 받는다.

사용자 코드 create_publisher typesupport 핸들 획득 (템플릿, 컴파일 시점) rcl_publisher_init (void* + 핸들) rmw_create_publisher 벤더 typesupport 선택 identifier 매칭

두 갈래의 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) 이다. 규칙은 세 가지로 요약된다.

  1. 필드는 선언 순서대로 나열된다.
  2. 각 기본 타입은 자기 크기에 맞춰 정렬(align) 된다 — uint32는 4의 배수 위치, float64는 8의 배수 위치. 맞추기 위해 패딩 바이트가 들어간다.
  3. 가변 길이(문자열·시퀀스)는 앞에 uint32 길이가 붙는다. 문자열은 널 종료 문자를 포함해 센다.

앞에 인코딩 헤더 4바이트(representation identifier + options)가 붙어 엔디언과 인코딩 버전을 알린다. 그래서 빅엔디언·리틀엔디언 기계가 섞여 있어도 동작한다 — 수신 측이 필요하면 바이트를 뒤집는다.

직접 계산해 보기:

# Sample.msg
uint8  flag
float64 value
uint32 count
string name        # "abc" 라고 하자
오프셋내용크기
0인코딩 헤더4
4flag (uint8)1
5패딩 (8 정렬 맞춤)3
8value (float64)8
16count (uint32)4
20name 길이 (uint32, "abc\0" = 4)4
24name 내용 a b c \04
합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 직렬화를 건너뛰는 경로

지금까지의 그림은 "직렬화해서 네트워크로 보낸다"였다. 그런데 같은 프로세스 안이면 이 전부가 낭비다.

아니오 예 예 아니오 publish(msg) 같은 프로세스 + intra-process 활성? 포인터 전달 직렬화 없음 직렬화 (CDR) 같은 호스트 + 공유메모리? 공유 메모리 전송 UDP·TCP 네트워크
경로직렬화복사조건
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보다 훨씬 비용이 크다. 계층으로 보면 이유가 분명하다.

Python 객체 (속성 접근·타입 검사) _s.c 변환 코드 Python → C 구조체 C 구조체 CDR 직렬화 rmw → DDS

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_descriptionIndividualTypeDescription — 필드 이름·type_id·capacity·중첩 타입 이름, 그리고 참조된 타입들
type_sourcesinclude_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큰 타입을 뒤에 선언패딩 증가큰 타입부터 선언
5Python으로 고빈도 대용량 처리CPU·지터 폭증데이터 평면은 C++(2.8)
6intra-process를 켜고 성능이 그대로조건 미충족unique_ptr 발행·프로세스 배치 확인(2.7)
7벤더 혼용 시 타입 불일치연결은 되는데 값이 깨짐데이터 표현 설정·XCDR 버전 확인(3.6)
8ros2 topic bw를 와이어 부하로 오해실제 대역 과소 추정인터페이스·패킷 수준 측정
9제네릭 브리지가 타입 이름만 신뢰잘못된 데이터 중계type hash 확인
10이미지를 원본으로 무선 전송링크 포화압축 전송(7.2)
11프로세스를 갈라 둔 채 타입 적응 도입복잡도만 증가intra-process와 짝지어야 이득(1.3.12.2)
12타입 적응 변환 함수에서 락·큰 할당실시간 경로에 지터 유입변환은 순수·저비용으로(12.3)
13get_type_description에 빈 해시 전달Failed to parse type hashtopic 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() 호출부터 구독 콜백 실행까지 — 를 계층별로 따라간다.