제5부 좌표·모델
5.6 SDF와 URDF — 기술 형식의 이중성
5.6.1 이 장에서 배우는 것 / 선수 지식
5.3.8에서 URDF가 표현하지 못하는 것들을 봤다 — 폐쇄 사슬, 센서, 세계, 다중 로봇. 그 한계를 메우는 형식이 SDF(Simulation Description Format) 이며, Gazebo의 기본 형식이다.
여기서 실무적 난제가 시작된다. ROS 생태계는 URDF를 쓰고 시뮬레이터는 SDF를 쓴다. 같은 로봇을 두 형식으로 유지해야 하는가? 변환은 어디까지 되는가? 어느 것을 단일 출처로 삼을 것인가?
이 장은 그 판단을 다룬다. 결론을 먼저 말하면 — 대부분의 경우 URDF(xacro)를 단일 출처로 두고 시뮬 확장을 조건부로 추가하는 것이 실용적이며, SDF가 필요한 경우는 명확히 구분된다.
배우는 것:
- SDF의 정체 — 무엇을 더 표현하는가.
- 구조적 차이 — 월드·중첩 모델·센서·플러그인·폐쇄 사슬.
- 변환 — URDF → SDF가 어디까지 되는가.
- 판단 기준 — 어느 것을 단일 출처로.
- 실무 구성 — xacro 조건부 vs SDF 병행.
- 버전 문제 — sdformat과 Gazebo 세대.
선수 지식: 5.3(URDF), 5.4(xacro 조건부 블록), 5.1(tf). Gazebo 자체는 10.2에서 본격적으로 다루며, 여기서는 형식의 선택에 집중한다.
정직: SDF·sdformat 명세와 Gazebo 세대(Gazebo Classic vs 신 Gazebo)의 관계는 복잡하고 변해 왔다. BK-05에 따라 이 책은 Jazzy와 짝을 이루는 Gazebo Harmonic을 기준으로 하되, 변환 도구의 지원 범위·태그 이름은 버전 의존이므로
(확인 필요)를 남겨 두었다. 다만<mimic>의 소비자별 지원과 URDF 검사 도구는 Jazzy에서 실행 확인했다(5.6.11). Gazebo 자체는 이 환경에 없어 SDF 변환 결과는 검증하지 못했다(목차 3).
5.6.2 SDF는 무엇을 더 표현하는가
| 기능 | URDF | SDF |
|---|---|---|
| 로봇 구조(링크·관절) | ✅ | ✅ |
| 폐쇄 사슬 | ❌ 트리만 | ✅ |
| 센서 정의 | ❌ 확장 태그로만 | ✅ 1급 개념 |
| 월드(지형·조명·중력) | ❌ | ✅ |
| 다중 모델 | ❌ | ✅ |
| 중첩 모델(모델 안의 모델) | ❌ | ✅ |
| 플러그인 | 확장 태그 | ✅ |
| 2축 관절(universal 등) | ❌ | ✅ |
| 물리 속성(마찰·반발) | 확장 태그 | ✅ |
| 조명·재질 상세 | 제한적 | ✅ |
SDF 예:
<?xml version="1.0"?>
<sdf version="1.10">
<world name="factory">
<physics name="default" type="ode">
<max_step_size>0.001</max_step_size>
<real_time_factor>1.0</real_time_factor>
</physics>
<light type="directional" name="sun">
<direction>-0.5 0.1 -0.9</direction>
</light>
<model name="ground">
<static>true</static>
<link name="surface">
<collision name="c">
<geometry><plane><normal>0 0 1</normal></plane></geometry>
<surface><friction><ode><mu>0.8</mu></ode></friction></surface>
</collision>
</link>
</model>
<model name="my_robot">
<link name="base_link">
<sensor name="lidar" type="gpu_lidar">
<update_rate>10</update_rate>
<topic>scan</topic>
<lidar>
<scan><horizontal><samples>360</samples></horizontal></scan>
<range><min>0.1</min><max>10.0</max></range>
</lidar>
</sensor>
</link>
</model>
</world>
</sdf>
센서가 1급 개념이라는 점이 URDF와의 큰 차이다. URDF에서는 <gazebo> 확장 태그로 밀어 넣어야 하고(5.3.7), 그것은 URDF 파서가 무시하는 영역이라 오타가 조용히 지나간다.
5.6.3 왜 두 형식이 존재하는가
역사적 이유다. URDF는 ROS의 로봇 기술 형식으로 먼저 만들어졌고, 시뮬레이션에 필요한 것들(월드·센서·물리)이 부족했다. Gazebo는 그 확장으로 SDF를 만들었고, 이후 독립적으로 발전했다.
| 형식 | 설계 목적 | 주 소비자 |
|---|---|---|
| URDF | ROS의 로봇 운동학·시각 기술 | tf(5.1), MoveIt 2(9.1), RViz2(11.4) |
| SDF | 물리 시뮬레이션 전반 | Gazebo(10.2) |
결과: 생태계가 갈라져 있다.
| 도구 | URDF | SDF |
|---|---|---|
robot_state_publisher(5.5) | ✅ | ❌ |
| MoveIt 2(9.1) | ✅ | ❌ — SDF 파서가 없다(실행 확인: moveit_core는 URDF 모델만 만든다) |
| RViz2 | ✅ | ❌ |
| ros2_control(6.1) | ✅(확장 태그) | — |
| Gazebo | 변환해서 | ✅ 네이티브 |
| 다른 시뮬레이터(Isaac 등 — 10.5) | 각자 다름 | 각자 다름 |
이것이 URDF를 단일 출처로 삼는 실무적 근거다. ROS 생태계의 핵심 도구들이 URDF만 이해하므로, SDF를 원본으로 하면 그 도구들을 위해 URDF를 별도로 만들어야 한다.
5.6.4 URDF → SDF 변환
Gazebo는 URDF를 받으면 내부적으로 SDF로 변환한다.
# 변환 결과 확인 (신 Gazebo. Classic 은 gz2sdf 계열이었다)
gz sdf -p my_robot.urdf > my_robot.sdf
# 주의: gz 명령은 Gazebo 를 설치해야 생긴다 — ros-jazzy-ros-gz 로는 안 온다(10.2)
# xacro → urdf → sdf
xacro robot.urdf.xacro | gz sdf -p /dev/stdin > robot.sdf
변환에서 일어나는 일:
| URDF 요소 | SDF 변환 결과 |
|---|---|
<link>·<joint> | 대응 요소로 |
<inertial> | 그대로(5.3.5의 값이 여기서 쓰인다) |
<gazebo> 확장 태그 | SDF 요소로 삽입 |
| 고정 관절 | ⚠️ 링크가 병합될 수 있다 |
<mimic> | URDF 쪽 소비자는 모두 지원(실행 확인 — 5.6.11). SDF 변환 시의 보존 여부는 미확인 |
"고정 관절로 이어진 링크가 병합된다"가 중요한 함정이다.
왜 병합하는가: 물리 엔진 입장에서 고정 관절로 이어진 두 강체는 하나의 강체다. 성능을 위해 합치는 것이 합리적이다.
결과로 생기는 문제: 그 링크에 센서를 달았거나 그 프레임을 참조하면 찾을 수 없게 된다.
회피:
<!-- 병합을 막는 태그 -->
<gazebo reference="laser_link">
<preserveFixedJoint>true</preserveFixedJoint>
</gazebo>
(확인 필요: 태그 이름과 지원은 Gazebo 세대에 따라 다르다. 신 Gazebo에서는 다른 방식일 수 있다.)
함정: 시뮬에서만 센서 프레임을 못 찾는 문제의 흔한 원인이 이것이다. 실기에서는 RSP가
/tf_static으로 발행하니 정상이고, 시뮬에서는 병합되어 없다. 증상이 환경에 따라 달라 진단이 혼동을 일으킨다.
변환의 한계:
| 한계 | 설명 |
|---|---|
| 폐쇄 사슬 | URDF에 없으니 변환도 불가 |
| SDF 고유 기능 | URDF에서 표현 못 하는 것은 <gazebo> 태그로만 |
| 월드 | URDF는 로봇만 — 월드는 별도 SDF 필요 |
| 정확도 | 변환기 구현에 의존 |
5.6.5 판단 — 어느 것을 단일 출처로
세 가지 접근이 있다.
① URDF(xacro) 단일 출처 + <gazebo> 확장
<!-- robot.urdf.xacro -->
<xacro:if value="${use_sim}">
<gazebo reference="laser_link">
<sensor name="lidar" type="gpu_lidar">...</sensor>
</gazebo>
</xacro:if>
| 축 | 평가 |
|---|---|
| ROS 도구 호환 | ✅ 완전 |
| 시뮬 표현력 | ⚠️ 확장 태그의 한계 안에서 |
| 유지보수 | ✅ 한 파일 |
| 폐쇄 사슬 | ❌ |
| 월드 | 별도 SDF 필요 |
적합: 대부분의 이동 로봇·매니퓰레이터. 기본 선택.
② SDF 단일 출처 + URDF 생성
SDF를 원본으로 두고, ROS 도구용 URDF를 별도로 만든다(수동 또는 도구).
| 축 | 평가 |
|---|---|
| 시뮬 표현력 | ✅ 완전 |
| ROS 도구 호환 | ⚠️ 변환 필요 |
| 유지보수 | ❌ SDF → URDF 자동 변환이 어렵다(확인 필요) |
| 폐쇄 사슬 | ✅ |
적합: 시뮬레이션이 주된 목적이고 ROS 도구 사용이 제한적인 경우. 드물다.
③ 병행 유지
URDF와 SDF를 각각 관리한다.
| 축 | 평가 |
|---|---|
| 각 영역 표현력 | ✅ 최대 |
| 일관성 | ❌ 반드시 어긋난다 |
| 유지보수 | ❌ 두 배 |
견해: ③은 피하라. 두 파일이 어긋나는 것은 시간 문제이고, 어긋난 뒤에는 "시뮬과 실기가 왜 다른가"를 며칠씩 쫓게 된다(10.6). 단일 출처 원칙(5.2.5·5.4.5·5.5.4에서 반복한 것)이 여기서 가장 중요하다.
견해: ①이 기본이고, 폐쇄 사슬이나 고급 시뮬 기능이 정말 필요할 때만 다시 생각하라. 그리고 그때도 "폐쇄 사슬을 시뮬에서만 닫는" 부분적 접근이 가능한지 검토한다 — Gazebo 플러그인이나 SDF 스니펫을 조건부로 삽입하는 방식이다.
5.6.6 실무 구성 예
단일 출처 xacro에서 세 가지 산출물을 만드는 구조:
<!-- robot.urdf.xacro -->
<xacro:arg name="use_sim" default="false"/>
<xacro:property name="use_sim" value="$(arg use_sim)"/>
<!-- 공통 구조 -->
<xacro:include filename="base.xacro"/>
<xacro:include filename="sensors.xacro"/>
<!-- 시뮬 전용 -->
<xacro:if value="${use_sim}">
<xacro:include filename="gazebo_sensors.xacro"/>
<xacro:include filename="gazebo_physics.xacro"/>
</xacro:if>
<!-- ros2_control: 시뮬과 실기가 다른 플러그인 (10.4) -->
<ros2_control name="MyRobot" type="system">
<hardware>
<xacro:if value="${use_sim}">
<plugin>gz_ros2_control/GazeboSimSystem</plugin>
</xacro:if>
<xacro:unless value="${use_sim}">
<plugin>my_robot_driver/MyRobotHardware</plugin>
<param name="device">/dev/ttyUSB0</param>
</xacro:unless>
</hardware>
<!-- 관절 정의는 공통 -->
</ros2_control>
이 구조의 핵심 이점: 관절 이름·치수·관성이 시뮬과 실기에서 자동으로 동일하다. Sim2Real 간극(10.6)의 상당 부분이 "모델이 미묘하게 다른 것"에서 오는데, 그것이 구조적으로 차단된다.
월드는 별도 SDF로 유지한다:
| 파일 | 내용 |
|---|---|
worlds/factory.sdf | 지형·장애물·조명·물리 설정 |
urdf/robot.urdf.xacro | 로봇 |
로봇을 월드에 스폰하는 것은 실행 시점에 한다(10.2) — 월드 파일에 로봇을 고정 포함하지 않는다. 그래야 같은 월드에 여러 로봇을 스폰하거나 로봇만 교체할 수 있다.
5.6.7 버전 문제
sdformat 버전과 Gazebo 세대가 얽혀 있다.
| 축 | 설명 |
|---|---|
| sdformat 버전 | <sdf version="1.10"> 같은 명세 버전 |
| Gazebo 세대 | Gazebo Classic(구) vs 신 Gazebo(Harmonic 등) |
| ROS 연동 패키지 | gazebo_ros_pkgs(구) vs ros_gz(신) |
정직: Gazebo Classic과 신 Gazebo는 태그·플러그인·토픽 구조가 상당히 다르다. 인터넷의 예제 상당수가 Classic 기준이며, 그대로 쓰면 동작하지 않는다. BK-05에 따라 이 책은 Jazzy와 짝을 이루는 Harmonic을 기준으로 하되, 구체 태그·플러그인 이름은 Gazebo 문서의 해당 버전을 확인해야 한다
(확인 필요).
세대 구분 신호:
| 신호 | 세대 |
|---|---|
gazebo_ros·libgazebo_ros_*.so | Classic |
ros_gz_sim·gz-sim 플러그인 | 신 Gazebo |
<sensor type="ray"> | Classic |
<sensor type="gpu_lidar"> | 신 Gazebo(확인 필요) |
gazebo 명령 | Classic |
gz sim 명령 | 신 Gazebo |
견해: 예제를 참고할 때 세대를 먼저 확인하라. 태그 하나가 다른 것이 아니라 구조 전체가 다르므로, Classic 예제를 조금씩 고쳐 쓰려다 시간을 버리는 경우가 많다. Harmonic 공식 문서와
ros_gz저장소의 예제를 출발점으로 삼는 편이 빠르다.
5.6.8 SDF가 정말 필요한 경우
①을 기본으로 하되, 다음에서는 SDF를 직접 다루게 된다.
| 상황 | 이유 |
|---|---|
| 월드 제작 | URDF로 표현 불가 |
| 폐쇄 사슬 로봇 | 델타 로봇·병렬 기구·4절 링크 |
| 복잡한 물리 튜닝 | 접촉·마찰·서스펜션 세부 |
| 다중 모델 상호작용 | 컨베이어·문·리프트 |
| 액터(사람 등) 시뮬 | SDF 고유 기능 |
| Gazebo 고유 기능 | 확장 태그로 다 안 되는 것 |
폐쇄 사슬의 실무적 접근:
세 번째 경우에도 URDF를 버릴 필요는 없다 — URDF는 트리로 두고, 시뮬에서만 추가 관절로 사슬을 닫는 구성이 가능하다. 조건부 블록(5.4.5)으로 시뮬 전용 SDF 스니펫을 삽입한다.
5.6.11 <mimic>은 누가 처리하나 — 실측
이 장과 5.5가 함께 (확인 필요)로 남긴 항목이다 — "mimic 관절의 처리는 소비자마다 다르다". URDF 쪽 소비자는 셋 다 지원한다는 것을 확인했다(실행 확인, Jazzy).
| 소비자 | 지원 | 근거 |
|---|---|---|
URDF 파서(urdfdom) | ✅ | <mimic joint multiplier offset>이 든 URDF를 check_urdf가 정상 파싱 |
ros2_control | ✅ | hardware_info.hpp에 MimicJoint 구조체(joint_index·mimicked_joint_index·multiplier·offset)와 MimicAttribute 열거형(NOT_SET/TRUE/FALSE) |
| MoveIt 2 | ✅ | JointModel에 getMimic()·getMimicFactor()·getMimicOffset()·getMimicRequests()·setMimic() |
robot_state_publisher | ✅ | 5.5.6에서 이미 다룬다 |
즉 "소비자마다 다르다"는 우려는 URDF 경로에서는 해소된다. 평행 그리퍼처럼 한 모터가 두 손가락을 움직이는 구조를 URDF에 <mimic>으로 적으면, 상태 발행·계획·제어가 모두 그 관계를 안다.
ros2_control의MimicAttribute에NOT_SET이 있다는 점이 설계를 드러낸다. URDF의<mimic>유무만이 아니라<ros2_control>블록에서 명시적으로 켜고 끌 수 있다는 뜻이다 — URDF는 mimic인데 하드웨어는 두 축을 독립 제어하는 경우를 표현하기 위한 것으로 보인다.
함정: 확인하지 못한 것은 SDF 변환 경로다. URDF를 SDF로 바꿀 때 mimic 관계가 보존되는지는 이 환경에 Gazebo가 없어 검증하지 못했다. 시뮬레이터에서 그리퍼 한쪽만 움직인다면 이 지점을 의심하라 — URDF에서 되는 것이 SDF에서도 된다는 보장이 없다는 것이 이 장의 요지이기도 하다.
5.6.11.1 gz 명령은 별도 설치다
이 장의 변환 예시(gz sdf -p)를 그대로 따라 하려면 Gazebo가 설치되어 있어야 한다(실행 확인 — ROS 2만 설치된 환경에는 gz 명령이 없다). ros_gz 브리지 패키지를 설치해도 오지 않는다.
URDF 쪽 검사 도구는 ROS 2 설치에 포함된다:
| 도구 | 용도 |
|---|---|
check_urdf <file> | 파싱 + 링크 트리 출력 |
urdf_to_graphiz <file> | 트리를 그래프로(.gv·.pdf) |
check_urdf의 출력이 트리 구조를 그대로 보여 준다는 점이 유용하다:
robot name is: m
---------- Successfully Parsed XML ---------------
root Link: base has 1 child(ren)
child(1): a
child(1): b
견해: URDF를 고칠 때마다
check_urdf를 먼저 실행하라. RViz2나 시뮬레이터를 실행해 확인하는 것보다 훨씬 빠르고, 트리가 의도한 모양인지(5.3.3의 부모·자식 관계)를 한눈에 본다. 링크가 빠져 있거나 루트가 둘로 갈라진 경우가 여기서 바로 드러난다.
5.6.9 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | URDF·SDF 병행 유지 | 반드시 어긋남 | 단일 출처(5.6.5) |
| 2 | 고정 관절 링크 병합 | 시뮬에서만 프레임 없음 | preserveFixedJoint 계열 |
| 3 | Classic 예제를 신 Gazebo에 | 동작 안 함 | 세대 확인(5.6.7) |
| 4 | 월드에 로봇을 고정 포함함 | 재사용 불가 | 실행 시 스폰 |
| 5 | <gazebo> 태그 링크명 오타 | 조용히 무시(5.3.7) | 이름 대조 |
| 6 | 시뮬·실기 관절 이름 불일치 | 컨트롤러 실패 | 단일 xacro |
| 7 | SDF를 원본으로 두고 ROS 도구 사용 | URDF 수동 유지 부담 | URDF 원본 |
| 8 | 변환 결과 미확인 | 예상과 다른 SDF | gz sdf -p로 확인 |
| 9 | 폐쇄 사슬을 URDF로 시도 | 표현 불가 | 근사 또는 시뮬 전용 |
| 10 | sdformat 버전 불일치 | 파싱 오류·무시 | 버전 명시·확인 |
| 11 | 시뮬에서 그리퍼 한쪽만 움직임 | SDF 변환에서 <mimic> 유실 의심 | 5.6.11 |
5.6.10 체크포인트
- SDF가 URDF보다 더 표현하는 것 일곱 가지를 들 수 있는가. "센서가 1급 개념"인 것의 실무적 차이는?
- 두 형식이 존재하는 역사적 이유와, 생태계가 갈라진 결과를 설명할 수 있는가.
- URDF를 단일 출처로 삼는 실무적 근거는 무엇인가.
- URDF → SDF 변환에서 고정 관절에 무슨 일이 일어나는가. 왜 그렇게 하며, 어떤 문제를 만드는가.
- 그 문제가 왜 진단하기 혼동되는가(환경별 차이).
- 세 가지 단일 출처 접근의 장단을 비교하고, 권장되는 것과 피해야 하는 것을 말할 수 있는가.
- 단일 xacro에서 시뮬·실기를 분기하는 구조가 Sim2Real 간극에 어떤 도움이 되는가.
- 월드에 로봇을 고정 포함하지 않는 이유는?
- Gazebo 세대를 구분하는 신호 세 가지 이상을 들 수 있는가. 예제 참고 시 왜 세대 확인이 먼저인가.
- SDF를 직접 다뤄야 하는 상황 여섯 가지를 들 수 있는가.
- 폐쇄 사슬 로봇에서 필요에 따른 세 가지 접근을 구분할 수 있는가.
<mimic>관절을 URDF 쪽 소비자 셋은 각각 어떻게 처리하는가. 확인되지 않은 경로는 어디인가(5.6.11).
다음 장 5.7은 제5부의 마지막 — 로봇 모델 품질 검증이다. check_urdf가 포착하지 못하는 것들을 어떻게 체계적으로 걸러내고, 그 검증을 CI에 넣는가.