ROS 2 시뮬레이션을 10배속으로 돌리면 타임스탬프는 얼마나 어긋날까: 통신 지연과 real time factor 곱하기
ros2 bag play에 --rate 10을 주거나 시뮬레이터의 real time factor를 10으로 올렸을 때, 노드가 읽는 시각은 얼마나 뒤처질까. ROS 2 시뮬레이션을 10배속으로 돌리면 타임스탬프는 얼마나 어긋날까라는 질문에 가장 단순한 모델은 이렇게 답한다. /clock 메시지가 실제 시간으로 5 ms 늦게 도착하면, 그 사이 시뮬레이터 안에서는 50 ms가 이미 흘러 있다. 2 m/s로 움직이는 로봇이라면 0.1 m 전 자리의 시각을 ‘지금’으로 쓰는 셈이다. 여기에 /clock이 띄엄띄엄 오기 때문에 생기는 오차가 한 겹 더 붙는다.
두 오차는 성격이 다르고, 줄이는 수단도 다르다. 통신 지연, 배속, /clock 발행 빈도 세 값을 알면 각 오차를 따로 계산해서 배속을 낮출지 발행 빈도를 올릴지 고를 수 있다. 초와 밀리초 환산, 곱셈과 나눗셈, /clock이 시뮬레이션 시각을 알려 주는 토픽이라는 정도만 알면 따라올 수 있다. 원리는 ROS 2 설계 문서 “Clock and Time”과 rclcpp·rosbag2 소스(2026년 9월 25일 확인)에 기대고, 본문의 수치는 같은 날 Python 3.12.3으로 다시 검산한 설명용 산술이다. ROS 2 노드나 시뮬레이터를 띄워 지연을 잰 값은 아니다.
use_sim_time 노드의 ‘지금’은 마지막으로 받은 /clock 값이다
시뮬레이션 시간(simulated time)은 시뮬레이터나 로그 재생이 정하는 논리적인 시계다. ROS 2 노드에 use_sim_time을 켜면 그 노드의 ROS 시간(ROSTime)은 벽시계 대신 이 시계를 따른다. ROSTime·SystemTime·SteadyTime이 어떻게 다른지, 2배속 재생에서 dt 계산이 어떻게 바뀌는지는 ROS 2 시뮬레이션 시간이 뒤로 가면 로봇 타이머 계산은 어떻게 깨질까에서 먼저 다뤘다. 여기서는 그 시각이 노드에 전달되는 방식만 본다.
전달 통로가 /clock 토픽이다. 한 발행자가 최신 시뮬레이션 시각을 담아 보내고, use_sim_time 노드는 시간을 물을 때마다 마지막으로 받은 값을 돌려준다. 아직 하나도 받지 못했다면 0이고, 설계 문서는 이를 초기화되지 않은 상태로 본다(설계 문서 L110-L123). 현재 rclcpp 구현도 같다. /clock 메시지가 올 때마다 rcl_set_ros_time_override로 ROS 시간을 그 값으로 덮어쓰고, 다음 메시지가 올 때까지 값을 그대로 둔다(time_source.cpp L109-L134). 이 부분은 소스를 읽어 확인했고 실행해 보지는 않았다.
real time factor(RTF, 실시간 배율)는 시뮬레이션 시간이 실제 시간보다 몇 배 빨리 흐르는지를 나타낸다. RTF 10이면 벽시계로 1초가 지날 때 시뮬레이션 안에서는 10초가 지난다. 여기서 실제 시간(real time, wall time)은 벽시계가 흐르는 속도를 말한다. 정해진 마감 안에 계산을 끝내야 하는 실시간(real-time) 컴퓨팅과는 다른 뜻이라고 설계 문서가 따로 짚는다. SDFormat 1.11은 <real_time_factor>를 “시뮬레이션 시간 대 실제 시간 비로 정의한 목표 가속 배율, 기본값 1.0″으로 둔다(physics.sdf L17-L25). 목표값이므로 계산이 무거우면 실제로 달성되는 배율은 이보다 낮을 수 있다. 로그 재생에서는 ros2 bag play의 -r, --rate(기본 1.0, 0보다 커야 함)가 같은 역할을 한다(play.py L66-L67). rosbag2 자체는 RTF라는 말을 쓰지 않는다.
빨리감기 중인 영상 옆에 재생 시각 표시가 붙어 있는데, 그 표시가 조금 늦게 갱신된다고 생각하면 그림이 잡힌다. 빨리 감을수록 같은 갱신 지연 동안 영상은 더 많이 진행한다. 다만 비유는 두 군데서 어긋난다. 영상 플레이어는 표시를 매끄럽게 보간할 수 있지만 ROS 2의 기본 time source는 보간하지 않는다. 그리고 실제 네트워크 지연은 고정된 값이 아니라 매번 흔들린다.
5 ms 늦게 도착한 시각은 10배속에서 50 ms 전의 시각이 된다
통신 지연(latency) L은 /clock 메시지가 발행된 뒤 수신 노드가 그 값을 쓰기까지 걸린 실제 초다. 시각 t가 적힌 메시지가 L초 늦게 도착하면, 그 사이 시뮬레이터 안에서는 RTF × L초가 흘렀다. 수신 노드는 이미 지나간 t를 ‘지금’으로 쓴다. 설계 문서는 이 부정확성이 통신 지연에도, real time factor에도 비례한다고 쓰고, 아주 정확한 타임스탬프가 필요하면 RTF를 낮춰 지연을 상대적으로 작게 만들라고 권한다(설계 문서 L73-L76).
문서가 준 것은 “비례한다”까지다. 계수는 없다. 아래 계산은 메시지 한 번의 전달만 보고 비례계수를 1로 둔 교육용 모델이다.
| 기호 | 뜻 | 단위 | 예제 값 |
|---|---|---|---|
| L | 통신 지연 | 실제 초 | 0.005 (5 ms, 가정) |
| RTF | real time factor | 배 | 1, 10, 0.5 |
| v | 로봇 속도 | m/s | 2 |
| f | /clock 발행 빈도 |
실제 Hz | 40, 100, 1000 |
| E | 허용 타임스탬프 오차 | 시뮬레이션 초 | 0.02 |
지연 항은 RTF × L이다. 속도를 곱하면 그 시간 차이 동안 로봇이 움직인 거리, 즉 타임스탬프와 위치를 잘못 짝지을 수 있는 크기가 나온다.
| 배속 | 지연 항 RTF × L | 위치로 옮기면 v × RTF × L |
|---|---|---|
| RTF 1 | 0.005 × 1 = 0.005 s | 0.01 m |
| RTF 10 | 0.005 × 10 = 0.05 s | 0.1 m |
| RTF 0.5 | 0.005 × 0.5 = 0.0025 s | 0.005 m |
같은 5 ms라도 10배속에서는 시뮬레이션 시간으로 10배가 되고, 0.5배속에서는 절반이 된다. 0.1 m는 곱셈 결과이며 어떤 로봇에서 잰 위치 오차가 아니다.
이 결과는 배속을 목적에 따라 고르라는 설계 문서의 사용 사례와 맞물린다. 문서는 빠른 시뮬레이션을 고수준 테스트와 반복 시스템 테스트에, 느린 시뮬레이션을 속도보다 정확도가 중요한 복잡한 시스템에 쓴다고 나눈다(설계 문서 L42-L50). 10배속은 수많은 시나리오를 빨리 돌리는 데는 유리하지만, 타임스탬프 정밀도가 판정 기준인 테스트라면 그만큼 불리하다.
/clock 틱 사이에서는 노드의 시계가 멈춰 있다
두 번째 오차는 /clock이 연속으로 흐르지 않고 한 번씩 끊어서 온다는 데서 생긴다. /clock 한 번의 발행을 틱(tick)이라고 부르자. 설계 문서에 따르면 기본 time source에는 틱 사이 시각을 추정하는 보간(interpolation) 기능이 없다. 재생이나 시뮬레이션이 언제든 멈출 수 있어서, 시간이 계속 같은 속도로 흐른다는 가정이 깨지기 때문이다. 더 높은 정확도가 필요하면 시간 진행을 늦추거나 /clock 발행 빈도를 올리라고 하고, 이 조정은 정확도와 계산량·대역폭을 맞바꾸는 일이라고 적는다(설계 문서 L125-L132).
그래서 노드의 시계는 계단처럼 움직인다. 한 틱이 오면 시각이 확 뛰고, 다음 틱까지는 그대로다. 한 계단의 높이, 곧 한 틱 동안 흐르는 시뮬레이션 시간이 틱 항이다.
틱 항 = RTF ÷ f (f는 실제 초 기준 Hz)
지연 항 = RTF × L (L은 실제 초)
f가 실제 초 기준이어야 이 식이 맞는다. rosbag2는 /clock을 create_wall_timer, 즉 벽시계 기준 타이머로 발행한다(player.cpp L1715-L1723). 벽시계로 1/f초마다 한 번 보내는 사이에 시뮬레이션은 RTF/f초만큼 진행한다. ros2 bag play의 --clock은 값을 주면 그 빈도로, 값 없이 주면 40 Hz로 /clock을 발행하고, 옵션을 빼면 발행하지 않는다. 이 옵션을 쓰면 bag 안에 녹화된 /clock 토픽은 재생에서 빠진다(play.py L109-L112). rolling과 jazzy 브랜치에서 같은 정의를 확인했고, 다른 배포판은 확인하지 않았다.
아래는 식에 값을 넣어 보기 위한 설정 예시다. 이 글에서 실행한 명령이 아니다.
# 설명용 설정 예시 (실행하지 않음, 지연 L = 5 ms 가정)
ros2 bag play <bag> --rate 10 --clock 100
# 틱 항 = 10 ÷ 100 = 0.1 s (시뮬레이션 시간)
# 지연 항 = 10 × 0.005 = 0.05 s
# 합계 = 0.15 s
--clock에 값을 주지 않으면 40 Hz이므로 같은 10배속에서 틱 항은 10 ÷ 40 = 0.25 s로 커진다.

