제1부 계산 그래프의 코어
1.2 인터페이스 정의 — msg·srv·action과 IDL
1.2.1 이 장에서 배우는 것 / 선수 지식
인터페이스는 시스템에서 가장 오래 사는 것이다. 노드는 다시 쓰고 알고리즘은 갈아치우지만, 메시지 정의는 한번 배포되면 바꾸기 어렵다 — 바꾸는 순간 그 타입을 쓰는 모든 노드·모든 bag 파일·모든 외부 연동이 함께 흔들린다. 그래서 인터페이스 설계는 API 설계이자 데이터 스키마 설계이며, 코드보다 신중하게 다뤄야 한다.
배우는 것:
.msg·.srv·.action문법의 전수 — 타입, 배열(고정·유한·무한), 기본값, 상수, 중첩.- 문자열·배열의 한계 지정이 왜 성능·안전에 직결되는가.
- IDL로의 변환 — ROS 2 인터페이스가 실제로는 OMG IDL 위에 구축돼 있다는 사실과 그 함의.
- 인터페이스 패키지의 빌드 구조 —
rosidl_generate_interfaces, 의존 선언, 순환 의존을 피하는 배치. - 표준 인터페이스 카탈로그 — 무엇이 이미 있고, 언제 새로 만들어야 하는가.
- 인터페이스 진화 전략 — 호환을 깨지 않고 바꾸는 방법과, 깨야 할 때의 절차.
- 설계 판단 — 큰 메시지 하나 vs 작은 메시지 여럿, 헤더 포함 여부, 커스텀 타입의 비용.
선수 지식: 0.6(패키지 구조·빌드), 1.1(이름). 직렬화·타입 지원의 내부는 다음 장 1.3에서 다루므로 여기서는 "무엇을 어떻게 쓰는가"에 집중한다.
정직: 아래 문법·타입 표는
rosidl_adapter의 파서와 ROS 2 인터페이스 설계 문서를 근거로 정리했다. 이 환경에서 빌드해 검증하지 않았다(목차 3) — 다만 액션이 서비스 3개·토픽 2개로 분해되는 것은 실물에서 실행 확인했다(1.2.6). 특히 유한 배열·유한 문자열의 실제 적용 범위는 RMW 구현에 따라 다를 수 있어 해당 절에(확인 필요)로 표시했다.
1.2.2 세 가지 인터페이스와 통신 패턴의 대응
| 파일 | 통신 패턴 | 구조 | 방향성 |
|---|---|---|---|
.msg | 토픽(발행·구독) | 필드 목록 | 단방향·다대다·비동기 |
.srv | 서비스(요청·응답) | 요청 --- 응답 | 양방향·1:1·단발 |
.action | 액션(목표·피드백·결과) | 목표 --- 결과 --- 피드백 | 양방향·장시간·취소 가능 |
패턴 선택 자체는 1.4(토픽)·1.7(서비스)·1.8(액션)에서 판단 기준과 함께 다룬다. 여기서는 정의 파일을 어떻게 쓰는가가 주제다.
인터페이스 패키지의 표준 배치는 다음과 같다.
| 경로 | 내용 | 비고 |
|---|---|---|
my_interfaces/msg/MotorState.msg | 토픽 메시지 | 파일명 UpperCamelCase |
my_interfaces/srv/SetMode.srv | 서비스 | 동일 |
my_interfaces/action/Calibrate.action | 액션 | 동일 |
my_interfaces/package.xml | 메타데이터·의존 | member_of_group 필수(1.2.5) |
my_interfaces/CMakeLists.txt | rosidl_generate_interfaces 호출 | 동일 |
msg·srv·action 디렉터리 이름은 관례가 아니라 도구가 기대하는 이름이다(ros2 interface 계열과 생성기가 이 배치를 전제한다).
파일명은 UpperCamelCase여야 한다(MotorState.msg, motor_state.msg 아님). 생성되는 타입 이름이 파일명에서 나오기 때문이다. 필드 이름은 snake_case.
1.2.3 기본 타입과 필드 문법
기본 타입:
| 분류 | 타입 |
|---|---|
| 불리언 | bool |
| 정수 | int8 uint8 int16 uint16 int32 uint32 int64 uint64 |
| 실수 | float32 float64 |
| 문자 | char(= uint8), byte(= uint8) — 의미만 다름 |
| 문자열 | string, wstring |
ROS 1의 time·duration 내장 타입은 없다. ROS 2에서는 builtin_interfaces/msg/Time·builtin_interfaces/msg/Duration 메시지 타입을 쓴다. 이것은 단순한 이름 변경이 아니라 설계 변경이다 — 시간이 일반 메시지가 되면서 IDL로 표현 가능해졌다(1.2.6).
필드 선언:
# MotorState.msg
std_msgs/Header header # 다른 패키지 타입 중첩
string name # 무한 문자열
float64 position # 라디안 또는 미터 (REP 103)
float64 velocity
float64 effort
uint8 mode 0 # 기본값 0
bool enabled true # 기본값 true
배열 세 종류 — 이 구분이 성능과 안전을 가른다:
| 표기 | 이름 | C++ 매핑 | 특징 |
|---|---|---|---|
float64[3] | 고정 크기 | std::array<double,3> | 크기 불변. 힙 할당 없음. |
float64[<=10] | 유한(bounded) | rosidl_runtime_cpp::BoundedVector | 최대 크기 보장. 상한이 계약이 됨. |
float64[] | 무한(unbounded) | std::vector<double> | 제한 없음. 동적 할당. |
문자열도 같은 방식으로 한계를 준다.
string<=32 device_id # 최대 32바이트
string<=16[<=8] tags # 최대 8개, 각 최대 16바이트
왜 한계를 지정하는가 — 이것이 실무적으로 중요한 이유:
- 무할당 경로(zero-allocation)의 전제. 실시간 루프에서 힙 할당은 예측 불가능한 지연을 만든다(12.3). 메시지가 전부 고정·유한이면 크기가 컴파일 시점에 상한이 정해지고, 미리 할당해 둔 풀에서 꺼내 쓸 수 있다.
- loaned message·zero-copy의 전제. 공유 메모리 전송(2.7)은 고정 크기 메시지(plain old data) 에서 가장 효과가 크다. 무한 배열이 하나라도 있으면 이 경로가 막히거나 효율이 떨어진다.
- 방어. 상한이 없으면 악의적·버그성 거대 메시지가 수신 측 메모리를 소모한다(13.1).
- DDS 자원 계획. 유한 크기를 알면 DDS가 버퍼를 미리 할당할 수 있다.
함정: 이론은 위와 같지만 실제 이득은 RMW 구현과 QoS 설정에 달렸다. "유한 배열을 썼으니 할당이 없다"는 보장은 아니다 — 특히 Python(rclpy)에서는 어차피 객체 할당이 일어난다. 무할당 경로의 실제 조건은 2.6·12.3에서 다루며, 여기서는 "실시간을 노린다면 처음부터 유한하게 쓰라" 는 원칙만 세운다. 나중에 무한→유한으로 바꾸는 것은 호환을 깨는 변경이다. (
(확인 필요): 유한 배열이 실제 RMW에서 어떤 자원 계획으로 이어지는지는 벤더마다 다르다 — 3.6.)
상수:
# 상수는 대문자, '=' 사용
uint8 MODE_IDLE=0
uint8 MODE_RUN=1
uint8 MODE_FAULT=2
uint8 mode # 위 상수 중 하나
C++에서는 MotorState::MODE_RUN, Python에서는 MotorState.MODE_RUN으로 접근한다. 열거형이 없으므로 이 관용구가 사실상의 enum이다. 상수와 필드를 같은 파일에 두고 주석으로 관계를 명시하는 것이 관행이다.
함정: 기본값과 상수를 혼동하지 말 것.
uint8 mode 0은 기본값(초기화 값),uint8 MODE_IDLE=0은 상수(변경 불가 심볼)다. 문법이 비슷해 실수하기 쉽다.
1.2.4 srv와 action의 구조
# SetMode.srv
uint8 MODE_IDLE=0
uint8 MODE_RUN=1
uint8 mode
---
bool success
string message
--- 위가 요청, 아래가 응답. 응답에 bool success + string message를 두는 것은 std_srvs/srv/Trigger에서 온 강한 관행이며, 에러를 예외가 아니라 데이터로 돌려주는 방식이다. 서비스에는 별도 에러 채널이 없으므로(1.7) 이 관행을 따르는 편이 좋다.
# Calibrate.action
# --- 목표(Goal) ---
uint8 axis
float64 timeout_sec
---
# --- 결과(Result) ---
bool success
float64 offset
string message
---
# --- 피드백(Feedback) ---
float64 progress # 0.0 ~ 1.0
string stage
액션은 ---로 세 부분을 나눈다. 생성되는 타입은 Calibrate::Goal, Calibrate::Result, Calibrate::Feedback이며, 그 외에 액션 프로토콜용 내부 타입(SendGoal·GetResult·FeedbackMessage)이 자동으로 만들어진다 — 액션 하나가 실제로는 서비스 3개 + 토픽 2개로 구현되기 때문이다(1.8). 실물로 확인했다(85차, 실행 확인): 액션 서버 하나를 실행하면 <이름>/_action/send_goal·cancel_goal·get_result 서비스 셋과 <이름>/_action/feedback·status 토픽 둘이 정확히 그만큼 생긴다.
함정: 피드백에 대용량 데이터(이미지·포인트클라우드)를 넣지 말 것. 피드백은 자주 흐르고, 액션 클라이언트가 여럿이면 그만큼 곱해진다. 대용량 데이터는 별도 토픽으로 보내고 피드백에는 참조(프레임 번호·타임스탬프)만 싣는 것이 관행이다.
1.2.5 인터페이스 패키지의 빌드 구조
CMakeLists.txt:
cmake_minimum_required(VERSION 3.8)
project(my_interfaces)
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(std_msgs REQUIRED) # 중첩해 쓰는 패키지
find_package(builtin_interfaces REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/MotorState.msg"
"srv/SetMode.srv"
"action/Calibrate.action"
DEPENDENCIES std_msgs builtin_interfaces
)
ament_export_dependencies(rosidl_default_runtime)
ament_package()
package.xml:
<buildtool_depend>ament_cmake</buildtool_depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<depend>std_msgs</depend>
<depend>builtin_interfaces</depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
<member_of_group>rosidl_interface_packages</member_of_group>가 빠지는 것이 가장 흔한 빌드 실패 원인이다. 이 선언이 있어야 생성기들이 이 패키지를 인터페이스 패키지로 인식한다.
같은 패키지에서 인터페이스와 노드를 함께 빌드할 때 링크가 필요하다.
# 같은 패키지 안에서 자기가 만든 타입을 쓰려면
rosidl_get_typesupport_target(cpp_typesupport_target
${PROJECT_NAME} rosidl_typesupport_cpp)
target_link_libraries(my_node "${cpp_typesupport_target}")
견해: 인터페이스는 별도 패키지로 분리하는 것이 거의 항상 옳다(
my_robot_interfaces). 이유 셋. ① 인터페이스를 쓰는 쪽이 로직 패키지의 규모가 큰 의존을 끌어오지 않는다. ② 인터페이스만 배포·공유할 수 있다(외부 팀·다른 언어 클라이언트). ③ 순환 의존을 구조적으로 막는다 — 인터페이스 패키지는 아무것도 의존하지 않는 잎(leaf)이 된다. 대가는 패키지 수 증가와 빌드 순서 의존이며, 이 정도는 비용이 낮다.
1.2.6 IDL — ROS 2 인터페이스의 실제 정체
.msg는 최종 형식이 아니다. 빌드 과정에서 rosidl_adapter가 .msg·.srv·.action을 OMG IDL(.idl) 로 변환하고, 그 다음부터 모든 생성기는 IDL을 입력으로 삼는다.
왜 IDL을 거치는가:
- DDS가 IDL 기반이다. DDS·RTPS의 타입 시스템이 OMG IDL이므로, 여기에 맞추면 DDS 벤더의 기존 도구 사슬을 그대로 쓸 수 있다.
- 표현력.
.msg문법은 단순해서 표현 못 하는 것이 있다(중첩 모듈, 어노테이션). IDL은 그것을 담는다. - 비ROS 시스템과의 상호운용. IDL이 있으면 ROS를 쓰지 않는 DDS 애플리케이션도 같은 타입으로 통신할 수 있다 — 산업 현장에서 기존 DDS 인프라와 연결할 때 실제로 쓰인다.
.idl을 직접 쓸 수도 있다. rosidl_generate_interfaces에 .idl 파일을 그대로 넘기면 어댑터를 건너뛴다. .msg로 표현 못 하는 구조가 필요할 때(예: 특정 어노테이션) 쓴다. 다만 .msg가 정본인 편이 생태계 도구와 잘 맞으므로, 특별한 이유가 없으면 .msg를 쓴다.
정직:
.idl직접 사용은 문서화·도구 지원이.msg보다 얇다.ros2 interface show가 어떻게 보여주는지 등 세부는(확인 필요).
1.2.7 표준 인터페이스 — 만들기 전에 찾아라
커스텀 타입을 만드는 것은 비용이다. 표준 타입을 쓰면 RViz2가 바로 그려주고(11.4), rosbag2가 이해하고, 기존 노드가 연결된다. 커스텀 타입은 이 모든 것을 직접 해야 한다.
| 패키지 | 대표 타입 | 용도 |
|---|---|---|
std_msgs | Header, String, Bool, Float64 | 원시 타입 래퍼는 쓰지 말 것(아래) |
builtin_interfaces | Time, Duration | 시간 표현 |
geometry_msgs | Pose, Twist, Transform, PoseStamped, TwistStamped, Wrench, Accel | 기하 |
sensor_msgs | Image, CompressedImage, CameraInfo, PointCloud2, LaserScan, Imu, JointState, NavSatFix, BatteryState, Range | 센서(7.1) |
nav_msgs | Odometry, Path, OccupancyGrid, MapMetaData | 항법(8.1) |
trajectory_msgs | JointTrajectory, MultiDOFJointTrajectory | 궤적(6.5) |
diagnostic_msgs | DiagnosticArray, DiagnosticStatus | 진단(6.9) |
shape_msgs, visualization_msgs | SolidPrimitive, Marker, MarkerArray | 충돌·시각화 |
std_srvs | Trigger, SetBool, Empty | 흔한 서비스 |
action_msgs | GoalStatus, GoalInfo | 액션 내부(1.8) |
lifecycle_msgs | State, Transition | 라이프사이클(1.12) |
rcl_interfaces | ParameterEvent, SetParametersResult, Log | 파라미터·로깅(1.9·1.11) |
tf2_msgs | TFMessage | tf(5.1) |
control_msgs | JointJog, GripperCommand 등 | ros2_control(6.5) |
std_msgs의 원시 래퍼(std_msgs/msg/Float64 등)를 쓰지 말라는 것은 ROS 2 커뮤니티의 명시적 권고다. 이유: Float64 하나만 보면 그것이 무엇의 값이고 어떤 단위이며 언제 측정된 것인지 알 수 없다. 의미 있는 타입을 만들거나(단위·프레임·타임스탬프 포함), 기존 의미 타입을 써야 한다. 디버깅·프로토타이핑에는 편리하지만 제품 인터페이스로 남기면 부채가 된다.
Header를 넣을 것인가:
std_msgs/Header header # stamp(시간) + frame_id(좌표계)
- 넣어야 하는 경우: 데이터가 특정 시점에 특정 좌표계에서 온 것일 때 — 사실상 모든 센서 데이터. tf2 변환(5.1)·시간 동기화(7.7)·bag 재생(11.3)이 전부
header.stamp에 의존한다. - 없어도 되는 경우: 시점·좌표계와 무관한 설정·명령·상태(모드 전환, 불리언 플래그).
- 애매하면 넣어라. 나중에 추가하는 것은 호환을 깨는 변경이다.
geometry_msgs에 Pose와 PoseStamped가 둘 다 있는 이유가 이것이다. Stamped 접미어가 붙은 것은 헤더가 있는 버전이며, 로봇 시스템에서 노드 간 경계를 넘는 데이터는 대개 Stamped 쪽이 맞다.
함정:
geometry_msgs/msg/Twist에는 헤더가 없다./cmd_vel이Twist인 것은 역사적 이유이며, 그래서 "이 속도 명령이 어느 프레임 기준인가"가 메시지 안에 없다 — 관행(base_link)에 의존한다. Nav2·ros2_control 생태계 일부는TwistStamped로 옮겨가는 중이며, Jazzy 시점에서 패키지마다 어느 쪽을 쓰는지 갈린다(확인 필요). 통합 시 반드시 확인할 지점이다(8.5·6.5).
1.2.8 인터페이스 진화 — 호환을 깨지 않고 바꾸기
결론부터: ROS 2에서 메시지 정의 변경은 기본적으로 파괴적이다. ROS 1의 MD5 체크섬과 마찬가지로 ROS 2도 타입 정합성을 검사하며, Jazzy에서는 type hash(RIHS01) 로 이를 노출한다(1.3). 필드를 하나만 추가해도 해시가 바뀌고, 구버전·신버전 노드는 서로를 다른 타입으로 본다.
전략 ① 전면 교체(빅뱅) — 모든 노드를 함께 재빌드·재배포.
- 장점: 단순, 부채 없음.
- 단점: 무중단 배포 불가. 필드에 나간 로봇을 한꺼번에 업그레이드해야 한다. 기존 bag 파일이 새 코드로 안 읽힌다.
- 맞는 경우: 단일 팀·단일 배포 단위, 개발 초기.
전략 ② 병행(v2 타입 신설) — MotorState2 또는 my_interfaces_v2/msg/MotorState를 새로 만들고, 두 타입을 잠시 함께 발행.
- 장점: 무중단 이행. 소비자가 각자 속도로 옮긴다.
- 단점: 대역·CPU 이중 소모, 브리지 노드 필요, 정리 시점을 정해야 한다.
- 맞는 경우: 다수 팀·외부 소비자·필드 배포 중.
전략 ③ 확장 여지를 미리 둔다 — 처음부터 string[] extra_keys / string[] extra_values 같은 확장 필드나 자유 형식 문자열을 둔다.
- 장점: 소소한 추가에 재빌드가 필요 없다.
- 단점: 타입 안전을 스스로 포기하는 것. 스키마가 문서 밖으로 새고, 검증이 런타임으로 미뤄진다. 남용하면 "JSON을 메시지 안에 넣은" 상태가 되어 ROS 2 타입 시스템의 이점이 사라진다.
- 맞는 경우: 진단·메타데이터처럼 핵심 제어 경로가 아닌 곳에 한정.
견해: 기본은 ①, 외부 소비자가 있으면 ②. ③은 진단·로깅 계열에만 제한적으로 쓴다. 그리고 어느 쪽이든 초기 설계에 시간을 더 쓰는 것이 가장 비용이 낮다 — 인터페이스는 코드보다 오래 유지된다. 특히
Header포함 여부, 단위(SI — REP 103), 배열 한계, 상수 체계는 처음에 정하고 시작하라.
bag 호환은 별개 문제다. rosbag2는 기록 시점의 메시지 정의를 파일에 함께 저장하므로 재생 자체는 가능하지만, 새 정의로 컴파일된 노드가 옛 데이터를 역직렬화하려 하면 깨진다. 장기 데이터셋을 유지해야 한다면 이 점이 전략 선택을 지배한다(11.2·11.3).
1.2.9 설계 판단 — 굵은 메시지 vs 잔 메시지
같은 정보를 한 메시지로 묶을 것인가, 여러 토픽으로 분할할 것인가.
① 묶는다(굵은 메시지) — RobotState 하나에 관절·배터리·모드·오류를 전부.
- 장점: 원자성. 한 시점의 일관된 스냅숏이 보장된다. 구독자가 시간 정렬을 안 해도 된다(7.7의
message_filters가 불필요). - 단점: 일부만 필요한 구독자도 전부 받는다(대역·CPU 낭비). 한 필드의 갱신 주기가 다른 필드를 끌고 간다. 변경 시 영향 범위가 넓다.
② 분할한다(잔 메시지) — /joint_states, /battery, /mode를 각각.
- 장점: 필요한 것만 구독. 주기·QoS를 항목별로 다르게(배터리 1 Hz
TRANSIENT_LOCAL, 관절 500 HzBEST_EFFORT— 1.6). 변경 격리. - 단점: 시간 정렬 부담이 구독자에게 넘어간다. 토픽 수 증가 → 디스커버리·엔티티 비용 증가(3.3).
| 판단 축 | 묶기 유리 | 분할 유리 |
|---|---|---|
| 소비자가 항상 전부 필요한가 | 예 | 아니오 |
| 항목별 주기가 같은가 | 같다 | 크게 다르다 |
| 항목별 QoS가 같은가 | 같다 | 다르다 |
| 원자적 일관성이 필요한가 | 필요 | 불필요 |
| 항목이 자주 추가되는가 | 아니오 | 예 |
견해: 제어에 쓰이는 상태는 묶고(원자성), 관측·기록용은 분할한다를 기본으로 본다.
sensor_msgs/msg/JointState가 관절 전체를 하나로 묶은 것은 원자성 때문이고, 진단이 별도 토픽인 것은 주기·소비자가 다르기 때문이다. 두 방식은 배타적이지 않다 — 제어용 묶음 토픽과 관측용 분리 토픽을 함께 두는 시스템도 흔하다.
1.2.10 인터페이스 검사 명령
ros2 interface list # 설치된 전체
ros2 interface packages # 인터페이스를 가진 패키지
ros2 interface package sensor_msgs # 그 패키지의 타입 목록
ros2 interface show sensor_msgs/msg/Imu # 정의(중첩 펼침)
ros2 interface proto sensor_msgs/msg/Imu # 채워 넣을 YAML 뼈대
ros2 interface show는 중첩 타입을 펼쳐 보여주므로 실제 필드 구조를 파악하는 데 가장 빠르다. proto는 ros2 topic pub·ros2 service call의 인자를 직접 만들 때 쓴다(0.7.7).
1.2.11 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | <member_of_group>rosidl_interface_packages</member_of_group> 누락 | 코드 생성 안 됨·링크 실패 | package.xml에 추가 |
| 2 | 파일명 snake_case | 빌드 실패 또는 비정상적인 타입 이름 | UpperCamelCase |
| 3 | 인터페이스와 노드를 같은 패키지에 두고 링크 누락 | 자기 타입을 못 찾음 | rosidl_get_typesupport_target |
| 4 | 인터페이스 패키지가 로직 패키지를 의존 | 순환 의존·빌드 교착 | 인터페이스 패키지는 잎으로 |
| 5 | std_msgs/msg/Float64를 제품 인터페이스로 | 의미·단위·시각 불명 | 의미 있는 타입 정의 |
| 6 | Header 누락 후 나중에 추가 | 전체 재배포 필요 | 애매하면 처음부터 포함 |
| 7 | 무한 배열만 사용 | 실시간 경로에서 할당 지터 | 유한 배열·문자열(12.3) |
| 8 | 기본값과 상수 혼동 | 의도와 다른 값 | uint8 x 0 vs uint8 X=0 |
| 9 | 액션 피드백에 이미지 탑재 | 대역 폭증 | 참조만 싣고 별도 토픽 |
| 10 | 필드 순서 변경을 "무해"하다고 판단 | 와이어 포맷·해시 변경 | 순서도 계약의 일부(1.3) |
| 11 | Twist vs TwistStamped 혼용 | 통합 시 타입 불일치로 구독이 매칭 안 됨 | Jazzy diff_drive_controller는 TwistStamped만 받는다(6.5.12.1) |
| 12 | 단위를 문서화하지 않음 | deg/rad·mm/m 혼동 사고 | REP 103(SI) 준수 + 주석 |
1.2.12 체크포인트
.msg의 배열 세 종류를 쓰고, 유한 배열이 실시간·zero-copy와 어떻게 연결되는지 설명할 수 있는가. 그 이득이 보장인지 가능성인지 구분할 수 있는가.- 기본값 문법과 상수 문법을 즉시 구분해 쓸 수 있는가.
- ROS 2가
.msg를 IDL로 변환하는 이유 세 가지를 말할 수 있는가. - 인터페이스를 별도 패키지로 분리하는 이유 세 가지와, 그 대가를 말할 수 있는가.
std_msgs/msg/Float64를 제품 인터페이스로 쓰지 말라는 권고의 근거를 설명할 수 있는가.- 어떤 메시지에
Header를 넣을지 판단 기준을 세울 수 있는가.Twist에 헤더가 없어서 생기는 실무 문제는 무엇인가. - 이미 배포된 메시지에 필드를 추가해야 할 때, ①②③ 전략 중 무엇을 고르겠는가. 기존 bag 파일이 그 판단에 어떤 영향을 주는가.
- 상태 정보를 굵은 메시지 하나로 묶을지 여러 토픽으로 분할할지, 판단 축 다섯 가지로 설명할 수 있는가.
다음 장 1.3에서는 이 정의가 코드와 바이트열이 되는 과정 — rosidl 코드 생성, 타입 지원(typesupport), CDR 직렬화, 그리고 Jazzy에서 노출된 type hash — 를 연다.