ROS2와 Fast DDS 완벽 가이드 | RMW·DDS 미들웨어 아키텍처와 QoS 실전 튜닝
이 글의 핵심
ROS2 노드가 실제로 어떻게 통신하는지, 그 아래 깔린 RMW 추상화 계층과 기본 구현체인 Fast DDS를 자세히 다룹니다. QoS 정책 설계, XML 프로파일 설정, 멀티캐스트가 막힌 환경을 위한 Discovery Server 구성, 그리고 서브넷·Docker·클라우드 환경에서 자주 발생하는 디스커버리 실패를 진단하는 방법까지 실전 위주로 정리합니다.
들어가며
ROS2로 로봇을 다루다 보면 “ros2 topic pub으로 메시지를 보내면 구독자가 받는다”는 사실은 금방 익숙해지지만, 그 사이에 실제로 무슨 일이 일어나는지는 의외로 잘 다뤄지지 않습니다. 노드 두 개가 어떻게 서로를 찾아내는지, 메시지가 소켓 몇 개를 거쳐 전달되는지, 왜 어떤 토픽은 구독을 늦게 시작해도 과거 값을 받아오는데 어떤 토픽은 그렇지 않은지 — 이 모든 질문의 답은 ROS2 코드가 아니라 그 아래 깔린 미들웨어 레이어에 있습니다.
이 글은 창고 자동화나 산업용 로봇 프로젝트에서 여러 대의 AMR(자율 이동 로봇)을 동시에 운영하다가, 어느 순간 노드 간 통신이 끊기거나 QoS 불일치로 데이터가 전혀 오가지 않는 문제를 마주친 엔지니어를 염두에 두고 썼습니다. AMR 자체의 SLAM이나 경로 계획 같은 상위 개념은 다루지 않습니다. 대신 ROS2 노드들이 실제로 어떻게 통신하는가, 그리고 ROS2의 기본 통신 구현체인 Fast DDS가 내부적으로 어떤 방식으로 동작하는지에만 집중합니다. AMR 소프트웨어 파이프라인 전반에 대한 개념은 AMR 완벽 가이드에서 이미 다루었으므로, 이 글에서는 그 6장에서 짧게 언급된 “노드가 토픽으로 통신한다”는 문장 뒤에 숨어 있는 실제 메커니즘을 풀어냅니다.
1. ROS2와 RMW — 왜 통신 계층을 추상화했는가
1.1 ROS1의 한계와 ROS2의 설계 선택
ROS1은 roscore라는 단일 마스터 노드가 모든 노드의 등록과 토픽 검색을 중개하는 구조였습니다. 이 구조는 이해하기 쉽지만, 마스터 노드가 죽으면 전체 통신이 멈추는 단일 장애점(SPOF)이 되고, 실시간성이나 보안(암호화, 인증) 요구를 만족시키기 어려웠습니다. 산업 현장에 로봇을 투입하려는 요구가 늘어나면서, ROS 커뮤니티는 이미 항공·국방·금융 분야에서 검증된 산업 표준 통신 프로토콜인 DDS(Data Distribution Service) 위에 ROS2를 새로 설계하기로 결정했습니다.
여기서 중요한 설계 선택이 하나 있습니다. ROS2 코어 개발팀은 DDS 표준 자체는 채택했지만, 특정 DDS 벤더 구현체에 종속되지 않도록 중간에 추상화 계층을 하나 끼워 넣었습니다. 이것이 바로 RMW(ROS Middleware Interface) 입니다.
1.2 RMW: DDS 구현체를 갈아끼울 수 있게 만드는 인터페이스
RMW는 rclcpp/rclpy 같은 상위 클라이언트 라이브러리와, Fast DDS·Cyclone DDS·RTI Connext 같은 실제 통신 구현체 사이에 위치하는 얇은 C 인터페이스 계층입니다. 노드를 만들고, 토픽을 발행/구독하고, 서비스를 호출하는 등의 동작을 표준화된 함수 시그니처로 정의해두고, 각 DDS 벤더는 이 시그니처에 맞춰 자신들의 구현체를 RMW 패키지 형태로 제공합니다.
[사용자 코드: rclpy / rclcpp]
↓ (표준 ROS2 API)
[RMW 인터페이스]
↓ (구현체별 어댑터)
rmw_fastrtps_cpp | rmw_cyclonedds_cpp | rmw_connextdds
↓
Fast DDS | Cyclone DDS | RTI Connext DDS
↓
[실제 네트워크: UDP/SHM]
이 구조 덕분에 애플리케이션 코드는 단 한 줄도 바꾸지 않고, 실행 시점에 환경변수 하나로 통신 구현체를 교체할 수 있습니다. 왜 이런 설계가 필요했을까요. DDS 표준은 API 수준까지 완전히 통일되어 있지 않고, 벤더마다 성능 특성·라이선스·지원 플랫폼(임베디드, 실시간 커널 등)이 다릅니다. 특정 벤더에 종속되면 라이선스 정책 변경이나 유지보수 중단 같은 리스크를 ROS2 생태계 전체가 떠안게 됩니다. RMW 추상화는 이런 리스크를 분산시키고, 사용자가 프로젝트 요구사항(임베디드 최적화, 보안 인증, 실시간성 등)에 맞는 구현체를 선택할 수 있게 해줍니다.
1.3 왜 rmw_fastrtps_cpp가 기본값인가
rmw_fastrtps_cpp는 eProsima의 Fast DDS를 감싸는 RMW 어댑터로, ROS2가 처음 출범할 때부터 기본 RMW로 채택되어 왔습니다(Humble, Jazzy 등 현재 주요 LTS 배포판 모두 동일). 이렇게 된 배경에는 몇 가지 실질적인 이유가 있습니다. eProsima가 OMG(Object Management Group)의 DDS 표준화 작업에 적극 참여해온 만큼 표준 준수도가 높고, Apache 2.0 라이선스로 상용·오픈소스 프로젝트 모두에서 제약 없이 쓸 수 있으며, ROS2 코어 팀과의 협업을 통해 초기부터 ROS2 전용 최적화(타입 지원 코드 생성, XML 프로파일 연동 등)가 이루어졌습니다. 그 결과 가장 많은 튜토리얼과 트러블슈팅 자료가 Fast DDS를 기준으로 작성되어 있다는 것도 실무적으로 중요한 이유입니다.
2. DDS란 무엇인가 — 브로커 없는 pub/sub 아키텍처
2.1 DDS의 핵심: 중앙 브로커가 없다
Kafka나 RabbitMQ 같은 메시지 브로커에 익숙한 개발자라면, DDS를 처음 접했을 때 가장 낯선 점이 중앙 브로커가 존재하지 않는다는 사실입니다. DDS는 Data-Centric Publish-Subscribe(DCPS) 모델을 따르는데, 발행자(Publisher)와 구독자(Subscriber)가 별도의 중개 서버를 거치지 않고 P2P로 직접 데이터를 주고받습니다. 대신 그 자리를 “이 시스템에 어떤 참여자들이 있고, 각자 무엇을 발행/구독하는가”를 서로 자동으로 알아내는 디스커버리(Discovery) 프로토콜이 대신합니다.
브로커가 없다는 설계는 로봇 시스템에 특히 잘 맞습니다. 중앙 브로커가 있으면 그 자체가 단일 장애점이자 성능 병목이 되는데, 로봇 제어처럼 지연시간에 민감한 시스템에서는 이 중개 홉 하나가 치명적일 수 있습니다. DDS는 발행자가 데이터를 만들면 구독자에게 최단 경로로 직접 전달하므로, 브로커를 거치는 구조보다 지연시간이 낮고 브로커 장애로 전체 통신이 마비되는 상황도 없습니다.
2.2 Domain Participant와 Domain ID
DDS 네트워크에 참여하는 각 프로세스는 하나의 Domain Participant로 표현됩니다. ROS2 노드 하나가 대략 하나의 Domain Participant에 대응한다고 생각하면 됩니다. 여러 Domain Participant가 서로 통신하려면 같은 Domain ID에 속해 있어야 하며, ROS2에서는 이 값을 ROS_DOMAIN_ID 환경변수로 지정합니다.
# 이 셸에서 실행하는 모든 ROS2 노드는 Domain ID 42에 속합니다
export ROS_DOMAIN_ID=42
ros2 run my_robot_pkg lidar_driver_node
Domain ID는 논리적으로 완전히 독립된 통신 영역을 만드는 역할을 합니다. 같은 물리 네트워크에 여러 팀, 여러 로봇, 여러 시뮬레이션 인스턴스가 동시에 떠 있는 상황(예: 여러 엔지니어가 각자 랩탑에서 같은 Wi-Fi로 시뮬레이션을 돌리는 사무실)에서, 모두가 기본값(0)을 그대로 쓰면 서로의 토픽이 뒤섞이거나 디스커버리 트래픽이 불필요하게 늘어납니다. 실무에서는 로봇 개체별, 혹은 팀/환경별로 Domain ID 할당 규칙을 미리 정해두는 것이 좋습니다. 유효 범위는 플랫폼과 설정에 따라 다르지만 통상 0~232 범위 내에서 사용하는 것이 권장됩니다.
2.3 Discovery: SPDP와 SEDP
브로커가 없는 대신, DDS 참여자들은 두 단계의 디스커버리 프로토콜로 서로를 찾아냅니다.
SPDP(Simple Participant Discovery Protocol)는 “이 도메인에 어떤 Domain Participant들이 있는가”를 알아내는 1단계입니다. 각 참여자는 주기적으로 자신의 존재를 멀티캐스트로 광고(announce)하고, 다른 참여자들의 광고를 듣습니다. 기본 설정에서는 UDP 멀티캐스트를 사용하기 때문에, 네트워크가 멀티캐스트를 지원하지 않으면 이 단계부터 실패합니다.
SEDP(Simple Endpoint Discovery Protocol)는 SPDP로 서로의 존재를 확인한 참여자들이, 이번에는 “너는 어떤 토픽을 발행/구독하고 있고, 그 QoS는 무엇인가”를 유니캐스트로 교환하는 2단계입니다. 이 단계에서 양쪽의 토픽 이름·타입·QoS가 서로 호환되는지 확인하고, 호환되면 비로소 실제 데이터 채널(엔드포인트 매칭)이 열립니다.
정리하면, SPDP는 “누가 있는가”를, SEDP는 “그들이 무엇을 주고받을 준비가 되어 있는가”를 알아내는 단계입니다. 뒤에서 다룰 Discovery Server는 바로 이 SPDP 단계를 멀티캐스트 대신 중앙 서버 경유 유니캐스트로 대체하는 메커니즘입니다.
3. Fast DDS 소개
Fast DDS(과거 명칭 Fast RTPS)는 스페인의 eProsima가 개발한 오픈소스 DDS 구현체로, OMG의 DDS 표준과 그 하위 와이어 프로토콜인 RTPS(Real-Time Publish-Subscribe) 표준을 모두 준수합니다. C++로 작성되었고 Apache 2.0 라이선스로 배포되며, ROS2 외에도 산업 자동화, IoT 게이트웨이 등 다양한 분야에서 독립적으로 쓰이고 있습니다.
Fast DDS가 ROS2 생태계에서 갖는 실무적 장점은 세 가지로 요약할 수 있습니다. 첫째, XML 기반 프로파일 설정을 지원해서 코드를 재컴파일하지 않고도 transport, QoS, Discovery 방식을 외부 설정 파일로 바꿀 수 있습니다. 둘째, Discovery Server 모드를 자체 제공해서 멀티캐스트가 제한된 환경에서도 안정적인 디스커버리를 구성할 수 있습니다. 셋째, 공유 메모리(SHM) transport를 지원해서 같은 호스트 안의 노드 간 통신에서는 실제 네트워크 스택을 거치지 않고 메모리 복사만으로 데이터를 주고받아 지연시간을 크게 줄입니다. 이 세 가지는 각각 5장과 6장에서 구체적인 설정 예제와 함께 다시 다룹니다.
4. QoS 정책 심화
4.1 QoS가 왜 중요한가 — “같은 토픽도 다르게 취급할 수 있다”
전통적인 메시지 큐는 대부분 “메시지는 무조건 안전하게 보관하고 전달한다”는 단일한 보장 수준을 제공합니다. DDS는 다릅니다. 토픽마다, 심지어 같은 토픽에 대한 각 발행자/구독자 쌍마다 신뢰성, 이력 보관 정책, 데이터 유효기간 등을 세밀하게 다르게 지정할 수 있습니다. 이것이 QoS(Quality of Service) 정책이며, ROS2는 이 DDS QoS를 rclpy/rclcpp에서 그대로 노출합니다.
QoS를 제대로 이해하지 않고 기본값만 쓰다가 겪는 가장 흔한 문제가 “코드는 멀쩡한데 데이터가 전혀 안 온다”는 증상입니다. 이는 대부분 발행자와 구독자의 QoS가 호환되지 않아 애초에 연결(매칭)이 성립하지 않은 경우입니다. DDS는 이런 상황에서 에러를 던지지 않고 조용히 매칭을 거부하기 때문에, QoS 정책을 정확히 이해하는 것이 트러블슈팅의 핵심입니다.
4.2 Reliability: RELIABLE vs BEST_EFFORT
Reliability는 메시지 전달을 보장할 것인가를 결정합니다.
- RELIABLE: 유실된 메시지를 재전송해서라도 모든 메시지가 구독자에게 도달하도록 보장합니다. 대신 재전송 확인(ACK/NACK) 오버헤드가 있어 지연시간과 대역폭 비용이 늘어납니다.
- BEST_EFFORT: 재전송을 시도하지 않습니다. 네트워크 상황이 나쁘면 메시지가 유실될 수 있지만, 오버헤드가 가장 적고 최신 데이터 전달이 지연되지 않습니다.
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
# LiDAR처럼 초당 수십 회 갱신되는 센서 스트림
sensor_qos = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=5,
)
self.create_subscription(LaserScan, '/scan', self.scan_callback, sensor_qos)
센서 데이터에 BEST_EFFORT를 쓰는 이유는 단순합니다. 한두 프레임이 유실되어도 다음 프레임이 금방 다시 오기 때문에 굳이 재전송을 기다릴 필요가 없고, 오히려 재전송을 기다리다가 최신 데이터 처리가 밀리는 쪽이 더 해롭습니다. 반면 로봇의 목표 위치 명령이나 설정값처럼 한 번이라도 유실되면 안 되고 빈도도 낮은 데이터는 RELIABLE을 씁니다.
4.3 Durability: VOLATILE vs TRANSIENT_LOCAL
Durability는 구독자가 늦게 접속했을 때 과거 데이터를 받을 수 있는가를 결정합니다.
- VOLATILE(기본값): 구독을 시작하기 이전에 발행된 메시지는 받지 못합니다. 구독이 시작된 이후의 메시지만 전달됩니다.
- TRANSIENT_LOCAL: 발행자가 마지막으로 보낸 메시지(들)를 로컬에 캐시해두고, 새로 구독을 시작한 노드에게도 그 캐시된 값을 전달합니다.
from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy, HistoryPolicy
# 로봇 설정값·상태처럼 "늦게 구독해도 현재 값을 알아야 하는" 토픽
state_qos = QoSProfile(
reliability=ReliabilityPolicy.RELIABLE,
durability=DurabilityPolicy.TRANSIENT_LOCAL,
history=HistoryPolicy.KEEP_LAST,
depth=1,
)
self.publisher_ = self.create_publisher(RobotState, '/robot_state', state_qos)
맵 데이터나 로봇 파라미터, 현재 배터리 상태처럼 “노드가 나중에 뜨더라도 최신 값을 곧바로 알아야 하는” 토픽에는 TRANSIENT_LOCAL이 적합합니다. 예를 들어 SLAM 노드가 만든 지도(/map)는 대개 TRANSIENT_LOCAL로 발행되는데, 시각화 도구(RViz)나 내비게이션 노드가 나중에 켜져도 맵 데이터를 처음부터 다시 스캔하지 않고 곧바로 받아볼 수 있어야 하기 때문입니다. 주의할 점은, 발행자와 구독자의 Durability가 호환되지 않으면 연결 자체가 맺어지지 않는다는 것입니다. 구독자가 VOLATILE인데 발행자가 TRANSIENT_LOCAL을 요구하는 조합은 괜찮지만(구독자가 더 낮은 요구 수준), 반대로 구독자가 TRANSIENT_LOCAL을 요구하는데 발행자가 VOLATILE이면 매칭되지 않습니다.
4.4 History: KEEP_LAST vs KEEP_ALL
History는 발행자/구독자가 내부적으로 몇 개의 메시지를 큐에 보관할지를 결정합니다.
- KEEP_LAST(depth=N): 최근 N개의 메시지만 보관하고, 그 이상은 오래된 것부터 버립니다. 대부분의 토픽에서 기본으로 쓰입니다.
- KEEP_ALL: 리소스(메모리) 한계까지 모든 메시지를 보관합니다. 한 건도 놓치면 안 되는 로그성 데이터 등에 쓰이지만, 큐가 무한정 쌓일 위험이 있어 신중하게 사용해야 합니다.
depth 값은 생각보다 중요합니다. depth가 너무 작으면(예: 1) 구독 콜백이 처리 속도를 따라가지 못할 때 최신 메시지가 오기도 전에 이전 메시지가 버려질 수 있고, 반대로 depth가 너무 크면 콜백이 밀렸을 때 오래된(이미 의미 없는) 메시지까지 계속 처리하느라 지연이 누적되는 역효과가 날 수 있습니다.
4.5 Deadline과 Liveliness
Deadline은 “이 토픽은 최소 몇 초에 한 번은 갱신되어야 한다”는 기대 주기를 선언하는 정책입니다. 발행 주기가 이 기대를 넘기면 콜백으로 알림을 받을 수 있어, 센서가 조용히 멈춰버리는 상황(예: LiDAR 드라이버가 죽었는데 노드 프로세스 자체는 살아있는 경우)을 감지하는 데 유용합니다.
Liveliness는 “이 노드/발행자가 여전히 살아있는가”를 판단하는 정책입니다. AUTOMATIC(기본, 미들웨어가 자동으로 생존을 판단), MANUAL_BY_TOPIC(애플리케이션이 명시적으로 생존 신호를 보내야 함) 등의 옵션이 있으며, 안전이 중요한 시스템에서 “노드는 떠 있지만 실제로는 멈춰 있는” 상황을 조기에 감지하는 데 씁니다.
from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy, HistoryPolicy
from rclpy.duration import Duration
# 센서 생존 확인이 중요한 안전 관련 토픽 예시
safety_qos = QoSProfile(
reliability=ReliabilityPolicy.RELIABLE,
durability=DurabilityPolicy.VOLATILE,
history=HistoryPolicy.KEEP_LAST,
depth=1,
deadline=Duration(seconds=0, nanoseconds=200_000_000), # 200ms 이내 갱신 기대
)
Deadline과 Liveliness는 실무에서 QoS 정책 중 가장 늦게, 그리고 가장 드물게 튜닝되는 항목입니다. 대부분의 프로젝트는 Reliability·Durability·History 세 가지만으로 충분하지만, 안전 인증이 요구되는 시스템이나 센서 장애를 빠르게 감지해야 하는 프로덕션 환경에서는 이 두 정책이 실질적인 가치를 발휘합니다.
4.6 QoS 프로파일 선택 가이드
| 데이터 성격 | Reliability | Durability | History | 예시 |
|---|---|---|---|---|
| 고빈도 센서 스트림 | BEST_EFFORT | VOLATILE | KEEP_LAST(depth 낮게) | /scan, /image_raw, /imu |
| 로봇 상태/설정값 | RELIABLE | TRANSIENT_LOCAL | KEEP_LAST(depth=1) | /robot_state, /map |
| 제어 명령 | RELIABLE | VOLATILE | KEEP_LAST(depth 낮게) | /cmd_vel |
| 서비스/액션 요청 | RELIABLE | VOLATILE | KEEP_ALL 또는 낮은 depth | 작업 지시, 트리거 |
이 표는 절대적인 규칙이 아니라 출발점입니다. 실제 값은 네트워크 대역폭, 콜백 처리 속도, 유실 허용 범위를 함께 고려해서 조정해야 합니다. ROS2에는 rmw_qos_profile_sensor_data, rmw_qos_profile_default 같은 사전 정의된 프로파일도 있어, 대부분의 경우 이를 기반으로 필요한 부분만 오버라이드하는 것이 실용적입니다.
5. Fast DDS XML 프로파일 설정
5.1 FASTRTPS_DEFAULT_PROFILES_FILE
Fast DDS는 코드를 재컴파일하지 않고도 transport, discovery, QoS 기본값을 외부 XML 파일로 바꿀 수 있는 프로파일 시스템을 제공합니다. 환경변수 FASTRTPS_DEFAULT_PROFILES_FILE에 XML 파일 경로를 지정하면, 노드가 시작될 때 이 파일을 읽어 기본 설정으로 적용합니다.
export FASTRTPS_DEFAULT_PROFILES_FILE=/home/robot/config/fastdds_profiles.xml
ros2 run my_robot_pkg lidar_driver_node
이 방식의 가장 큰 이점은 운영 환경별로 설정을 분리할 수 있다는 점입니다. 개발 중에는 기본 UDP 멀티캐스트 transport를 쓰다가, 실제 로봇에 배포할 때는 같은 코드를 재빌드하지 않고 XML 파일만 바꿔서 SHM transport나 Discovery Server 연결을 활성화하는 식의 운영이 가능합니다.
5.2 Transport 설정 예제 — UDP와 SHM
다음은 UDP transport의 버퍼 크기를 조정하고, 같은 호스트 내부 통신에는 공유 메모리(SHM)를 우선 사용하도록 지정하는 XML 프로파일 예제입니다.
<?xml version="1.0" encoding="UTF-8"?>
<dds xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
<transport_descriptors>
<transport_descriptor>
<transport_id>udp_transport</transport_id>
<type>UDPv4</type>
<sendBufferSize>1048576</sendBufferSize>
<receiveBufferSize>4194304</receiveBufferSize>
</transport_descriptor>
<transport_descriptor>
<transport_id>shm_transport</transport_id>
<type>SHM</type>
</transport_descriptor>
</transport_descriptors>
<participant profile_name="robot_participant_profile" is_default_profile="true">
<rtps>
<userTransports>
<transport_id>shm_transport</transport_id>
<transport_id>udp_transport</transport_id>
</userTransports>
<useBuiltinTransports>false</useBuiltinTransports>
</rtps>
</participant>
</dds>
이 설정의 핵심은 userTransports에 shm_transport를 udp_transport보다 먼저 나열했다는 점입니다. Fast DDS는 같은 호스트에서 실행되는 참여자끼리는 SHM transport로 자동 전환하는 것을 우선 시도하고, 호스트 밖의 참여자와는 UDP로 통신합니다. LiDAR 드라이버 노드와 SLAM 노드가 같은 컴퓨터에서 대용량 포인트 클라우드를 주고받는 상황이라면, SHM transport 덕분에 커널 네트워크 스택을 거치지 않고 공유 메모리 복사만으로 데이터가 전달되어 지연시간과 CPU 사용량이 눈에 띄게 줄어듭니다. useBuiltinTransports를 false로 두어 기본 transport를 끄고, 명시한 transport만 쓰도록 강제한 것도 눈여겨볼 부분입니다 — 이렇게 하지 않으면 기본 UDP transport와 커스텀 설정이 함께 활성화되어 의도와 다르게 동작할 수 있습니다.
5.3 QoS 기본값을 XML로 지정하기
애플리케이션 코드를 건드리지 않고 특정 토픽의 QoS 기본값을 바꾸고 싶을 때도 XML 프로파일을 활용할 수 있습니다.
<profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
<publisher profile_name="reliable_state_publisher">
<qos>
<reliability>
<kind>RELIABLE_RELIABILITY_QOS</kind>
</reliability>
<durability>
<kind>TRANSIENT_LOCAL_DURABILITY_QOS</kind>
</durability>
</qos>
</publisher>
</profiles>
다만 실무에서는 QoS를 XML로 지정하기보다, 4장에서 다룬 것처럼 rclpy/rclcpp 코드에서 QoSProfile로 명시하는 방식을 더 권장합니다. QoS는 애플리케이션 로직과 밀접하게 연결된 설정이라 코드 리뷰에서 함께 보이는 것이 유지보수에 유리하고, XML 프로파일은 주로 transport나 Discovery처럼 배포 환경에 따라 달라지는 인프라 설정에 활용하는 것이 역할 분담이 명확합니다.
6. Discovery Server 모드
6.1 멀티캐스트 기반 디스커버리가 무너지는 지점
2장에서 살펴본 기본 SPDP 디스커버리는 UDP 멀티캐스트로 동작합니다. 이 방식은 노드 몇 개짜리 단일 로봇, 단일 서브넷 환경에서는 아무 문제 없이 동작하지만, 다음과 같은 상황에서는 깨지기 시작합니다.
- 대규모 창고 Wi-Fi: 수십 대의 AMR이 같은 AP를 공유하면, 각 로봇의 여러 노드가 주기적으로 뿌리는 멀티캐스트 디스커버리 패킷이 서로 누적되어 무선 대역폭을 상당 부분 잠식합니다. AP가 멀티캐스트를 유니캐스트로 변환해 브로드캐스트하는 방식(Multicast-to-Unicast)을 쓰는 경우 이 부담은 더 커집니다.
- 네트워크 장비의 멀티캐스트 차단: 많은 기업/클라우드 네트워크 장비가 기본적으로 멀티캐스트 트래픽을 제한하거나 차단합니다. 특히 클라우드 VPC나 Kubernetes 오버레이 네트워크(CNI)는 대부분 멀티캐스트를 지원하지 않습니다.
- 서브넷 경계: 멀티캐스트는 기본적으로 같은 브로드캐스트 도메인(서브넷) 안에서만 전달되므로, 서로 다른 서브넷에 있는 노드는 라우터가 멀티캐스트 라우팅을 별도로 설정하지 않는 한 서로를 찾지 못합니다.
6.2 Discovery Server: 유니캐스트 기반 중앙 디스커버리
Fast DDS는 이 문제를 풀기 위해 Discovery Server 모드를 제공합니다. 멀티캐스트로 서로를 찾는 대신, 모든 참여자가 잘 알려진 하나(또는 여러 개)의 서버 주소로 유니캐스트 연결을 맺고, 그 서버가 “누가 무엇을 발행/구독하는지”에 대한 정보를 중계합니다. 이름은 “서버”지만, 실제 데이터(토픽 메시지)는 여전히 참여자 간 P2P로 오가고, Discovery Server는 오직 디스커버리 정보 교환만 중개한다는 점이 메시지 브로커와의 차이입니다.
Discovery Server를 띄우는 방법은 fastdds CLI 도구를 사용합니다.
# Discovery Server를 ID 0, UDP 포트 11811로 실행
fastdds discovery --server-id 0 --port 11811
이 서버를 사용할 각 노드는 ROS_DISCOVERY_SERVER 환경변수로 서버 주소를 지정합니다.
# 서버가 192.168.1.10:11811에서 실행 중이라고 가정
export ROS_DISCOVERY_SERVER=192.168.1.10:11811
ros2 run my_robot_pkg navigation_node
이렇게 설정된 노드는 SPDP 단계에서 멀티캐스트를 전혀 사용하지 않고, 곧바로 지정된 서버로 유니캐스트 연결을 맺어 디스커버리 정보를 주고받습니다. 대규모 창고 환경이라면 이 서버를 안정적인 유선 네트워크의 고정 호스트(예: 플릿 관리 서버와 같은 머신)에 두고, 모든 AMR이 이 서버로 접속하도록 구성하는 것이 일반적인 패턴입니다. 고가용성이 필요하면 여러 개의 Discovery Server를 백업 구성으로 띄울 수도 있습니다.
6.3 Discovery Server 사용 시 주의할 점
Discovery Server는 어디까지나 디스커버리 단계만 중앙화할 뿐, 실제 데이터 트래픽까지 서버를 거치게 만드는 것은 아닙니다. 따라서 이 서버가 다운되어도 이미 매칭이 끝난 노드 간의 통신은 계속 유지되며, 새로 시작되는 노드의 디스커버리만 영향을 받습니다. 다만 로봇이 재시작되거나 새 노드가 추가되는 시점에는 서버가 필수적이므로, 운영 환경에서는 이 서버 자체의 가용성(재시작 정책, 모니터링)을 신경 써야 합니다.
또한 Discovery Server 모드와 기본 Simple Discovery는 서로 다른 프로토콜 경로를 쓰기 때문에, 한쪽 방식으로 설정된 노드와 다른 쪽 방식으로 설정된 노드는 기본적으로 서로를 찾지 못합니다. 같은 시스템에 속한 노드는 모두 동일한 디스커버리 방식을 쓰도록 배포 스크립트나 컨테이너 이미지 수준에서 일관되게 관리하는 것이 중요합니다.
7. 다른 RMW 구현체와의 비교
7.1 Cyclone DDS
Cyclone DDS는 Eclipse 재단이 관리하는 오픈소스 DDS 구현체로, ROS2에서 rmw_cyclonedds_cpp 패키지를 통해 지원됩니다. Fast DDS 대비 코드베이스가 더 단순하고 가볍다는 평가를 받으며, 일부 벤치마크에서는 노드 수가 많아질 때 CPU·메모리 사용량이 더 낮게 나타나기도 합니다. 디스커버리 관련 이슈(특히 Fast DDS의 특정 버전에서 보고된 디스커버리 지연·불안정 문제)를 겪었을 때, 실무에서 가장 먼저 시도해보는 대안이 Cyclone DDS인 경우가 많습니다.
7.2 RTI Connext DDS
RTI Connext는 DDS 구현체 중 가장 오래되고 항공·국방 분야에서 폭넓게 검증된 상용 제품입니다. ROS2에서는 rmw_connextdds로 지원되며, 엄격한 실시간성 요구, 벤더 기술 지원, 고급 보안 기능(DDS-Security 인증서 기반 세밀한 접근 제어 등)이 필요한 산업/국방 프로젝트에서 채택되는 경우가 많습니다. 다만 상용 라이선스가 필요하다는 점이 오픈소스 프로젝트나 스타트업에서는 진입 장벽으로 작용합니다.
7.3 RMW_IMPLEMENTATION으로 전환하기
RMW 구현체를 바꾸는 방법은 간단합니다. 대상 RMW 패키지를 설치한 뒤, 환경변수로 지정하면 됩니다.
# Cyclone DDS RMW 패키지 설치 (예: Ubuntu + apt)
sudo apt install ros-humble-rmw-cyclonedds-cpp
# 이 셸에서 실행되는 노드는 Cyclone DDS를 통신 구현체로 사용
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ros2 run my_robot_pkg navigation_node
이때 반드시 기억해야 할 원칙은 같은 통신 그룹에 속한 모든 노드가 동일한 RMW 구현체를 써야 한다는 것입니다. 서로 다른 DDS 벤더 구현체는 RTPS 와이어 프로토콜 수준에서는 표준을 따르지만, 벤더별 확장 기능이나 QoS 세부 처리 방식의 차이로 상호 운용이 완전히 보장되지 않는 경우가 실무에서 종종 보고됩니다. 로봇 한 대 안에서 일부 노드는 Fast DDS, 일부는 Cyclone DDS로 실행하는 식의 혼합 구성은 권장하지 않습니다.
8. 실전 트러블슈팅
8.1 서로 다른 서브넷/Docker 컨테이너 간 디스커버리 실패
가장 흔하게 보고되는 문제는 “로컬에서는 잘 되는데 Docker 컨테이너로 띄우면 노드가 서로를 못 찾는다”는 증상입니다. 원인은 대개 다음 중 하나입니다.
- 컨테이너가 기본 브리지 네트워크(bridge)를 쓰면 각 컨테이너가 별도의 네트워크 네임스페이스에 격리되어, 멀티캐스트 패킷이 컨테이너 경계를 넘지 못합니다.
- 호스트 네트워크 모드(
--network host)를 쓰지 않으면 컨테이너의 IP가 호스트와 달라, SEDP 단계에서 교환하는 유니캐스트 locator 정보가 실제로는 도달 불가능한 주소를 가리키게 됩니다.
가장 간단한 해결책은 컨테이너를 --network host로 실행해서 컨테이너가 호스트와 동일한 네트워크 네임스페이스를 공유하도록 하는 것입니다.
docker run --network host --env ROS_DOMAIN_ID=42 my_robot_image
--network host를 쓸 수 없는 환경(쿠버네티스, 격리가 필수인 멀티 테넌트 환경 등)이라면, 6장에서 다룬 Discovery Server를 도입해 멀티캐스트 의존성 자체를 없애는 것이 더 근본적인 해결책입니다. 컨테이너 간에는 Discovery Server 주소로의 유니캐스트 접근만 가능하면 되므로, 컨테이너 네트워크 정책을 크게 바꾸지 않고도 디스커버리 문제를 해결할 수 있습니다.
8.2 클라우드/Kubernetes 환경에서의 대안
시뮬레이션을 클라우드에서 돌리거나, 로봇 플릿의 일부 노드(예: 플릿 관리, 원격 모니터링)를 Kubernetes 클러스터에 올리는 구성이 늘어나면서, 멀티캐스트가 아예 막혀 있는 네트워크에서 ROS2를 운영해야 하는 경우가 흔해졌습니다. 이런 환경의 표준적인 대응은 다음과 같습니다.
- Discovery Server를 클러스터 내 안정적인 서비스로 배포하고, 각 파드에
ROS_DISCOVERY_SERVER환경변수로 그 서비스의 ClusterIP나 안정적인 DNS 이름을 지정합니다. - 파드 간 통신이 필요한 포트(Discovery Server 포트 및 실제 데이터 전송에 쓰이는 UDP 포트 범위)를 NetworkPolicy에서 명시적으로 허용합니다.
- 클러스터 밖의 실제 로봇과 통신해야 한다면, VPN이나 전용 게이트웨이를 통해 유니캐스트 경로를 확보해야 합니다 — 멀티캐스트가 클러스터 경계를 넘는 구성은 일반적으로 권장되지 않습니다.
8.3 ROS_DOMAIN_ID 충돌
같은 네트워크에 여러 시스템이 공존할 때, ROS_DOMAIN_ID를 설정하지 않아 모두 기본값(0)을 쓰는 것이 의외로 흔한 실수입니다. 증상은 “내가 만든 적 없는 토픽이 ros2 topic list에 보인다”거나, “다른 팀이 재시작한 노드 때문에 내 시스템의 디스커버리 트래픽이 순간적으로 튄다”는 형태로 나타납니다. 진단은 간단합니다.
echo $ROS_DOMAIN_ID
ros2 topic list # 예상보다 훨씬 많은 토픽이 보이면 Domain ID 충돌을 의심
해결책은 팀/프로젝트/로봇 단위로 Domain ID 할당표를 만들어 두고, 셸 프로파일이나 컨테이너 이미지의 ENV ROS_DOMAIN_ID=...에 명시적으로 고정하는 것입니다.
8.4 ros2 doctor, ros2 daemon status로 환경 점검하기
QoS나 네트워크 설정을 하나하나 의심하기 전에, ROS2가 기본으로 제공하는 진단 도구로 먼저 환경 자체에 문제가 없는지 확인하는 것이 효율적입니다.
# 네트워크 설정, 플랫폼, 패키지 버전 등 전반적인 환경 문제를 점검
ros2 doctor --report
# ROS2 데몬(백그라운드에서 토픽/노드 캐시를 관리) 상태 확인
ros2 daemon status
# 데몬이 오래된 캐시를 들고 있는 것으로 의심되면 재시작
ros2 daemon stop
ros2 daemon start
ros2 doctor는 네트워크 인터페이스 설정, ROS_DOMAIN_ID 값, 설치된 RMW 구현체 등을 종합적으로 점검해서 잠재적인 문제를 경고해줍니다. 특히 여러 네트워크 인터페이스(유선 + Wi-Fi, 혹은 Docker의 가상 인터페이스)가 동시에 떠 있는 개발 머신에서, 의도치 않은 인터페이스로 디스커버리 트래픽이 나가는 문제를 잡아내는 데 유용합니다.
8.5 Wireshark로 DDS 트래픽 직접 들여다보기
CLI 도구로도 원인이 명확하지 않을 때는, 실제 네트워크 패킷을 직접 확인하는 것이 가장 확실합니다. DDS(RTPS)는 기본적으로 UDP 위에서 동작하며, Wireshark는 RTPS 프로토콜 디코더를 기본 내장하고 있습니다.
# 멀티캐스트 디스커버리 트래픽 확인 (기본 포트 대역: 7400번대)
sudo tcpdump -i any udp portrange 7400-7500 -w rtps_capture.pcap
캡처한 .pcap 파일을 Wireshark로 열고 rtps 필터를 적용하면, 어떤 참여자가 SPDP 광고를 보내고 있는지, SEDP 단계에서 QoS 정보가 실제로 어떻게 교환되는지를 패킷 단위로 확인할 수 있습니다. 특히 “발행자는 데이터를 보내고 있다고 로그를 찍는데 구독자에는 아무것도 안 온다”는 상황에서, 패킷 자체가 아예 나가지 않는지(방화벽/네트워크 문제) 아니면 패킷은 나가는데 구독자가 무시하는지(QoS 불일치)를 구분하는 데 결정적인 단서를 제공합니다.
마치며
ROS2의 통신은 rclpy나 rclcpp의 API 뒤에 RMW라는 추상화 계층이 있고, 그 아래에서 실제로 데이터를 주고받는 것은 Fast DDS 같은 DDS 구현체라는 계층 구조로 이루어져 있습니다. 이 계층 구조를 이해하면, “토픽 발행/구독”이라는 단순한 개념 뒤에서 Domain Participant, SPDP/SEDP 디스커버리, QoS 매칭이라는 구체적인 메커니즘이 동작하고 있음을 알 수 있고, 통신 문제가 생겼을 때 어느 계층을 먼저 의심해야 할지 판단하는 기준이 생깁니다.
실무에서는 QoS 불일치로 인한 “조용한 연결 실패”와, 네트워크 환경(서브넷, Docker, 클라우드)에서의 디스커버리 실패가 가장 흔하게 마주치는 두 가지 문제입니다. 전자는 ros2 topic info --verbose로, 후자는 Discovery Server 도입이나 ros2 doctor, Wireshark 패킷 분석으로 접근하면 대부분 원인을 좁힐 수 있습니다. 여러 대의 AMR을 운영하는 환경이라면, 처음부터 Domain ID 할당 규칙과 Discovery Server 구성을 표준 배포 템플릿에 포함시켜 두는 것이 나중에 겪을 디버깅 비용을 크게 줄여줍니다.