rclpy.time.Time()과 Node.get_clock().now()는 뭐가 다를까: Time() 기본값 SYSTEM_TIME과 from_msg 기본값 ROS_TIME이 섞일 때 TypeError가 나는 지점 확인하기
rclpy 노드에서 시각이 필요할 때 rclpy.time.Time()을 써야 할까, Node.get_clock().now()를 써야 할까. 2023년 10월 Robotics Stack Exchange에 같은 질문이 올라왔다. 작성자는 ROS 2 Humble과 Gazebo 11 환경에서 Time()은 시뮬레이터의 /clock과 같은 값처럼 보이고, get_clock().now()는 컴퓨터의 “절대” 시각처럼 보인다고 적었다. 그런데 Time()은 현재 시각을 읽는 함수가 아니다. 0 나노초짜리 시각 객체를 새로 만드는 생성자이고, 지금 값을 돌려주는 쪽은 get_clock().now()뿐이다.
둘을 섞었을 때 생기는 문제는 숫자보다 객체에 붙은 꼬리표에서 온다. rclpy의 시각 객체에는 시계 종류(clock type)가 함께 저장되는데, 인자 없는 Time()의 기본값은 SYSTEM_TIME이고 메시지에서 시각을 되살리는 Time.from_msg()의 기본값은 ROS_TIME이다. 시계 종류가 다르면 뺄셈과 크기 비교가 값과 상관없이 TypeError로 끝난다. ROS 2 Jazzy rclpy 소스를 2026년 10월 10일에 줄 단위로 읽어 이 규칙을 세웠고, 손 계산 예제 하나와 Jazzy 컨테이너에서 5회 반복한 실측으로 확인했다. 실측은 소스에서 세운 예측과 모두 같았다. 시뮬레이터나 실제 로봇 없이 rclpy만 실행한 확인이며, 타이머 콜백 하나가 이 예외로 멈추는 장면까지 재현했다.
따라오는 데는 토픽 발행·구독 정도의 rclpy 경험, Python의 ==가 클래스의 __eq__ 메서드로 정해진다는 사실, 나눗셈의 몫(//)과 나머지(%)면 충분하다.
Time()은 지금을 읽지 않는다
rclpy의 Time은 두 가지를 함께 담은 값이다. 하나는 원점(epoch, 시각을 세기 시작한 기준점)에서 몇 나노초가 지났는지이고, 다른 하나는 그 숫자를 어느 시계로 셌는지다. 소스의 docstring도 Time을 “epoch 이후의 기간과 시계 종류의 조합”으로 정의한다(time.py L33-L41).
시계 종류는 세 가지가 쓰인다.
- 시스템 시간(
SYSTEM_TIME, system time): 운영체제의 달력 시각이다. 시각 보정 때문에 값이 불연속으로 건너뛸 수 있다. - ROS 시간(
ROS_TIME, ROS time): 노드들이 함께 쓰는 논리 시각이다.use_sim_time파라미터가 꺼져 있으면 시스템 시각과 같은 값을 보고하고, 켜지면/clock토픽으로 들어온 시뮬레이션 시각을 보고한다(ROS 2 design L94-L98). - 단조 시간(
STEADY_TIME, steady 또는 monotonic time): 뒤로 가지 않고 경과 시간만 세는 시계다.
Time()을 인자 없이 부르면 생성자 기본값 seconds=0, nanoseconds=0, clock_type=ClockType.SYSTEM_TIME이 그대로 들어간다(time.py L43-L47). 언제 호출해도 결과는 “시스템 시계 기준 0 나노초”다. 반면 노드는 생성될 때 ROSClock을 만들어 붙이고(node.py L222-L223), get_clock()은 그 시계를 그대로 돌려준다(node.py L349-L351). 그 시계의 now()가 비로소 현재 값을 읽는다.
이렇게 보면 질문자의 관찰도 풀린다. 시뮬레이션이 정지 상태로 시작해 /clock이 0이었다면, 항상 0인 Time()과 우연히 같아 보였을 것이다. get_clock().now()가 컴퓨터 시각처럼 보였다는 것은 그 노드에 use_sim_time이 켜져 있지 않았다는 신호로 읽힌다. 질문자 환경을 직접 본 것은 아니므로 여기까지는 해석이다. 이 질문에 달린 유일한 답변(점수 1, 미채택)도 Time()이 0 시각이라고 설명하고 노드 시계를 쓰라고 권한다. 다만 그 답변은 파라미터 이름을 use_simulated_time으로 적었는데, rclpy가 읽는 이름은 use_sim_time이다(time_source.py L32).
비유로 옮기면 “서울 10:00″과 “도쿄 10:00″에 가깝다. 숫자가 같아도 기준이 다르면 같은 순간인지 알 수 없다. rclpy는 여기서 한 걸음 더 나가 기준이 다르면 비교 자체를 거부한다. 비유가 맞지 않는 지점도 있다. 시간대는 정해진 시차로 환산할 수 있지만, 시뮬레이션 중인 ROS 시간과 시스템 시간 사이에는 일반적인 환산식이 없다. rclpy가 자동 변환을 시도하지 않는 이유도 여기 있다고 본다. 공식 문서가 그렇게 설명하지는 않으므로 이 부분은 편집자 해석이다.
같은 Time 클래스인데 만드는 길마다 기본 시계가 다르다
헷갈리기 쉬운 지점은 Time 클래스 하나가 생성 경로에 따라 다른 시계 종류를 갖는다는 사실이다. ROS 2 Jazzy rclpy(jazzy 브랜치, package.xml 7.1.12, 커밋 4e607c0) 기준으로 표에 모았다.
| 만드는 코드 | 기본 시계 종류 | 소스 근거 |
|---|---|---|
Time(), Time(seconds=…) |
SYSTEM_TIME |
time.py L43-L47 |
Time.from_msg(msg) |
ROS_TIME |
time.py L166-L180 |
Clock().now() |
SYSTEM_TIME |
clock.py L184-L193, L212-L216 |
node.get_clock().now() |
ROS_TIME |
clock.py L340-L345 |
t + Duration(…), t - Duration(…) |
왼쪽 t의 종류 유지 |
time.py L85-L94, L99-L115 |
노드 시계가 use_sim_time 설정과 상관없이 ROS_TIME이라는 점도 표에서 읽어야 한다. use_sim_time은 시간원(time source, 노드의 ROS 시계에 /clock 값을 공급하는 장치)이 ROS 시간 덮어쓰기(override)를 켤지만 정한다(time_source.py L87-L93, L115-L120). 바뀌는 것은 값의 출처이지 시계 종류가 아니다.
기간(duration, rclpy.duration.Duration)은 두 시각의 차이를 나타내는 값이고 시계 종류가 없다. 그래서 Time ± Duration은 검사 없이 왼쪽 시각의 종류를 물려받는다. 섞으면 무조건 예외가 나는 것은 아니다.

1.5초를 메시지로 보냈다가 되돌리면 비교할 수 없게 된다
기본값 차이가 실제 코드에서 어떻게 드러나는지 손으로 계산해 보자. 시스템 시계 기준 1.5초짜리 시각 B = Time(seconds=1.5)를 만들고, 메시지로 바꿨다가 다시 시각 객체로 되돌리는 예제다.
- 생성자는
int(seconds * S_TO_NS) + int(nanoseconds)로 나노초를 만든다(time.py L54-L55).S_TO_NS는 정수 10⁹이다(constants.py L15). 그래서B는 int(1.5 × 10⁹) = 1500000000 ns, 시계 종류는SYSTEM_TIME이다. B.to_msg()는 나노초를 몫과 나머지로 쪼개builtin_interfaces/msg/Time의sec,nanosec에 넣는다(time.py L67-L74, L157-L164). 1500000000 // 10⁹ = 1, 1500000000 % 10⁹ = 500000000이다.C = Time.from_msg(msg)는 1 × 10⁹ + 500000000 = 1500000000 ns를 복원한다. 시계 종류 인자를 주지 않았으니ROS_TIME이 붙는다.B와C의 나노초 차이는 0이다. 그런데B == C는True도False도 아닌TypeError가 되어야 한다. 시계 종류가SYSTEM_TIME과ROS_TIME으로 갈렸기 때문이다.
함께 기억할 계산이 하나 더 있다. Time(seconds=2, nanoseconds=500000000)은 두 인자를 더해 2500000000 ns가 된다. nanoseconds가 초 아래 자리를 채우는 값이 아니라 합산되는 값이라는 점은 생성자 식에서 바로 보인다.
이 계산은 같은 값을 사칙연산으로 다시 계산한 example.py와 8개 기대값을 기록한 verification.json으로 Python 3.12.3에서 산술 검산했다. example.py는 int()와 몫(//) 대신 1.5 * 1000000000, (1500000000 - 500000000) / 1000000000 같은 식으로 같은 값을 다시 구한다. 이 단계는 사칙연산 검산일 뿐 rclpy를 실행한 것이 아니다. rclpy가 정말 이렇게 동작하는지는 다음 절의 실측이 맡는다.
공식 테스트는 이 경우를 비켜 간다. rclpy의 메시지 왕복 테스트는 원본 시각을 처음부터 ROS_TIME으로 만든 뒤 왕복시켜 ==를 통과시킨다(test_time.py L191-L202). 기본값 SYSTEM_TIME 객체를 왕복시키는 테스트는 없다. 입문자 코드는 대개 그 빈칸에서 출발한다.
ROS 2 Jazzy 컨테이너에서 rclpy로 확인한 결과
예측을 실제 rclpy에 대 보기 위해 스크립트 하나로 시각 객체 8개를 만들고 연산 22개를 하나씩 실행했다. 예외가 나면 예외 클래스와 메시지를 그대로, 나지 않으면 결과의 타입·시계 종류·나노초를 JSON 한 줄씩 기록한다. use_sim_time을 켠 노드에는 같은 프로세스의 다른 노드가 /clock에 42초 250000000 ns를 직접 발행했다. 마지막으로 rosbridge 사례(아래 절)를 본뜬 타이머 콜백 두 변형을 돌렸다.
재현 스크립트와 실행별 원본 출력은 실험 폴더에 공개돼 있다. 파일 단위로는 experiment.py와 results.json이다. Docker가 있는 Linux 셸에서 다음 명령으로 같은 조건을 재현할 수 있다. 이미지는 움직이는 태그 대신 실측에 쓴 고정 digest로 지정했다.
mkdir rclpy-clock-type && cd rclpy-clock-type
curl -fsSLO https://raw.githubusercontent.com/sungpyo9053/blog/284cd5f79d7fbb48b1da2b16d2fb108d7bcb01f8/experiments/rclpy-time-default-clock-type-from-msg-mismatch/experiment.py
sha256sum experiment.py
# 기대값: ec92d0d98a882989d78f14e755102c3b4acc3fd3861940c4ec23f16d6b5ef9fb
docker run --rm --network none --cpus 1 --memory 1g -v "$PWD":/work ros@sha256:066420e07f60aa18262f2479981def87ebcfcec42eefb0c0c57c4a46098348ca bash -c "source /opt/ros/jazzy/setup.bash && python3 /work/experiment.py"
정상이라면 종료 코드 0과 함께 "kind": "done" 줄로 끝나는 JSON 줄들이 나온다. 시스템 시각이 들어간 값은 실행할 때마다 달라지므로, 비교할 것은 시계 종류, 예외 여부와 문구, 고정 입력에서 나온 나노초다.
측정 환경은 ros:jazzy-ros-base 이미지(digest 고정), Python 3.12.3, rclpy 7.1.12(데비안 패키지 7.1.12-1noble.20260902.053513), RMW(ROS middleware, 노드 사이 메시지를 실제로 주고받는 하위 통신 구현)는 기본값인 rmw_fastrtps_cpp였다. 네트워크 없이 CPU 1개, 메모리 1 GiB로 제한했고, 하네스가 같은 스크립트를 5회 실행해 5회 모두 종료 코드 0이었다. 실행일은 2026년 10월 10일이다. 시스템 시각에서 나온 값을 빼면 다섯 실행의 시계 종류·예외·나노초 출력은 한 줄도 다르지 않았다.
먼저 시각 객체다. 왼쪽이 소스에서 세운 예측, 오른쪽이 실측이다.
| 객체 | 만드는 코드 | 예측 | 실측(5회 동일) |
|---|---|---|---|
| A | Time() |
SYSTEM_TIME, 0 | SYSTEM_TIME, 0 |
| B | Time(seconds=1.5) |
SYSTEM_TIME, 1500000000 | SYSTEM_TIME, 1500000000 |
| B2 | Time(seconds=2, nanoseconds=500000000) |
SYSTEM_TIME, 2500000000 | SYSTEM_TIME, 2500000000 |
| C | Time.from_msg(B.to_msg()) |
ROS_TIME, 1500000000 | ROS_TIME, 1500000000 |
| G | Time(seconds=1.5, clock_type=node.get_clock().clock_type) |
ROS_TIME, 1500000000 | ROS_TIME, 1500000000 |
| D | node.get_clock().now() (use_sim_time false) |
ROS_TIME, 시스템 시각 | ROS_TIME, 시스템 시각 |
| E | sim.get_clock().now() (use_sim_time true, /clock 수신 전) |
ROS_TIME, 0 | ROS_TIME, 0 |
| E2 | 같은 노드, /clock 수신 후 |
ROS_TIME, /clock 값 |
ROS_TIME, 42250000000 |
| F | Clock().now() |
SYSTEM_TIME, 시스템 시각 | SYSTEM_TIME, 시스템 시각 |
B.to_msg()는 sec=1, nanosec=500000000, B2.to_msg()는 sec=2, nanosec=500000000으로 손 계산과 같았다. 두 노드의 시계 클래스는 모두 ROSClock, 시계 종류는 ROS_TIME이었고, use_sim_time을 켠 노드의 ROS 시간 덮어쓰기는 true로 기록됐다. /clock은 첫 발행 1건에 반영됐다.
연산 결과는 다음과 같다.
| 연산 | 예측 | 실측(5회 동일) |
|---|---|---|
B == C, B != C, B < C, B <= C, B > C, B >= C |
TypeError | 6개 모두 TypeError: Can't compare times with different clock types |
B - C |
TypeError | TypeError: Can't subtract times with different clock types |
B.nanoseconds == C.nanoseconds |
True | True |
D - A, A < D |
TypeError | TypeError(각각 subtract, compare 문구) |
A == E (둘 다 0 ns) |
TypeError | TypeError, A.nanoseconds == E.nanoseconds는 True |
C == G |
True | True |
D - C, D - E |
Duration | Duration, 예외 없음 |
F - D |
TypeError | TypeError |
B + Duration(seconds=1) |
SYSTEM_TIME 유지 | SYSTEM_TIME, 2500000000 |
C - Duration(seconds=1) |
ROS_TIME 유지 | ROS_TIME, 500000000 |
A == None / A < None |
False / Python 일반 TypeError | False / '<' not supported between instances of 'Time' and 'NoneType' |
Time(1, 500000000) |
TypeError | Time.__init__() takes 1 positional argument but 3 were given |
Time.from_msg(msg, clock_type=ClockType.SYSTEM_TIME) == B |
True | True |
E2 - A / E2 - C |
TypeError / Duration | TypeError / Duration 40750000000 |
타이머 단계에서는 0.05초 주기 타이머 콜백이 now() - last_used를 계산하게 했다. last_used = Time()인 변형은 다섯 실행 모두 콜백에 1번 들어가 0번 완료됐고, TypeError: Can't subtract times with different clock types가 spin_once 밖으로 빠져나왔다. last_used = get_clock().now()인 변형은 다섯 실행 모두 콜백 5번을 예외 없이 완료했다. 실행별 최소 경과 시간은 50334404 ns에서 50381457 ns 사이였다.
1회차 원본 출력에서 판단을 가른 줄만 그대로 옮긴다.
{"kind": "op", "label": "make_B", "expr": "Time(seconds=1.5)", "raised": false, "result": {"type": "Time", "clock_type": "SYSTEM_TIME", "nanoseconds": 1500000000, "repr": "Time(nanoseconds=1500000000, clock_type=SYSTEM_TIME)"}}
{"kind": "msg", "expr": "Time(seconds=1.5).to_msg()", "sec": 1, "nanosec": 500000000}
{"kind": "op", "label": "make_C", "expr": "Time.from_msg(Time(seconds=1.5).to_msg())", "raised": false, "result": {"type": "Time", "clock_type": "ROS_TIME", "nanoseconds": 1500000000, "repr": "Time(nanoseconds=1500000000, clock_type=ROS_TIME)"}}
{"kind": "op", "label": "B==C", "expr": "B == C", "raised": true, "exception": "TypeError", "message": "Can't compare times with different clock types"}
{"kind": "op", "label": "B.ns==C.ns", "expr": "B.nanoseconds == C.nanoseconds", "raised": false, "result": {"type": "bool", "value": true}}
{"kind": "op", "label": "A==E", "expr": "A == E # both 0 ns?", "raised": true, "exception": "TypeError", "message": "Can't compare times with different clock types"}
{"kind": "op", "label": "D-C", "expr": "D - C", "raised": false, "result": {"type": "Duration", "nanoseconds": 1791610548803266206}}
{"kind": "timer", "variant": "Time() cache", "last_used": {"type": "Time", "clock_type": "SYSTEM_TIME", "nanoseconds": 0, "repr": "Time(nanoseconds=0, clock_type=SYSTEM_TIME)"}, "callbacks_entered": 1, "callbacks_completed": 0, "min_age_ns": null, "exception_escaped_spin_once": {"exception": "TypeError", "message": "Can't subtract times with different clock types"}}
{"kind": "timer", "variant": "now() cache", "last_used": {"type": "Time", "clock_type": "ROS_TIME", "nanoseconds": 1791610550373011371, "repr": "Time(nanoseconds=1791610550373011371, clock_type=ROS_TIME)"}, "callbacks_entered": 5, "callbacks_completed": 5, "min_age_ns": 50334404, "exception_escaped_spin_once": null}
측정에서 확인된 것은 예측의 확정이다. 새로 드러난 동작은 없었다. 그래도 실측이 더한 정보는 있다. A == E처럼 양쪽이 모두 0 나노초여도 예외가 난다는 것, 즉 값이 아니라 시계 종류가 비교 가능 여부를 가른다는 것을 실제 출력으로 볼 수 있다. use_sim_time을 켠 노드의 시계도 ROS_TIME이었으므로, 시뮬레이션 설정을 바꾼다고 이 예외가 사라지거나 생기지는 않는다.
예외가 나는 연산은 일곱 개, 나지 않는 연산도 있다
소스를 다시 보면 규칙이 짧다. 시계 종류가 다르면 __sub__(Time − Time), __eq__, __lt__, __le__, __gt__, __ge__ 여섯 메서드가 직접 TypeError를 던진다(time.py L99-L115, L117-L127, L129-L155). __ne__는 not self.__eq__(other)로 구현돼 같은 예외가 그대로 올라온다. 연산자 기준으로 세면 -, ==, !=, <, <=, >, >= 일곱 개다.
예외가 나지 않는 경우도 같은 소스에서 나온다. 첫째, Time ± Duration은 시계 종류를 검사하지 않고 왼쪽 시각의 종류를 유지한다. 둘째, 상대가 Time이 아니면 rclpy 메서드는 NotImplemented를 돌려주고, Python 기본 규칙에 따라 ==는 False가 된다. <는 Python 자체의 TypeError가 나는데 문구가 rclpy 것과 다르다. 공식 테스트도 time1 == 1이 False, time1 > duration이 TypeError임을 확인한다(test_time.py L148-L160). None과의 ==가 예외 없이 False가 된 것은 2022년에 보고된 rclpy 이슈 #952가 2023년에 해결된 결과다.
널리 참조되는 ros2_cookbook의 rclpy 시간 변환표는 이 맥락에서 두 군데를 조심해서 읽어야 한다. 표의 Time(nanoseconds=t)와 Time.from_msg(t)는 서로 다른 시계 종류를 만들지만 표에는 그 차이가 드러나지 않는다. 또 float를 Time으로 바꾸는 칸의 Time(int(sec), int(nano * 1e9))(L16)는 위치 인자 호출인데, Jazzy의 Time 생성자는 키워드 전용 인자만 받는다. 같은 형태인 Time(1, 500000000)은 실측에서 TypeError로 끝났다.
설계 문서와 구현의 표현 차이도 짚어 둔다. ROS 2 설계 문서는 SystemTime과 ROSTime을 서로 바꿔 쓸 수 있는(interchangeable) 것으로 묘사하고, 비교 가능성은 런타임 검사로 확인한다고 쓴다(design L148-L152). rclpy 구현은 SYSTEM_TIME과 ROS_TIME의 조합도 거부한다. use_sim_time이 꺼져 두 값이 같은 시스템 시각에서 나왔더라도 마찬가지다. 설계 문장이 런타임 검사에서 무엇을 허용하는지 명시하지 않으므로 설계 문서가 틀렸다고 단정할 근거는 없다. 코드를 쓸 때 기준은 구현과 테스트다.
타이머 콜백에서 터지면 그 주기 작업이 멈춘다
이 실수는 실제 패키지에서도 났다. 웹과 ROS를 잇는 rosbridge_suite의 rosapi는 캐시한 파라미터 클라이언트의 last_used_time을 Time()으로 초기화했다(수정 전 params.py L160). 정리 타이머는 _node.get_clock().now()에서 그 값을 뺐다(L172-L176). 수정 커밋 aa9a7a3의 메시지는 이 뺄셈이 0.5초마다 TypeError를 냈다고 적는다. 2026년 8월 17일 병합된 PR #1308과 Jazzy 백포트 PR #1314가 이 값을 time.monotonic()으로 바꿨다. HuntLab이 겪은 일이 아니라 외부 저장소의 공개 기록이다.
위 실측의 타이머 단계가 이 패턴을 줄인 재현이다. 실행기(executor, 대기 중인 콜백을 꺼내 실행하는 루프)인 SingleThreadedExecutor의 spin_once를 반복 호출했더니, Time() 기준 변형은 첫 콜백에서 예외가 spin_once 밖으로 나왔다. 이 예외를 잡지 않는 노드라면 spin 루프가 거기서 끝난다고 보는 것이 자연스럽다. 다만 rclpy.spin()이나 멀티스레드 실행기에서의 동작은 측정하지 않았다. 로봇 쪽으로 옮겨 보면, 경과 시간으로 명령 timeout이나 센서 신선도를 판정하는 콜백이 여기서 멈출 수 있다는 뜻이다.
고치는 방법은 기준 시각을 어떤 시계로 잴지 정하는 데서 갈린다. 아래 줄들은 모두 실측 스크립트에서 같은 형태로 실행해 결과를 확인한 것이다.
# 고치기 전: 기준 시각이 SYSTEM_TIME 0 ns라서 노드 시계와 뺄 수 없다
self.last_used = Time()
age = self.get_clock().now() - self.last_used # TypeError
# 지금을 기준으로 잡을 때는 노드 시계에서 읽는다
self.last_used = self.get_clock().now()
# 숫자로 기준 시각을 만들 때는 비교 상대의 시계 종류를 명시한다
start = Time(seconds=1.5, clock_type=self.get_clock().clock_type)
# 메시지를 되살릴 때도 비교 상대에 맞춰 시계 종류를 줄 수 있다
restored = Time.from_msg(msg, clock_type=ClockType.SYSTEM_TIME)
# "아직 설정되지 않았다(0)"만 보고 싶다면 시계 종류와 무관한 정수 비교를 쓴다
if self.last_used.nanoseconds == 0:
...
마지막 줄은 실측의 A.nanoseconds == E.nanoseconds가 시계 종류가 달라도 True를 돌려준 것과 같은 원리다. C++ 쪽에서 0 시각과 now()의 비교 예외를 완화하자는 제안(rclcpp 이슈 #2113)에 달린 답변이 nanoseconds()로 0을 검사하라고 권한 것도 같은 방향이다.
rosbridge가 고른 단조 시계는 성격이 다른 선택이다. 캐시 수명처럼 ROS 시간이 멈추거나 되감겨도 실제로 흐른 시간을 재야 하는 값이라면 time.monotonic()이 시계 종류 혼동과 시스템 시각 점프를 함께 피한다. ROS 2 설계 문서도 주변기기의 하드웨어 timeout을 다루는 드라이버를 steady 시계의 사용 예로 든다(design L100-L104). 반대로 시뮬레이션 배속에 맞춰 흘러야 하는 제어 주기나 메시지 스탬프와의 비교에는 ROS 시간이 맞다. 어느 쪽이 옳은지는 그 값이 무엇의 수명인지에 달렸고, 이것을 보편 규칙으로 정할 근거는 없다. 세 시계를 언제 쓰는지는 시뮬레이션 시간이 뒤로 갈 때 타이머 계산이 깨지는 과정을 다룬 글에서 더 자세히 다뤘다.
답을 가리고 풀어 볼 세 가지 코드
1. Time(seconds=2, nanoseconds=500000000).to_msg()의 sec와 nanosec은?
답은 2와 500000000이다. 생성자가 두 인자를 더해 2500000000 ns를 만들고, to_msg()는 2500000000 // 10⁹ = 2, 2500000000 % 10⁹ = 500000000으로 나눈다. nanoseconds=500000000을 “0.5초를 따로 적은 값”으로 읽으면 sec를 2.5로 착각하기 쉽다.
2. 구독 콜백에서 Time.from_msg(msg.header.stamp) - self.start가 TypeError를 냈다. self.start = Time()이었다면?
from_msg는 기본으로 ROS_TIME, Time()은 SYSTEM_TIME이라 뺄셈이 거부된다. self.start = self.get_clock().now()로 시작 시각을 노드 시계에서 잡거나, 숫자로 만들어야 한다면 clock_type=self.get_clock().clock_type을 명시한다. from_msg 쪽을 SYSTEM_TIME으로 맞추는 것도 문법상 가능하지만, 메시지 스탬프는 보통 ROS 시간이므로 시작 시각을 맞추는 편이 의미가 맞다.
3. Time(seconds=1.5) + Duration(seconds=1)은 예외를 낼까?
내지 않는다. 기간에는 시계 종류가 없고, 덧셈은 왼쪽의 SYSTEM_TIME을 그대로 물려받아 2500000000 ns가 된다. 이 결과를 나중에 노드 시계 값과 비교하면 그때 예외가 난다. 예외가 터지는 줄과 실수가 들어간 줄이 다를 수 있다는 뜻이다.
시계 종류를 맞춰도 남는 문제
TypeError가 사라졌다고 계산이 맞다는 보장은 없다. 실측의 D - E는 둘 다 ROS_TIME이라 예외 없이 Duration을 돌려줬지만, 한쪽은 시스템 시각이고 다른 쪽은 /clock을 받기 전의 0이라 그 차이는 아무 의미가 없다. 노드마다 use_sim_time 설정이 다르면 같은 ROS_TIME끼리도 이렇게 어긋난다. 메시지 스탬프와 수신 노드 시계가 서로 다른 시간원에서 온 경우는 ‘Message Filter dropping message: frame laser’ 로그를 다룬 글에서 따로 다뤘다.
이 글이 확인하지 않은 범위도 분명히 해 둔다.
- 실측은 Jazzy rclpy 7.1.12 하나다. Humble·Kilted·Rolling의
time.py도 같은 기본값과 같은 예외 문구를 갖는다는 것은 2026년 10월 10일에 각 브랜치 원문으로 확인했지만 실행하지는 않았다. - rclcpp에도 같은 비대칭이 있다.
rclcpp::Time의 정수 생성자는RCL_SYSTEM_TIME(time.hpp L54), 메시지 생성자는RCL_ROS_TIME(L70-L73)이 기본이고, 불일치하면std::runtime_error("can't compare times with different time sources")를 던진다(time.cpp L108-L114). 소스만 대조했고 실행하지 않았다. - 생성자가
int(seconds × 10⁹)로 소수점 아래를 버리므로, 이진수로 정확히 표현되지 않는 초 값에서는 왕복 뒤 나노초가 1 단위 어긋날 수 있다. 예제의 1.5와 2.5는 정확히 표현되는 값으로 골랐고, 어긋나는 사례는 검산하지 않았다. /clock은 같은 프로세스의 노드가 발행한 값이다. Gazebo 같은 시뮬레이터나 실제 로봇에서 실행한 결과가 아니다.
use_sim_time 노드가 /clock을 받기 전에 0을 기준 시각으로 저장하면 첫 경과 시간이 어떻게 튀는지는 시각 0과 첫 dt를 다룬 글에서 이어서 확인할 수 있다. 이번 글의 예외는 시계 종류가 다를 때 나고, 그 글의 문제는 시계 종류가 같아도 값의 출처가 준비되지 않았을 때 생긴다는 점이 다르다.
“지금”이 필요하면 self.get_clock().now()를 쓰고, Time()은 0 시각 상수로만 다룬다. 시각 객체를 직접 만들거나 메시지에서 되살릴 때는 비교할 상대의 시계 종류를 인자로 적는다. 어느 줄이 의심스럽다면 repr(t)를 한 번 출력해 보면 된다. clock_type=SYSTEM_TIME과 clock_type=ROS_TIME이 한 식에 같이 보이면 그 줄이 예외 지점이다.
참고 링크
- ros2/rclpy time.py (Jazzy, 커밋 4e607c0): 생성자 기본값, 연산자별 시계 종류 검사,
from_msg기본값. 2026-10-10 확인. - ros2/rclpy clock.py, node.py, time_source.py: 노드 시계와
use_sim_time의 역할. 2026-10-10 확인. - ROS 2 Design: Clock and Time: 세 시계의 정의와 용도. 2026-10-10 확인.
- 실측 결과 results.json (커밋 284cd5f): 5회 실행 원본 출력과 환경 기록.
이 글에는 제휴·협찬이 없다.