제3부 미들웨어: DDS·RTPS·RMW
3.5 RMW 계층 — API와 구현 교체
3.5.1 이 장에서 배우는 것 / 선수 지식
이 책은 계속 "벤더마다 다르다"고 말해 왔다. 이 장은 그 경계선을 정확히 긋는다. rmw는 ROS 2가 미들웨어에게 요구하는 계약이며, 그 계약 안이 이식 가능한 영역, 밖이 벤더 영역이다.
실무적 질문 셋에 답한다. ① RMW_IMPLEMENTATION을 바꾸면 정확히 무엇이 달라지는가. ② 서로 다른 RMW를 쓰는 노드끼리 통신되는가. ③ 벤더를 바꿀 때 무엇을 다시 검증해야 하는가.
배우는 것:
- rmw API의 표면 — 어떤 함수들이 계약을 이루는가.
- 런타임 로딩 메커니즘 — 재빌드 없이 교체되는 원리.
- RMW 혼용 — 되는가, 안 되는가, 왜 그런가.
- 기능 협상 — 벤더가 지원하지 않는 기능은 어떻게 처리되는가.
- 교체 시 재검증 목록 — 무엇이 실제로 달라지는가.
- 커스텀 rmw — 만든다면 무엇을 구현해야 하는가(개관).
선수 지식: 2.1(계층), 3.1(DDS), 1.5(QoS), 1.3(typesupport).
정직: rmw API 함수 목록·시그니처는
ros2/rmw저장소 헤더가 정본이며 배포판마다 추가·변경된다. 아래는 개념적 분류이며 전수가 아니다(확인 필요). RMW 혼용 가능 여부는 특히 조건이 많아 해당 절에서 자세히 다룬다. 이 환경에서 검증하지 않았다(목차 3). 141차(120번째 회차)가ros:jazzy-ros-base·ros:humble-ros-base에서 이 장의 핵심 주장을 실행 확인했다 — 기본 RMW·환경변수 교체·미설치 시 실패 메시지·RMW 혼용(토픽은 되고 서비스는 실제로 멈춤)까지. 아래 각 절의 정정·추가 블록을 보라.
3.5.2 rmw는 무엇을 약속하는가
rmw는 C 헤더의 모음이다. 구현은 없고, 함수 시그니처와 자료구조만 있다.
API 표면의 분류:
| 분류 | 대표 함수 | 역할 |
|---|---|---|
| 초기화 | rmw_init·rmw_shutdown·rmw_context_fini | 컨텍스트 수명 |
| 노드 | rmw_create_node·rmw_destroy_node | 참가자 생성 |
| 발행 | rmw_create_publisher·rmw_publish·rmw_publish_serialized_message | 데이터 송신 |
| 구독 | rmw_create_subscription·rmw_take·rmw_take_with_info | 데이터 수신 |
| 서비스 | rmw_create_service·rmw_send_request·rmw_take_response | 요청·응답(1.7.2) |
| 대기 | rmw_create_wait_set·rmw_wait | executor의 기반(2.2.4) |
| 그래프 | rmw_get_node_names·rmw_get_topic_names_and_types | 그래프 질의(1.1.8) |
| GID·비교 | rmw_get_gid_for_publisher·rmw_compare_gids_equal | 엔티티 식별(3.2.3) |
| QoS | rmw_qos_profile_check_compatible | 호환성 판정(1.5.4) |
| 이벤트 | rmw_take_event | QoS 이벤트(1.4.6) |
| loaned | rmw_borrow_loaned_message·rmw_publish_loaned_message | 무복사(2.6.5) |
| 보안 | enclave·security options | SROS2(13.2) |
| 기능 질의 | rmw_feature_supported 등 | 지원 여부 확인(확인 필요) |
이 목록이 곧 "ROS 2가 미들웨어에게 요구하는 전부"다. 여기 없는 것은 ROS 2 표준 API로 접근할 수 없다 — 3.1.7에서 본 DDS QoS 대부분이 여기 없는 이유다.
반대 방향도 성립한다. rmw에 있는 개념(노드·서비스·액션)은 DDS에 없으므로, rmw_*_cpp가 번역한다. 서비스를 토픽 두 개로 만드는 것(1.7.2), 토픽 이름에 rt/ 접두어를 붙이는 것(2.1.4)이 그 번역이다.
3.5.3 런타임 로딩 — 재빌드가 필요 없는 이유
핵심은 rmw_implementation 패키지다. 이것이 rmw 함수를 구현하되, 내용은 실제 구현 라이브러리로 위임한다.
# 사용 가능한 구현 확인
ros2 doctor --report | grep -i rmw
ls /opt/ros/jazzy/lib/librmw_*.so
# 교체
RMW_IMPLEMENTATION=rmw_cyclonedds_cpp ros2 run demo_nodes_cpp talker
# 현재 무엇을 쓰는지
ros2 doctor --report | grep -i middleware
typesupport도 함께 선택된다(1.3.3). 메시지 타입마다 벤더별 typesupport 라이브러리가 있고, 선택된 RMW의 identifier와 일치하는 것이 런타임에 로드된다. 그래서 RMW를 바꿔도 메시지 패키지를 재빌드할 필요가 없다 — 필요한 typesupport가 이미 설치되어 있다면.
함정: 모든 RMW용 typesupport가 항상 설치되어 있는 것은 아니다.
ros-jazzy-rmw-cyclonedds-cpp를 설치하지 않고RMW_IMPLEMENTATION=rmw_cyclonedds_cpp를 지정하면 로딩에 실패한다(실행 확인, 141차 — Humble·Jazzy 양쪽에서 재현:Error getting RMW implementation identifier / RMW implementation not installed ... failed to load shared library 'librmw_cyclonedds_cpp.so' due to dlopen error: ... No such file or directory. 조용한 폴백이 아니라 즉시·명확한 오류로 실패하고 프로세스가 종료된다 —ros2 topic list같은 CLI 명령도 예외 없이 이 경로를 그대로 거친다). 커스텀 메시지 패키지도 마찬가지로, 빌드 시점에 설치되어 있던 RMW들의 typesupport만 생성된다(확인 필요: 생성 조건).
실행 확인(141차, Humble·Jazzy 양쪽):
ros-jazzy-rmw-cyclonedds-cpp(Humble은ros-humble-)를 설치한 뒤RMW_IMPLEMENTATION=rmw_cyclonedds_cpp로talker/listener를 실행하면 정상적으로 통신한다 — 재빌드 없이 교체된다는 이 절의 핵심 주장이 확인됐다. 다만 Cyclone DDS 쪽은 매 실행마다[WARN] [rmw_cyclonedds_cpp]: Failed to parse type hash for topic '...' from USER_DATA '(null)'경고를 그래프의 모든 토픽·서비스에 대해 반복 출력했다(기능에는 영향 없음, 3.5.4의 혼용 실험에서도 동일하게 관찰됨 — Fast DDS 참가자가 광고하는 타입 해시 형식을 Cyclone DDS가 못 읽는 것으로 보인다).
sudo apt install ros-jazzy-rmw-cyclonedds-cpp # 필요한 구현 설치
함정: 환경변수는 프로세스 단위다. 노드마다 다른 RMW로 실행하는 것이 기술적으로 가능하며, 그것이 다음 절의 문제로 이어진다.
3.5.4 RMW 혼용 — 되는가?
짧은 답: 공식적으로 지원되지 않으며, 기대하지 말아야 한다.
긴 답은 흥미롭다. RTPS는 상호운용 표준이므로(3.2.2) 서로 다른 DDS 벤더가 원칙적으로는 통신할 수 있다. 실제로 조건이 맞으면 되는 경우도 있다. 그런데 조건이 많다.
| 통신 종류 | 혼용 시 |
|---|---|
| 토픽 | 조건이 맞으면 동작할 수 있다(확인 필요) |
| 서비스 | 위험 — ROS 2 서비스는 벤더별 구현 세부(상관 ID 처리)에 의존해 왔다 |
| 액션 | 서비스에 의존하므로 동일 |
| 그래프 질의 | 참가자 정보 해석이 달라 불완전할 수 있다 |
| loaned·공유메모리 | 벤더 전용이므로 불가 |
| 보안 | 설정·플러그인이 달라 사실상 불가 |
견해: 혼용을 "되는지 시험해 보고 되면 쓰는" 것은 잘못된 결정이다. 개발 중에는 동작하다가 패치 릴리스에서 깨질 수 있고, 그 실패는 조용하다(연결이 안 되거나 데이터가 이상함). 시스템 전체를 하나의 RMW로 통일하라. 서로 다른 미들웨어를 이어야 한다면 명시적 브리지 프로세스를 두는 것이 옳다 — 양쪽에 각각 참여하는 두 프로세스가 데이터를 옮기는 구조(1.1.7의 도메인 브리지와 같은 발상).
혼용을 진단하는 법:
# 각 노드가 무엇을 쓰는지 확인하기 어렵다 — 환경을 확인하는 것이 실질적
cat /proc/$(pgrep -f my_node)/environ | tr '\0' '\n' | grep RMW
# 패킷 캡처에서 벤더 ID 확인 (3.2.3)
# Wireshark: rtps.vendorId
벤더 ID로 확인하는 것이 확실하다. 캡처에서 서로 다른 벤더 ID가 보이면 혼용 중이다.
실행 확인(141차, Humble·Jazzy 양쪽) — 위 표의 "토픽"·"서비스" 행을 직접 재현했다. 같은 컨테이너(같은 네트워크 네임스페이스, 기본 도메인 ID)에서
RMW_IMPLEMENTATION=rmw_fastrtps_cpp(기본)로demo_nodes_cpp talker를 실행하고,RMW_IMPLEMENTATION=rmw_cyclonedds_cpp로 구독·서비스 클라이언트를 실행했다.
- 토픽: 실제로 통신됐다.
ros2 topic echo /chatter --once(Cyclone 쪽)가 Fast DDS 쪽talker가 보낸data: 'Hello World: N'을 정확히 받았다(Humble·Jazzy 둘 다). RTPS가 상호운용 표준이라는 이 절의 설명대로다. 다만 Cyclone DDS 쪽에서 그래프의 모든 엔티티(우리 토픽뿐 아니라ros_discovery_info·rosout·서비스 요청/응답 토픽까지)에 대해 "Failed to parse type hash ... USER_DATA '(null)'" 경고가 반복됐다 — 통신 자체는 되지만 조용하지 않다.- 서비스: 표의 "위험" 경고가 정확히 재현됐다. Fast DDS로 실행한
gap140_trigger서비스 서버에 대해, Cyclone DDS 클라이언트의ros2 service list는 그 서비스를 정상적으로 발견했다 — 하지만 실제ros2 service call /gap140_trigger std_srvs/srv/Trigger {}은requester: making request: ...를 출력한 뒤 응답을 받지 못하고 타임아웃까지 멈췄다(Humble·Jazzy 둘 다, 8초 대기까지 무응답). 디스커버리(그래프 질의)와 실제 요청·응답 상관관계 처리가 서로 다른 신뢰도로 동작한다는 뜻이다 — 서비스가 "보이는데" 실제로는 안 되는, 표가 경고하는 바로 그 조용한 실패 양상이다.결론: 이 절의 flowchart가 "⚠️ 동작할 수 있음, 단 비공식"이라 표시한 최종 상태가 토픽에서는 실제로 관찰됐고, 서비스의 "위험" 경고도 실측으로 확인됐다. "짧은 답: 공식적으로 지원되지 않으며 기대하지 말아야 한다"는 여전히 옳다 — 토픽이 된 것은 이번 버전 조합·로컬 네트워크 조건에서일 뿐, 패치 릴리스 하나로 깨질 수 있다는 이 절의 경고 그대로다. 목차 3,
tools/gap_harness/gap141.
3.5.5 기능 차이 — 지원하지 않으면 어떻게 되는가
rmw API의 모든 기능을 모든 벤더가 구현하는 것은 아니다.
| 기능 | 미지원 시 동작 |
|---|---|
| loaned message | can_loan_messages() false, 일반 경로로 fallback(2.6.5) |
| 공유메모리 | 설정이 무시되고 UDP로 |
| QoS 이벤트 일부 | 콜백이 호출되지 않음(1.4.6) |
| 특정 QoS 정책 | 요청과 다르게 적용되거나 오류 |
| 보안 | 활성화 실패 |
| 그래프 정보 일부 | 빈 결과 또는 부분 정보 |
공통 패턴: 조용한 fallback 또는 조용한 무시. 이것이 이 책이 반복해서 "설정했다고 믿지 말고 측정하라"고 말하는 이유다(2.6.7·2.7.8).
방어적 확인 습관:
void log_middleware_capabilities(rclcpp::Node * node,
rclcpp::PublisherBase * pub) {
RCLCPP_INFO(node->get_logger(), "RMW: %s", rmw_get_implementation_identifier());
RCLCPP_INFO(node->get_logger(), "loaned message: %s",
pub->can_loan_messages() ? "지원" : "미지원");
// 실제 적용된 QoS 확인
const auto & q = pub->get_actual_qos();
RCLCPP_INFO(node->get_logger(), "실제 QoS depth=%zu reliability=%d",
q.depth(), static_cast<int>(q.reliability()));
}
견해:
get_actual_qos()를 초기화 로그에 남기는 것을 강하게 권한다. 요청한 QoS와 실제 적용된 QoS가 다를 수 있고(벤더 XML이 우선하는 경우 — 3.7), 그 사실을 나중에 알면 이미 시간을 낭비한 뒤다. RMW·QoS·loan 지원 여부를 시작 로그 한 줄로 남기면 현장 진단이 크게 쉬워진다(1.11.8).
3.5.6 교체 시 재검증 목록
2.1.9에서 "나중에 바꾸면 된다는 위험한 계획"이라고 했다. 구체적으로 무엇을 다시 봐야 하는가:
| 항목 | 왜 달라지나 | 확인 방법 |
|---|---|---|
| 지연·처리량 | 구현이 다르다 | 종단 지연 측정(2.7.8) |
| 디스커버리 시간·비용 | 참가자 매핑·SPDP 구현 차이 | 캡처(3.3.6) |
| 메모리 사용량 | 캐시·버퍼 정책 | RSS 추이(2.6.7) |
| QoS 실제 적용 | 매핑이 다를 수 있다 | topic info -v·get_actual_qos() |
| 대용량 메시지 | 조각화·흐름 제어 기본값(3.2.7) | 실제 전송 시험 |
| 무선·유실 환경 | 재전송 정책 | 부하·유실 환경 시험 |
| 튜닝 설정 | XML 전체가 무효 | 재작성 필요(3.7→3.8) |
| loaned·공유메모리 | 지원 여부·조건 | can_loan_messages() |
| 보안 설정 | 플러그인·설정 형식 | SROS2 재검증(13.3) |
| Discovery Server | Fast DDS 전용 | 대안 필요(3.4) |
| 대규모 안정성 | 스케일 거동이 다르다 | 장시간·대규모 시험 |
| 알려진 이슈 | 벤더별로 다름 | 이슈 트래커 확인 |
"XML 전체가 무효"가 가장 큰 비용이다. Fast DDS 프로파일과 Cyclone 설정은 형식도 개념 구성도 다르므로 번역이 아니라 재작성이다(3.7·3.8).
견해: RMW 교체는 "환경변수 하나"가 아니라 "성능·안정성 재검증 프로젝트"로 계획해야 한다. 교체 자체는 5초지만 신뢰를 회복하는 데는 며칠이 든다. 그래서 벤더 선택은 초기에, 근거를 갖고 하는 편이 낫다(3.6).
교체를 쉽게 만드는 설계 습관:
- 벤더 XML에 의존하는 설정을 최소화하고, 가능하면 ROS 2 표준 QoS로 표현한다.
- 벤더 전용 기능(Discovery Server·공유메모리)을 쓴다면 그 사실을 문서화하고 대안을 적어 둔다.
- 시작 로그에 RMW·실제 QoS를 남긴다(3.5.5).
- 성능 기준선(baseline)을 측정해 둔다 — 교체 후 비교 대상이 있어야 한다.
3.5.7 커스텀 rmw — 개관
새 미들웨어 백엔드를 만드는 것은 가능하며, 실제로 여러 구현이 존재한다(zenoh가 최근 예 — 3.10).
필요한 것:
| 항목 | 내용 |
|---|---|
| rmw 함수 전부 구현 | 미지원은 적절한 오류 반환 |
| typesupport 제공 | 자체 직렬화 또는 기존 재사용(1.3.3) |
| identifier 등록 | rmw_get_implementation_identifier() |
| 패키지 구성 | rmw_implementation이 찾을 수 있게 |
| ROS 규약 구현 | 이름 접두어·서비스 매핑·그래프 질의 |
현실적 난이도: 매우 높다. 함수 개수도 많지만, ROS 2가 기대하는 의미론(QoS 호환 규칙·그래프 일관성·서비스 상관)을 정확히 재현하는 것이 어렵다.
만들 만한 경우:
- 사내 전용 미들웨어가 이미 있고 ROS 2를 도입해야 할 때.
- 특수 전송(공유 버스·백플레인·위성 링크)이 필요할 때.
- 연구 목적.
대안을 먼저 검토하라: 대부분의 경우 기존 RMW + 브리지 노드가 훨씬 비용이 낮다. "우리 프로토콜을 쓰는 노드를 하나 만들고 ROS 토픽으로 변환"하는 구조는 며칠이면 되지만, rmw 구현은 수개월이다.
상세는 15.4에서 다룬다.
3.5.8 진단 — 무엇을 쓰고 있고 잘 동작하는가
# 1. 현재 구현
ros2 doctor --report | grep -iA2 middleware
echo $RMW_IMPLEMENTATION
# 2. 설치된 구현
ls /opt/ros/jazzy/lib/ | grep librmw_
# 3. 프로세스별 확인 (혼용 탐지)
for p in $(pgrep -f ros2); do
echo -n "$p: "; tr '\0' '\n' < /proc/$p/environ | grep RMW_IMPLEMENTATION || echo "(기본)"
done
# 4. 실제 QoS 적용 확인
ros2 topic info -v /scan
# 5. 벤더 ID (패킷 수준)
sudo tcpdump -i any -w v.pcap 'udp portrange 7400-7700'
# Wireshark 필터: rtps.vendorId
실행 확인(141차). 1·2·3번은 Humble·Jazzy ros-base 이미지에서 그대로 동작했다 — ros2 doctor --report는 middleware name : rmw_fastrtps_cpp를 정확히 보고했고(RMW_IMPLEMENTATION 미설정 상태의 기본값), ls .../lib/ | grep librmw_는 설치된 구현만 나열했다(기본 이미지엔 rmw_fastrtps_cpp 계열만 설치되어 있고 rmw_cyclonedds_cpp는 apt install 후에야 나타났다). 4·5번(get_actual_qos()·tcpdump 벤더 ID)은 이번 회차에 재현하지 않았다(확인 필요).
3번이 특히 유용하다. 여러 터미널·launch·컨테이너에서 노드를 실행하다 보면 환경이 일관되지 않은 경우가 생기고, 그것이 "일부 노드만 안 보인다"의 원인일 수 있다.
3.5.9 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | 구현 패키지 미설치 | RMW 로딩 실패 | apt install ros-jazzy-rmw-* |
| 2 | RMW 혼용 | 조용한 통신 실패·서비스 불가 | 전체 통일(3.5.4) |
| 3 | 환경변수 일부 노드만 적용 | 그래프 분열 | 프로세스별 확인 |
| 4 | 미지원 기능을 지원으로 가정 | 조용한 fallback | 능력 확인·로그 |
| 5 | 요청 QoS = 실제 QoS로 가정 | XML이 우선할 수 있음 | get_actual_qos() |
| 6 | 교체를 환경변수 문제로 취급 | 성능·안정성 회귀 | 재검증 프로젝트로 |
| 7 | XML 튜닝 후 벤더 교체 | 설정 전부 무효 | 재작성 예산 |
| 8 | Discovery Server 후 벤더 교체 | 디스커버리 구조 붕괴 | 대안 준비(3.4) |
| 9 | 커스텀 rmw를 성급히 착수 | 수개월 소모 | 브리지 노드 먼저 |
| 10 | 기준선 없이 교체 | 개선·악화 판단 불가 | 사전 측정 |
3.5.10 체크포인트
- rmw API의 분류 열 가지 이상을 들 수 있는가. "여기 없는 것은 ROS 2 표준으로 접근 불가"라는 명제의 예를 댈 수 있는가.
- rmw에는 있지만 DDS에는 없는 개념 세 가지와,
rmw_*_cpp가 그것을 어떻게 번역하는지 말할 수 있는가. RMW_IMPLEMENTATION교체가 재빌드 없이 되는 원리를 typesupport와 함께 설명할 수 있는가. 실패하는 경우는?- RMW 혼용이 공식 지원되지 않는데도 "가끔 되는" 이유는 무엇인가. 토픽·서비스·보안 각각의 위험도는?
- 혼용을 시험해 보고 쓰는 것이 왜 잘못된 결정인가. 올바른 대안은?
- 벤더가 기능을 지원하지 않을 때의 공통 패턴은 무엇인가. 그것이 이 책의 어떤 반복된 조언과 연결되는가.
- 시작 로그에 남기면 좋은 미들웨어 정보 세 가지는?
- RMW 교체 시 재검증해야 할 항목 열두 가지 중, 비용이 가장 큰 것은 무엇이며 왜인가.
- 교체를 쉽게 만드는 설계 습관 네 가지를 말할 수 있는가.
- 커스텀 rmw 대신 브리지 노드를 먼저 검토해야 하는 이유는?
다음 장 3.6은 이 계약 위에 선 실제 벤더들을 비교한다 — Fast DDS·Cyclone DDS·Connext·GurumDDS·zenoh를 어떤 축으로 평가하고, 어떻게 자신의 시스템에서 판단할 것인가.