계단 그림에서 보이듯 틱 항은 틱 직후 0에 가깝고 다음 틱 직전에 RTF ÷ f로 가장 크다. 예제는 이 최악값을 쓴다. 평균으로 보면 그 절반쯤이지만, 이것도 측정이 아니라 톱니 모양을 가정한 해석이다.
틱 항이 어떻게 드러나는지는 다른 사람의 보고로 볼 수 있다. rclcpp 이슈 #1239(2020년 7월 등록, 같은 해 8월 “버그 아님”으로 종료)에서는 ROS 2 Foxy와 Gazebo(RTF 0.9~1.0) 환경에서 200 Hz 콜백이 get_clock()->now()로 시간 차이를 구했더니 0이 자주 나왔다. /clock이 10 Hz로 발행되고 있었기 때문이다. Gazebo 유지보수자는 기본 clock 빈도가 10 Hz이고 publish_rate 파라미터로 바꿀 수 있다고 답했고, 시뮬레이션이 생각하는 정확한 시각을 받는 것이 중요하다며 자동 보간에는 반대했다. 해당 플러그인 코드에도 DEFAULT_PUBLISH_FREQUENCY = 10.(gazebo_ros_init.cpp L107)과 실제 시간 기준 스로틀(L192-L201)이 있다. 모델에 넣으면 RTF 1·10 Hz의 틱 항은 0.1 s이고, 200 Hz 콜백은 한 계단 안에서 스무 번쯤 같은 시각을 읽게 된다. 이것은 Gazebo Classic 연동 플러그인의 2020년 고정 커밋 기준이며, 현재 Gazebo(gz-sim)의 기본값으로 일반화할 수 없다.
틱 항을 물리 엔진의 계산 간격과 섞으면 안 된다. SDFormat의 max_step_size(기본 0.001)는 시뮬레이터가 운동을 몇 초 간격으로 다시 계산하는지이고, /clock 틱은 계산된 시각을 다른 노드에 얼마나 자주 알려 주는지다. 계산 간격이 위치 오차에 미치는 영향은 로봇 시뮬레이션의 시간 간격을 줄이면 위치 계산은 왜 달라질까에서 따로 다뤘다.
두 항을 더해 보면 먼저 손댈 쪽이 정해진다
두 항을 단순히 더하면 다음과 같다.
합계 = RTF × L + RTF ÷ f = RTF × (L + 1/f)
식을 이렇게 묶어 놓으면 세 가지가 한눈에 보인다. 두 항 모두 RTF에 비례하므로 배속을 낮추면 둘 다 줄어든다. 발행 빈도 f는 괄호 안의 1/f만 줄이고 L에는 손대지 못한다. 그리고 두 항이 같아지는 빈도는 L = 1/f, 즉 f = 1/L이다. L이 5 ms면 200 Hz이고, 이 경계는 RTF와 상관없다.
RTF 10, L 5 ms에서 빈도만 바꿔 보면 판단이 뒤집히는 지점이 드러난다.
- 100 Hz: 틱 항 0.1 s, 지연 항 0.05 s, 합계 0.15 s. 200 Hz보다 낮으니 틱 항이 크다. 여기서는 빈도를 올리는 것이 먼저다.
- 1000 Hz: 틱 항 0.01 s, 지연 항 0.05 s, 합계 0.06 s. 이제 지연 항이 크다. 빈도를 더 올려도 0.05 s 아래로는 내려가지 않고, 이를 줄이려면 RTF를 낮춰야 한다.
“/clock 빈도를 올려도 소용없다”는 말은 두 번째 경우에만 맞는다. 첫 번째 경우에는 빈도를 올리는 쪽이 합계를 훨씬 크게 줄인다. 설계 문서가 정확도를 높이는 수단으로 든 두 가지(시간 진행 늦추기, 발행 빈도 올리기)와 RTF를 낮추라는 권고를 각각 어느 항에 작용하는지로 나눈 것은 이 글의 해석이다.
허용 오차 E가 주어지면 최대 배속도 거꾸로 구할 수 있다. 지연 항만 보면 E ÷ L = 0.02 ÷ 0.005 = 4다. 하지만 4는 빈도를 한없이 올려 틱 항이 0이 됐을 때의 극한값이다. 틱 항까지 넣은 한도 E ÷ (L + 1/f)는 빈도에 따라 달라진다.
/clock 빈도 f |
1/f | 틱 포함 최대 RTF = 0.02 ÷ (0.005 + 1/f) |
|---|---|---|
40 Hz (--clock 기본값) |
0.025 s | 약 0.67 |
| 100 Hz | 0.01 s | 약 1.33 |
| 1000 Hz | 0.001 s | 약 3.33 |
| 무한대(극한) | 0 | 4 |
40 Hz 기본값을 그대로 두면 이 오차 한도에서는 실시간(RTF 1)조차 넘는다. 이런 설정이라면 배속을 논하기 전에 빈도부터 올려야 한다.
지연 항, 위치, 지연 항만 본 최대 RTF 4, 10배속에서의 100 Hz·1000 Hz 틱 항, 1000 Hz 합계 0.06 s까지 아홉 개 사례는 공개 고정 예제 example.py에 들어 있다. Python 3.12.3에서 실행한 출력과 종료 코드 0은 verification.json에 남아 있고, 이 기록의 범위도 로봇이나 시뮬레이터 실행이 아닌 산술로 적혀 있다. 2026년 9월 25일에 같은 예제를 다시 실행해 아홉 개 출력이 이 기록과 일치함을 확인했다. 합계 0.06은 부동소수점 표기로 0.060000000000000005가 찍히지만 수학적으로는 0.06이다. 교차 빈도 200 Hz, 100 Hz 합계 0.15 s, 틱을 포함한 최대 RTF 표는 고정 예제에 없는 추가 계산이며, 분수 연산으로 따로 검산했다.
확인 문제: 이 설정이라면 어느 쪽을 건드릴까
지연은 모두 실제 초, 오차는 시뮬레이션 초 기준이다. 답을 보기 전에 두 항을 먼저 적어 보자.
1. L = 10 ms, RTF 5, f = 100 Hz. 두 항은 각각 얼마이고, 어느 쪽이 큰가?
지연 항 5 × 0.01 = 0.05 s, 틱 항 5 ÷ 100 = 0.05 s로 같고 합계는 0.1 s다. 이 설정에서는 f가 정확히 1/L = 100 Hz이기 때문이다. 빈도를 올리든 배속을 낮추든 줄어들지만, 빈도는 틱 항 하나만 줄이고 배속을 낮추면 두 항이 함께 줄어든다.
2. 허용 오차 0.02 s, L = 5 ms, f = 1000 Hz. 지연 항만 보고 구한 최대 배속 4로 돌려도 되는가?
안 된다. 합계가 4 × (0.005 + 0.001) = 0.024 s로 한도를 넘는다. 틱 항까지 넣은 한도는 0.02 ÷ 0.006, 약 3.33이다. 한도를 계산할 때 틱 항을 빼면 이렇게 조금씩 초과한다.
3. L = 5 ms에서 ros2 bag play <bag> --rate 10 --clock처럼 빈도 값 없이 재생했다. 무엇부터 바꿀까?
값을 주지 않았으니 f는 40 Hz다. 틱 항 10 ÷ 40 = 0.25 s, 지연 항 0.05 s, 합계 0.3 s다. f가 교차 빈도 200 Hz보다 한참 낮으므로 빈도를 올리는 쪽이 먼저다. 빈도를 200 Hz 위로 올린 뒤에도 한도를 넘는다면 그때부터는 배속을 낮춰야 한다.
이 어림 계산이 맞지 않는 경우와 다음에 잴 것
이 모델은 설계 문서의 “비례한다”를 계산할 수 있는 모양으로 바꾸려고 가정을 여러 개 얹었다. 비례계수 1, 고정된 지연, 두 항의 단순한 합, 틱 항의 최악값이 모두 가정이다. 설계 문서 자체도 규범 명세나 배포판별 동작 보증이 아닌 설계 해설이며, 여기서는 고정 커밋 12f61b1(해당 파일의 마지막 변경은 2021년 10월) 기준으로 인용했다.
모델에 없는 요소도 있다. 지연이 매번 흔들리는 지터(jitter), DDS와 QoS 설정, executor가 콜백을 처리하는 시간, 발행 측의 처리 지연, 시뮬레이터 내부 step은 넣지 않았다. RTF 10을 목표로 설정해도 실제 달성 배율이 낮으면 계산에 넣을 RTF도 달라진다. /clock을 시뮬레이션 시간 기준으로 발행하는 도구라면 한 틱의 간격은 RTF와 무관한 1/f_sim이 되므로 틱 항의 식부터 바꿔야 한다. 실제 시간 기준 발행을 확인한 것은 rosbag2와 Gazebo Classic 플러그인뿐이고, gz-sim이나 Isaac Sim의 발행 기준과 기본값은 확인하지 않았다. rosbag2에는 특정 토픽이나 모든 재생 메시지 직전에 /clock을 갱신하는 --clock-topics, --clock-topics-all 옵션도 있어 틱 항을 메시지 시점에 맞추는 대안이 될 수 있지만, 효과는 검증하지 않았다.
실물 로봇으로 넘어갈 때는 경계가 분명하다. 이 글의 수치는 모두 산술이므로 실물의 안전이나 성능을 보장하지 않는다. 배속 시뮬레이션용 use_sim_time 설정을 실물 로봇 제어 노드로 그대로 옮기지 않는다. 설계 문서도 하드웨어 타임아웃이 있는 주변장치를 다루는 드라이버에는 SteadyTime을 예로 든다.
자기 설정에 적용하려면 세 값을 실제로 확인하는 데서 시작한다. /clock의 실제 빈도는 ros2 topic hz /clock 같은 방법으로 볼 수 있을 것이고, 지연 L은 따로 측정해야 한다. 이 두 측정 방법은 이 글에서 실행해 확인하지 않았다. 값을 얻었다면 RTF × L과 RTF ÷ f를 나란히 적고 큰 쪽부터 줄인다. f가 1/L보다 낮으면 빈도를, 이미 높으면 배속을 조정한다. 계산값과 실제 그래프에서 잰 타임스탬프 차이가 얼마나 벌어지는지는 아직 비교하지 않았고, 그 비교가 이 모델의 다음 검증 항목이다.
참고 링크
모두 2026년 9월 25일에 확인했다.
- ROS 2 Design — Clock and Time (고정 커밋 12f61b1), 공개 HTML 페이지
- rclcpp time_source.cpp @05de6c8
- rosbag2 ros2bag play.py @1ef3269, rosbag2 player.cpp @08780f9
- SDFormat physics.sdf 1.11 @91876e7
- gazebo_ros_pkgs gazebo_ros_init.cpp @e1de3c1, rclcpp 이슈 #1239
- 이 글의 산술 예제: example.py, verification.json
이 글에는 제휴·협찬이 없다.