상태: 초안 (2026-10-18), 실측. 측정은 2026-10-17 에 홈랩 쿠버네티스의 임시 파드 하나에서 했습니다. 노드는 미니 PC 한 대(Intel Core Ultra 계열 22스레드, 메모리 31 GiB)이고, 파드 상한은 CPU 3코어·메모리 4 GiB 이며 GPU 와
/dev/dri는 주지 않았습니다(특권 없음, 추가 capability 없음). 이미지는ros:jazzy(ros-base, Ubuntu 24.04.5)에 apt 로ros-jazzy-ros-gz·ros-jazzy-turtlebot3-gazebo를 더했습니다. 판은 Gazebo Sim 8.15.0(Harmonic), ros_gz 1.0.24, turtlebot3_gazebo 2.3.7 입니다. 반복은 B 2회, C 3회, 나머지 1회이며 표의 값은 실행마다 한 번 잰 값입니다(중앙값 아님). 원자료(rosbag2 mcap, 요약 JSON, 1초 간격 cgroup 표본)는 작업 디렉터리의 ROS 측정 폴더에 있고, 글에는 노드 이름과 주소를 가린 값만 옮겼습니다.
목차
- 1. 직관: 눈을 감고 걸음 수를 세는 로봇
- 2. 구성: 파드 하나 안의 헤드리스 시뮬레이션
- 3. 네 가지 월드
- 4. 주행 노드: 시간이 아니라 odom 을 보고 간다
- 5. 결과: 실시간을 지키는 비용과 모서리 오차
- 6. 바닥 마찰 한 줄을 빠뜨렸을 때
- 7. GPU 없이 도는 라이다
- 8. CI 로 쓸 수 있는 최대 속도 모드
- 9. 겪은 문제
- 10. 한계
- 11. 다음 편으로
- 참고
1. 직관: 눈을 감고 걸음 수를 세는 로봇
눈을 감고 「열 걸음 앞으로, 왼쪽으로 90도」를 네 번 하면 처음 자리로 돌아온다고 믿기 쉽습니다. 걸음 수는 정확히 셌으니까요. 그런데 바닥이 미끄러우면 한 걸음이 덜 가고, 돌 때 발이 헛돌면 90도가 아니라 80도만 돕니다. 걸음 수만 세는 사람은 그 차이를 모릅니다.
바퀴 로봇의 오도메트리(odom) 가 바로 걸음 수 세기입니다. 바퀴가 몇 바퀴 돌았는지를 적분해 「지금 여기 있다」고 말합니다. 시뮬레이터는 여기에 한 가지를 더 줍니다. 물리 엔진이 아는 실제 위치(정답 자세) 입니다. 이 글은 두 값을 나란히 기록하면 무엇이 보이는지를 실측으로 보여 줍니다.
2. 구성: 파드 하나 안의 헤드리스 시뮬레이션
gz sim -s -r로 서버만 띄웠습니다. 화면(GUI)이 없으므로 X 서버나 VNC 가 필요 없습니다.ros_gz_bridge가 Gazebo 토픽과 ROS 2 토픽을 잇습니다. TurtleBot3 가 기본으로 쓰는 브리지 설정에 한 줄을 더해, Gazebo 의/world/default/dynamic_pose/info를/gt_poses라는 이름으로 꺼냈습니다. 이것이 정답 자세입니다.ros2 bag record -s mcap이/odom,/cmd_vel,/gt_poses,/imu(라이다가 있는 월드는/scan까지)를 한 파일에 담습니다.- DDS 탐색이 파드 네트워크로 새어 나가지 않도록
ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST로 묶었습니다.
이미지 비용도 쟀습니다. ros:jazzy 는 압축 283 MiB 이고 받는 데 18.3 s 가 걸렸습니다. 여기에 시뮬레이션 패키지를 apt 로 더하면
768개 패키지, 554 MB 를 받아 설치 뒤 2,637 MB 가 늘고, 272 s 가 걸렸습니다. 참고로 레지스트리 매니페스트로만 본 osrf/ros:jazzy-desktop 은
압축 1,203.9 MiB, jazzy-simulation 은 698.3 MiB 입니다(받지 않았습니다). 매번 설치하는 시간이 아깝다면 이 단계를 이미지로 굳히는 것이 다음 할 일입니다.
3. 네 가지 월드
| 이름 | 무엇이 다른가 |
|---|---|
| A | TurtleBot3 의 빈 월드 원본입니다. Sensors 시스템(ogre2)과 렌더링이 필요한 gpu_lidar 가 있어 --headless-rendering 으로 띄웠습니다. |
| A2 | A 에 정적인 상자 두 개를 놓았습니다. 빈 월드에서는 라이다 값이 모두 무한대라 렌더링이 제대로 되는지 알 수 없기 때문입니다. |
| B | Sensors 시스템을 뺐습니다. 바닥은 인라인 평면이고, 마찰은 원본 바닥과 같게 mu 100, mu2 50 입니다. 렌더링도 네트워크도 쓰지 않습니다. |
| C | B 에서 real_time_factor 를 0 으로 바꿔 실시간에 묶지 않고 최대 속도로 돌립니다. |
4. 주행 노드: 시간이 아니라 odom 을 보고 간다
주행 노드는 「5초 동안 직진」 같은 열린 루프가 아닙니다. /odom 을 보고 모서리 (1,0), (1,1), (0,1), (0,0) 을 차례로 찾아가는 닫힌 루프이고,
최대 0.2 m/s, 0.6 rad/s, 시뮬레이션 시각 기준 50 Hz 입니다. 실제 로봇의 웨이포인트 주행과 같은 방식이어야 「목표 대비 오차」가 제어기, 오도메트리,
물리의 합으로 읽히기 때문입니다. 핵심은 직진 단계의 몇 줄입니다.
if self.phase == 'drive':
tx, ty = CORNERS[self.leg]
dx, dy = tx - x, ty - y
# 진행 방향 위로 남은 거리. 지나치면 음수가 된다.
seg_heading = HEADINGS[self.leg - 1] if self.leg else 0.0
along = dx * math.cos(seg_heading) + dy * math.sin(seg_heading)
if along <= POS_TOL: # 5 mm 안이면 멈추고 돌기 시작
self.send(0.0, 0.0)
self.mark('corner')
self.phase = 'turn'
return
bearing = math.atan2(dy, dx) if math.hypot(dx, dy) > 0.05 else seg_heading
w = max(-W_MAX, min(W_MAX, 2.0 * wrap(bearing - th)))
v = max(0.02, min(V_MAX, 0.8 * along))
self.send(v, w)
(TT Lab 실험 코드 square_driver.py 에서 발췌했고 주석은 글에 맞게 줄이거나 더했습니다. 회전 목표는 모서리마다 절대 방위 0, π/2, π, −π/2 로 주어 회전 오차가 쌓이지 않게 했습니다.)
5. 결과: 실시간을 지키는 비용과 모서리 오차
| 실행 | RTF | 한 바퀴 벽시계 | 모서리 실제 오차 mm (1→4) | 끝 위치 실제 오차 | odom↔실제 최대 | CPU 평균 | anon 최대 | gz RSS |
|---|---|---|---|---|---|---|---|---|
| A 렌더링 라이다 | 1.000 | 47.5 s | 11, 33, 40, 25 | 16 mm | 41 mm | 0.91코어 | 467 MiB | 515 MiB |
| A2 A + 상자 | 1.000 | 47.5 s | 11, 47, 66, 43 | 50 mm | 74 mm | 0.92코어 | 466 MiB | 509 MiB |
| B 물리만 | 1.000 | 47.6 s | 17, 23, 14, 5 | 16 mm | 26 mm | 0.73코어 | 196 MiB | 156 MiB |
| B 반복 | 1.000 | 47.5 s | 15, 33, 28, 13 | 16 mm | 32 mm | 0.74코어 | 196 MiB | 측정 안 함 |
| C 최대 속도 | 11.11 | 4.3 s | 19, 27, 20, 7 | 16 mm | 39 mm | 2.88코어 | 196 MiB | 156 MiB |
| C 반복 2 | 11.76 | 4.1 s | 20, 28, 20, 8 | 17 mm | 39 mm | 2.54코어 | 196 MiB | 측정 안 함 |
| C 반복 3 | 10.88 | 4.4 s | 19, 28, 20, 7 | 16 mm | 39 mm | 2.86코어 | 196 MiB | 측정 안 함 |
RTF(실시간 대비 속도)는 /odom 머리말의 시뮬레이션 시각이 늘어난 양을 기록기가 받은 벽시계 시각이 늘어난 양으로 나눈 값입니다.
CPU 는 파드 cgroup 의 cpu.stat 을 1초마다 읽어 주행 구간을 평균했고, 메모리는 memory.stat 의 anon 최대값입니다.
memory.current 는 3.0–3.6 GiB 로 나오지만 대부분 apt 설치 때 생긴 페이지 캐시여서, 실제로 쥔 메모리는 anon 으로 봅니다.
- odom 기준으로는 모든 실행이 거의 완벽했습니다. 모서리 오차는 4.6–5.0 mm(정지 허용치 5 mm), 회전은 89.74–89.78°(허용치 0.005 rad)였습니다. 표의 실제 오차는 거의 전부 odom 과 실제가 벌어진 몫입니다.
- 제자리에서 90° 돌 때마다 실제 위치가 12–13 mm 옮겨졌습니다(B 실행, odom 은 0.3 mm 이하). 캐스터가 끌리는 탓으로 보지만 확인하지는 않았습니다(추정).
- 기동은
gz sim실행부터 첫/odom까지 3.6–3.7 s 였고, A 첫 실행만 원본 바닥 모델을 받느라 5.4 s 였습니다.
6. 바닥 마찰 한 줄을 빠뜨렸을 때
렌더링 없는 월드(B)를 처음 만들 때, 원본의 바닥 모델을 인라인 평면으로 바꾸면서 마찰 설정을 적지 않았습니다. 그러면 물리 엔진(ODE)의 기본값 mu 1 이 쓰입니다. 고친 뒤의 바닥은 다음 한 줄을 갖습니다.
FRIC = '<surface><friction><ode><mu>100</mu><mu2>50</mu2></ode></friction></surface>' # 원본 바닥과 같게
아래 그림은 같은 주행 노드가 두 월드에서 돈 기록입니다. 그림을 누르면 rosbag 에 남은 실제 궤적을 4배속으로 다시 재생합니다.
| 실행 | 회전당 실제 각도 | 모서리 실제 오차 mm (1→4) | 끝 위치 실제 오차 / 방향 | odom 이 본 끝 위치 오차 |
|---|---|---|---|---|
| B (마찰 있음) | 89.2, 89.7, 89.7, 89.7° | 17, 23, 14, 5 | 16 mm / −0.98° | 5 mm |
| B 마찰 누락 | 80.3, 79.7, 80.3, 80.9° | 10, 138, 344, 487 | 503 mm / −37.99° | 5 mm |
| C 마찰 누락 | 78.0, 79.2, 80.5, 80.9° | 19, 164, 399, 533 | 549 mm / −40.24° | 4 mm |
제자리 회전에서 바퀴가 미끄러져 90° 대신 약 80° 만 돌았고, 그 어긋남이 모서리마다 쌓였습니다. 그런데 odom 은 바퀴 관절 각도를 적분하므로 미끄러짐을 알 수 없습니다. 그래서 odom 은 끝까지 「5 mm 안에 돌아왔다」고 보고했습니다. 결과를 odom 만으로 판정했다면 이 월드는 시험을 통과했을 것입니다.
정답 자세가 맞는지도 따로 확인했습니다. 실행이 끝난 뒤 gz model -m burger -p 로 물은 위치를 /gt_poses 의 마지막 값과 비교했습니다.
C 마찰 누락 실행에서 gz model 은 (−0.2505, 0.4883) m, 방향 −40.0° 를 돌려주었고, /gt_poses 는 (−0.2505, 0.4884) m, −40.2° 였습니다.
마찰이 있는 B 에서는 두 값이 (−0.0130, 0.0099) m 로 같았습니다.
교훈. 시뮬레이션에서 로봇의 성능을 판정할 때는 로봇이 스스로 추정한 위치와 시뮬레이터의 정답 위치를 함께 기록하고, 판정은 정답 쪽으로 합니다. 추정 위치만 보면 물리 설정의 실수가 로봇의 성공으로 보입니다. 반대로 정답만 보면 추정기가 얼마나 틀리는지 모릅니다. 둘의 차이(그림의 빨간 선)가 오도메트리 표류이고, 이것이 실제 로봇에서 위치 추정기를 따로 두는 이유입니다.
7. GPU 없이 도는 라이다
A 와 A2 는 렌더링으로 거리를 재는 gpu_lidar(360빔, 5 Hz)를 씁니다. 파드에는 GPU 도 /dev/dri 도 없었지만 --headless-rendering(EGL)으로 떠서
RTF 1.000 을 지켰습니다.
/scan주기는 5.00 Hz 로, 모델에 적힌 update_rate 5 그대로였습니다.- A2 에서 마지막 다섯 번의 스캔 1,800빔 가운데 185빔이 유한한 거리였습니다. 상자를 실제로 보았다는 뜻입니다. 빈 월드 A 는 0/1,800(모두 무한대)으로 정상입니다.
- 렌더링 비용(A 와 B 의 차이)은 CPU +0.18코어, anon 메모리 +270 MiB, gz 프로세스 RSS 156 → 515 MiB 였습니다.
- 로그에는 ogre2 렌더링 플러그인을 불러왔다는 줄만 있고 렌더러 이름은 나오지 않습니다. GPU 가 없으니 Mesa 의 소프트웨어 렌더러로 그렸다고 보지만 확인하지 못했습니다(미확인).
- 카메라 센서(해상도와 주기가 훨씬 큼)는 재지 않았습니다(미실측). 라이다 한 대가 된다고 카메라가 되는 것은 아닙니다.
8. CI 로 쓸 수 있는 최대 속도 모드
C 는 real_time_factor 를 0 으로 둔 B 입니다. 1 m 정사각형 한 바퀴(시뮬레이션 47.7–51.7 s)가 벽시계 4.1–4.4 s 에 끝났고, RTF 는 10.88–11.76 이었습니다.
- 이 속도는 CPU 상한 3코어에 붙은 값입니다(평균 2.54–2.88코어). 물리 한 스레드와 브리지, 기록기, 주행 노드가 상한을 나눠 썼습니다. 코어 수를 바꿔 가며 RTF 가 어떻게 따라가는지는 재지 않았습니다(미실측).
- 재현성이 오히려 좋아졌습니다. C 를 세 번 돌린 모서리 오차는 1 mm 안에서 같았습니다(19–20, 27–28, 20, 7–8 mm). 실시간 B 를 두 번 돌리면 마지막 모서리가 5 mm 와 13 mm 로 달랐습니다. 실시간 모드에서는 벽시계와 맞추는 과정이 매번 조금씩 다르게 끼어드는 것으로 봅니다(추정).
- 한 가지 주의할 점이 있습니다. C 에서는
dynamic_pose가 벽시계 기준으로 나와 정답 자세가 시뮬레이션 시각으로 약 12 Hz 로 듬성해집니다. 정답과 odom 을 시각으로 맞출 때 보간이 필요합니다.
PR 마다 「로봇이 1 m 정사각형을 2 cm 안에서 돈다」를 4초짜리 시험으로 돌릴 수 있다는 뜻입니다. 단, 그 판정은 6절처럼 정답 자세로 해야 합니다.
9. 겪은 문제
set -u를 켠 셸 스크립트에서/opt/ros/jazzy/setup.bash를 읽으면 정의되지 않은 변수 때문에 멈춥니다.- 작업 제어가 없는 스크립트에서
&로 띄운ros2 bag record는 SIGINT 를 무시합니다. SIGTERM 으로 바꾸었고, rosbag2 는 SIGTERM 에도 파일을 정상으로 닫았습니다. ros2 run래퍼를 끝내도parameter_bridge자식 프로세스가 남아서 바이너리를 직접 실행했습니다.Pose_V를tf2_msgs/TFMessage로 브리지하면 frame_id, child_frame_id, 시각이 비어서 옵니다. 첫 항목이 로봇 모델이라는 것은gz model -p와 대조해 확인했고, 시각은 기록기가 받은 시각으로 맞췄습니다.- odom 주기는 시뮬레이션 시각 기준 50.0 Hz 로 쟀습니다. 모델 SDF 의
odom_publisher_frequency는 30 인데, 차이의 원인은 보지 않았습니다(미확인).
10. 한계
- 노드 한 대, 실행마다 한두 번입니다. 오차의 분포를 말하기에는 반복이 적습니다.
- 바퀴 로봇 한 종(burger)과 빈 평지만 보았습니다. 경사, 턱, 다른 바닥 재질은 다루지 않았습니다.
- 캐스터 끌림 가설(회전당 12–13 mm 이동)은 캐스터 마찰을 바꿔 보는 실험으로 가를 수 있는데, 아직 하지 않았습니다.
- GPU 노드에서의 대조와 카메라 센서는 미실측입니다.
11. 다음 편으로
다음 편은 탁구 로봇으로 넘어갑니다. 바퀴 대신 공 하나를 MuJoCo 에 올려, 반발과 회전이 시뮬레이터 설정 한 줄에 얼마나 흔들리는지 잽니다. 로봇 팔 자체의 기초(좌표계, 역기구학)는 함께 쓰이고 있는 「탁구 로봇 팔의 기초」 시리즈(목차)에서 다룹니다.
티티 한마디. 걸음 수만 세면 저는 늘 제자리에 돌아온 거예요. 그런데 바닥이 미끄러우면 실제로는 반 미터 옆에 서 있죠. 그래서 시뮬레이터에서는 꼭 「진짜 내 위치」도 같이 적어 둬요!
참고
모든 링크는 2026-10-18 에 열람해 열리는 것을 확인했습니다. 판은 측정에 쓴 판입니다.
- ROS 2 Jazzy 문서: https://docs.ros.org/en/jazzy/
- Gazebo Harmonic 문서: https://gazebosim.org/docs/harmonic/
- ros_gz(브리지, 1.0.24): https://github.com/gazebosim/ros_gz
- TurtleBot3 시뮬레이션(turtlebot3_gazebo 2.3.7): https://github.com/ROBOTIS-GIT/turtlebot3_simulations
- rosbag2: https://github.com/ros2/rosbag2, MCAP 형식: https://mcap.dev/spec
- 공식 이미지
ros:jazzy: https://hub.docker.com/_/ros - 인용 코드는 모두 TT Lab 실험 코드입니다.