ROS1에서 Time이 가지던 특징

ROS1에서 시간(Time) 처리는 기본적으로 다음과 같은 특징을 지녔다.

ROS1에서도 시뮬레이션 시간과 시스템 시간을 구분하기 위해 설정 파일이나 런치파일에서 use_sim_time을 true로 설정하고 /clock 토픽을 구독받아 사용하긴 했지만, 본질적으로 전역 타임 스탬핑 함수인 ros::Time::now()가 동시에 여러 형태의 시간을 혼용할 가능성이 존재했다.

ROS2에서 Time과 Clock 구조

ROS2는 내부적으로 rclcpp::Time, rclcpp::Clock, rclcpp::Duration 클래스를 사용하여 시간 처리를 더 구조화했다. 가장 큰 차이점은 ‘시계를 관리하는 Clock 객체’가 명시적으로 생겼다는 것이다. 이를 통해 시뮬레이션 시간과 시스템 시간을 명시적으로 구분하고, 필요한 시계 종류를 선택적으로 사용할 수 있다.

전역 함수 호출 방식에서 인스턴스 호출 방식으로 변화

ROS1에서는 ros::Time::now() 한 줄로 현재 시간을 전역(static) 함수처럼 쉽게 얻어왔다. 반면 ROS2에서는 각 노드 혹은 코드 블록에서 사용할 시계(Clock 객체)를 명시적으로 지정하여 clock->now() 형태로 사용하도록 권장된다.

예시를 살펴보자:

// ROS1 예시
ros::Time current_time = ros::Time::now();

// ROS2 예시
auto my_clock = std::make_shared<rclcpp::Clock>(RCL_STEADY_TIME);
rclcpp::Time current_time_ros2 = my_clock->now();

이처럼 ROS2에서는 “어떤 종류의 시간인지”를 Clock 객체를 통해 명시한다. 시뮬레이션 타임이 필요하면 RCL_ROS_TIME 타입으로 Clock을 생성해 /clock 토픽에 연결되도록 설정한다. 스테디 타임(프로세스가 시작된 이후부터 흐르는 고정적이고 변하지 않는 하드웨어 시계 등)을 쓰려면 RCL_STEADY_TIME, 시스템 시계를 쓰려면 RCL_SYSTEM_TIME를 사용한다.

지원되는 세 가지 Clock Type

ROS2에서는 다음과 같은 세 가지 Clock Type을 지원한다.

  1. RCL_SYSTEM_TIME
  2. 실제 시스템 시간을 제공한다.
  3. 흔히 우리가 알고 있는 Unix Epoch(1970년 1월 1일부터 경과한 시간)를 기준으로 한다.
  4. RCL_STEADY_TIME
  5. 단절 없는 단조(monotonic) 시계로, 시스템의 리얼 타임과 무관하게 일정하게 증가한다.
  6. 시스템 시간 변경(예: NTP 동기화 등)이 발생해도 영향을 받지 않는다.
  7. RCL_ROS_TIME
  8. /clock 토픽을 통해 주어지는 시간으로, 주로 시뮬레이션이나 로그 재생시 사용한다.
  9. 시뮬레이터(Gazebo 등)에서 제공하는 ROS 시간 혹은 rosbag2 재생 시뮬레이션 시간을 여기서 받아서 쓸 수 있다.
// 시계 타입별 Clock 생성 예시
auto system_clock = std::make_shared<rclcpp::Clock>(RCL_SYSTEM_TIME);
auto steady_clock = std::make_shared<rclcpp::Clock>(RCL_STEADY_TIME);
auto ros_clock = std::make_shared<rclcpp::Clock>(RCL_ROS_TIME);

노드 기반으로 시간 가져오기

ROS2 노드는 내부적으로 Clock을 내장하고 있으며, Node 객체의 메서드로 현재 시간을 가져올 수 있다. 예를 들면:

class MyNode : public rclcpp::Node
{
public:
  MyNode()
  : Node("my_node")
  {
    // Node 내부적으로 this->get_clock()을 제공한다.
    RCLCPP_INFO(this->get_logger(), "Current ROS time: %ld", this->now().nanoseconds());
  }
};

여기서 this->now()는 기본적으로 ROS 시간(RCL_ROS_TIME)을 사용하도록 구성된다. 다만 use_sim_time이 false라면 실제 시스템 시계에 매핑된다. 이러한 구동 방식은 ROS1과 달리 코드 곳곳에 전역 함수를 직접 호출하기보다 “노드가 가진 시계” 혹은 “직접 생성한 Clock 객체”를 통해 시간을 활용한다는 점이 핵심이다.

시뮬레이션 시간 동작 원리

ROS1에서는 /clock 토픽을 자동으로 구독하려면 노드 파라미터 use_sim_time을 true로 해야 했다. ROS2도 동일하지만, ROS2에서는 노드 초기화 과정에서 파라미터를 선언하고, 그 파라미터가 true이면 노드 내부 Clock이 /clock을 자동 구독하도록 되어 있다.

ROS2 C++ 코드에서 이를 설정하는 예시는 다음과 같다:

rclcpp::NodeOptions options;
options.parameter_overrides().push_back({"use_sim_time", true});
auto node = std::make_shared<rclcpp::Node>("test_node", options);

// 이제 node->now()를 하면 시뮬레이션 시간 기준으로 시간이 동작

파라미터 서버(parameter server)가 독립적으로 존재하던 ROS1과 달리, ROS2에서는 노드 옵션을 생성할 때 use_sim_time을 직접 등록하고 쓴다. 또는 런치파일 등에서 파라미터를 주입하는 방식으로 제어 가능하다.

Time, Duration 연산의 차이

ROS1에서도 ros::Duration을 통해 시간 간격 표현 및 덧셈·뺄셈 연산을 지원했지만, ROS2는 rclcpp::Duration이 보다 엄격한 자료형 변환 규칙을 가진다. 예를 들어, ROS1에서는 다음과 같이 정수 타입만 입력해도 동작했지만,

// ROS1
ros::Duration dur(10); // 10초?

ROS2에서는 초 단위뿐 아니라 나노초 단위로 명시하거나, 생성자 오버로딩을 통해 보다 정밀한 제어가 가능하다.

// ROS2
rclcpp::Duration dur = rclcpp::Duration::from_seconds(10.0);
// 또는
rclcpp::Duration dur_ns = rclcpp::Duration(0, 500000000); // 0초+5e8 ns = 0.5초

이러한 타입 기반의 명시적 선언으로 인해 개발자가 의도치 않은 단위 실수를 줄일 수 있다.

간단한 시각화

ROS2에서 여러 Clock 객체가 어떻게 시간을 제공하는지 간단히 나타내면 아래와 같다.

flowchart LR A[System Clock] -->|RCL_SYSTEM_TIME| C[Clock 객체] B["Sim Time(/clock)"] -->|RCL_ROS_TIME| C D[Steady Clock] -->|RCL_STEADY_TIME| C C -->|"now(), get_time()"| E["Time 객체(rclcpp::Time)"]

Time Jump Callback (시뮬레이션 시계 변동 탐지)

ROS1 환경에서 노드가 시뮬레이션 시간(/clock)을 구독하다가 갑자기 시뮬레이터(Gazebo 등)가 시간을 되돌리거나(Time reset) 혹은 많은 양을 건너뛰는 경우(상용 시뮬레이션 도구에서 특정 시점으로 “점프”하는 경우 등)를 포착하기가 어려웠다. 반면 ROS2에서는 rclcpp::JumpHandler, on_time_jump 등 콜백 구조를 통해 시간이 급격하게 변동되는 이벤트를 감지할 수 있다.

Jump의 발생 상황

  1. 시뮬레이터가 재시작되어 ROS 시계가 0초 부근으로 돌아간 경우(Backward jump)
  2. 시뮬레이터가 시간을 강제로 앞으로 크게 점프시킨 경우(Forward jump)
  3. 로그 재생(rosbag2) 시 특정 시점으로 재점프하는 경우
  4. NTP 동기화 등 시스템 시계가 크게 수정되는 경우(단, 시스템 시계를 사용 중일 때)

이런 상황에서 ROS1은 단순히 /clock 토픽이 변동된 사실만 알 수 있을 뿐, 얼마나 시계가 뒤로 갔는지 혹은 앞으로 뛰었는지를 노드 차원에서 세밀하게 제어하기가 어려웠다. 따라서 시뮬레이션 코드가 시간 변동을 제대로 처리하지 못해, 시간에 의존하는 알고리즘(예: Kalman 필터, TF 브로드캐스팅 등)이 혼란에 빠질 수 있었다.

ROS2에서는 다음과 같이 Jump Callback을 설정해 시간 점프 발생 시점을 감지하고, 필요하다면 대응 조치를 취할 수 있다.

// JumpCallback 예시 코드
#include <rclcpp/rclcpp.hpp>
#include <chrono>

using JumpHandler = rclcpp::JumpHandler;
using JumpThreshold = rclcpp::JumpThreshold;

int main(int argc, char** argv)
{
  rclcpp::init(argc, argv);

  // 노드 생성
  auto node = std::make_shared<rclcpp::Node>("jump_example");

  // ROS 타임으로 Clock 생성
  auto ros_clock = node->get_clock();

  // JumpThreshold 설정: 앞으로 뒤로 가는 점프 허용 범위 지정
  JumpThreshold threshold;
  threshold.on_clock_change = true;  // 시계 타입 변경 감지
  threshold.min_forward = std::chrono::nanoseconds(1); // 1ns 이상 앞쪽 이동 감지
  threshold.min_backward = std::chrono::nanoseconds(1); // 1ns 이상 뒤쪽 이동 감지

  // 실제 콜백 등록
  auto jump_callback = [](const rclcpp::TimeJump & jump) {
    if (jump.forward_in_time) {
      RCLCPP_WARN(rclcpp::get_logger("jump_example"), 
                  "Time jumped forward by %ld ns",
                  jump.delta_nanoseconds.count());
    } else {
      RCLCPP_WARN(rclcpp::get_logger("jump_example"), 
                  "Time jumped backward by %ld ns",
                  jump.delta_nanoseconds.count());
    }
  };

  // Clock에 jump callback 등록
  ros_clock->set_on_time_jump_callback(JumpHandler(threshold, jump_callback));

  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

이처럼 ROS2에서는 set_on_time_jump_callback을 통해 ‘시간 점프 발생’을 명시적으로 감지하고, 이후 로직을 어떻게 처리할지 정교하게 제어 가능하다. 예컨대 ROS1 대비 달라진 부분은:

Timer 생성 시 Clock 명시

ROS1의 경우, ros::Timer를 생성하면 내부적으로 시스템 시계를 기준으로 동작하거나, use_sim_time이 활성화되어 있으면 /clock을 참조했지만 개발자가 그 흐름을 직접 선택할 방법이 제한적이었다. ROS2에서는 create_wall_timer, create_timer, create_steady_timer 등 다양한 Timer 생성 함수를 제공하며, 원하는 Clock을 직접 지정할 수도 있다.

// 노드 내부에서 Timer 생성 시 Clock 지정 예시
auto custom_clock = std::make_shared<rclcpp::Clock>(RCL_STEADY_TIME);
auto timer_callback = []() {
  RCLCPP_INFO(rclcpp::get_logger("timer_example"), "Timer fired!");
};

auto timer = this->create_timer(
  custom_clock,
  rclcpp::Duration(std::chrono::seconds(1)),
  timer_callback
);

이 예시처럼, 스테디 타임(RCL_STEADY_TIME) 기반 타이머를 만들면, 시스템 시계나 시뮬레이션 시계가 변동되어도 이 타이머는 일정 간격으로 콜백을 부른다. 이는 ROS1 대비 개발자에게 시간 소스를 명확히 고를 수 있는 자유도가 크게 늘어난 부분이다.

QoS 설정을 통한 /clock 토픽 구독

ROS2에서 /clock 토픽은 일부 기본 QoS(Profile)를 적용한다. ROS1에서 /clock은 쉽게 구독만 하면 됐지만, ROS2에서 시뮬레이션 시간 동작을 원활히 하려면 노드 내부 Clock이 해당 QoS에 맞춰 연결되어야 한다. 다행히 대부분의 경우 “기본 QoS(같은 Reliability, Durability)”로 설정되어 있어 추가 설정 없이도 잘 동작하지만, 특수 상황(예: 네트워크 지연 혹은 특정 트랜스포트 사용)에서 QoS 파라미터를 세밀하게 바꿔야 할 수 있다.

노드가 /clock 토픽을 구독하는 동작 방식:

  1. 노드가 초기화될 때 파라미터(use_sim_time) 확인
  2. true면 노드 내부 Clock이 자동으로 /clock 구독 시도
  3. /clock의 QoS가 호환되면 정상적으로 타임 이벤트 수신
  4. 호환되지 않으면 경고가 발생하거나 타임 이벤트 수신 실패

실무에서 QoS 문제로 /clock 이벤트가 제대로 들어오지 않으면, Clock이 계속 시스템 시계를 보고 있을 가능성이 높다. 이때는 rclcpp::NodeOptions나 직접 Node 생성 시 QoS 호환성을 명시해야 한다.

TF(Transform) 스탬프 처리 차이

ROS1 TF vs. ROS2 TF2에서 타임스탬프(Header.stamp)를 할당하는 방식이 크게 다르진 않으나, 사용하는 Clock이 ROS1 대비 달라졌음을 주의해야 한다. 예를 들어 TF2 브로드캐스터에서,

// ROS1
geometry_msgs::TransformStamped t;
t.header.stamp = ros::Time::now();
// ROS2
geometry_msgs::msg::TransformStamped t;
t.header.stamp = node->now();

이렇게 바뀌는 것은 겉보기로는 단순하지만, node->now()가 실제로 시스템 시계인지, 시뮬레이션 시계인지, 혹은 다른 Clock인지 노드 설정에 따라 달라진다. 따라서 TF2를 사용하는 노드라면, 적절한 Clock 설정이 되어 있는지 반드시 확인해야 한다.

ROS2에서 Time 관련 추가 기능 및 주의사항

1. Clock 객체의 유효성

ROS2에서 Clock 객체를 명시적으로 생성하고 사용하다 보면, 시뮬레이션 시간(/clock 토픽)과의 연결이 ‘아직’ 안 되었거나, 네트워크 문제로 시계 정보가 들어오지 않는 상태일 수 있다. 이때 clock->now()를 호출하면 다음과 같은 상황이 발생할 수 있다.

이를 방지하기 위해서는 노드가 실제 시계 이벤트를 수신했는지 여부를 확인하거나, ROS2가 제공하는 몇 가지 편의 함수를 활용하는 방법이 있다.

// 예시: ROS time 활성화 되었는지 확인
auto ros_time_active = node->get_clock()->ros_time_is_active();
if (!ros_time_active) {
  RCLCPP_WARN(node->get_logger(), "ROS time not active yet.");
}

이 메서드는 ROS 타임(RCL_ROS_TIME)이 실제로 구독되고 있는지 알려준다. 즉, /clock 토픽의 메시지를 아직 못 받았다면 false가 된다.

2. Time과 Duration을 이용한 시간 비교

ROS2에서 rclcpp::Time 간 비교, rclcpp::Duration 간 비교는 다음과 같은 연산자를 지원한다:

예시:

rclcpp::Time t1 = node->now();                // 예: 1.5초
rclcpp::Duration dur = rclcpp::Duration(1, 0); // 1초
rclcpp::Time t2 = t1 + dur;                   // 결과: 2.5초
rclcpp::Duration diff = t2 - t1;              // 1초
if (t2 > t1) {
  RCLCPP_INFO(node->get_logger(), "t2 is later than t1");
}

ROS1에서도 유사한 연산이 있었으나, ROS2에서는 ‘Clock 객체를 통한 Time 생성’이라는 점을 고려해 시간의 출처가 다를 경우(예: system_clock vs. steady_clock 등) 연산의 의미가 달라질 수 있음을 주의해야 한다. 예를 들어, 스테디 타임으로 얻은 Time과 ROS 시뮬레이션 타임으로 얻은 Time을 단순 비교하거나 빼는 것은 실질적으로 서로 무관한 시계이므로 의미 없는 결과가 나온다.

3. Time의 내부 표현(64-bit 정수)

ROS1의 ros::Time(sec, nsec) 두 개의 32-bit 정수로 구분해 저장했다. ROS2의 rclcpp::Time64-bit 정수 하나로 나노초를 표현한다. 예컨대:

\[ t_{\text{ns}} = t_{\text{sec}} \times 10^9 + t_{\text{nsec}} \]

이러한 변경은 다음과 같은 장점이 있다.

  1. 연산 단순화
  2. 빼기, 더하기, 비교 시 나노초 단위로 일관 처리하므로 구현이 간결하다.
  3. 오버플로(overflow) 시점 지연
  4. 64-bit 정수로 표현하면 이론상 약 584년 후에나 오버플로가 발생하므로, 사실상 오버플로 걱정을 거의 안 해도 된다.

물론 메시지 레벨(builtin_interfaces::msg::Time)에서는 여전히 (sec, nanosec) 구조를 쓰지만, C++ 레벨에서 편의를 위해 64-bit 정수 기반으로 쉽게 변환·연산이 가능하다.

4. Duration의 from_seconds()와 to_chrono()

ROS2에서 Duration을 사용할 때, C++ 표준 라이브러리인 std::chrono와 상호 변환하는 경우가 많다. 예를 들면 rclcpp::Duration::from_seconds()rclcpp::Duration::to_chrono() 메서드를 이용해, 직관적인 초 단위 표현과 표준 시간 라이브러리를 오갈 수 있다.

auto dur = rclcpp::Duration::from_seconds(3.5);
auto chrono_dur = dur.to_chrono(); // std::chrono::nanoseconds

또, 반대로 std::chrono 타입에서 ROS2 Duration으로 만들려면 std::chrono::duration_cast 등을 써야 한다.

std::chrono::seconds cs(2);
rclcpp::Duration ros2_dur(cs);  // 2초

ROS1에는 이런 변환이 기본적으로 많지 않았으나, ROS2는 C++11 이상 환경이므로 std::chrono와의 연동이 좀 더 매끄럽다.

5. 시뮬레이션 vs. 실제 시간 동작 확인 예

ROS1 시대에는 시뮬레이션 시간인지 아닌지, /use_sim_time이 적용 중인지 아닌지 확인하려면 파라미터 서버를 직접 조회해야 했다. ROS2에서는 노드 레벨에서 쉽게 확인할 수 있다.

if (node->get_clock()->get_clock_type() == RCL_ROS_TIME) {
  RCLCPP_INFO(node->get_logger(), "Currently using ROS time (simulation).");
}
else {
  RCLCPP_INFO(node->get_logger(), "Using system/steady time.");
}

또는:

bool sim_active = node->get_clock()->ros_time_is_active();
if (sim_active) {
  RCLCPP_INFO(node->get_logger(), "ROS time is active.");
} else {
  RCLCPP_INFO(node->get_logger(), "ROS time not active.");
}

이처럼 노드 코드 자체에서 시뮬레이션 시간 사용 여부를 직관적으로 파악할 수 있기 때문에, “현재 clock이 어디에서 왔는지”를 알아야 하는 로직을 보다 간단히 구성할 수 있다.

6. 메시지 필드와 Time의 상호 변환

ROS2에서 대부분 메시지는 builtin_interfaces::msg::Time 필드를 사용한다(예: std_msgs::msg::Header::stamp). 이를 rclcpp::Time과 상호 변환할 때는 다음과 같이 한다.

// rclcpp::Time -> builtin_interfaces::msg::Time
rclcpp::Time rcl_time = node->now();
builtin_interfaces::msg::Time msg_time = rcl_time.to_msg();

// builtin_interfaces::msg::Time -> rclcpp::Time
rclcpp::Time rcl_time2(msg_time);

ROS1에서는 ros::Time::toSec(), ros::Time::fromBoost() 등의 여러 변환 함수를 썼는데, ROS2는 to_msg(), from_msg()(또는 생성자) 등을 주로 쓴다.

7. Clock을 별도로 만들 때 주의할 점

사용자가 별도로 rclcpp::Clock 객체를 std::make_shared로 생성했을 경우, ROS 타임(RCL_ROS_TIME)과 연결하고자 한다면, 내부적으로 /clock 토픽 구독을 위한 설정(노드와의 연결)이 추가로 필요하다. 단순히 Clock(RCL_ROS_TIME)을 생성했다고 해서 자동으로 /clock을 구독하지 않는다. 보통은 다음과 같이 노드가 제공하는 인터페이스를 사용하는 편이 권장된다:

auto node = std::make_shared<rclcpp::Node>("my_node");
auto ros_clock = node->get_clock(); // node와 연결된 Clock

이 경우, 노드가 내부적으로 /clock을 구독하며, 시뮬레이션 시간이 들어오면 해당 Clock이 갱신된다. 만약 정말 커스텀으로 /clock을 수동 구독하여 Clock을 업데이트하고 싶다면, rclcpp::TimeSource 클래스를 직접 사용할 수 있다.

rclcpp::TimeSource time_source(node);
auto custom_clock = std::make_shared<rclcpp::Clock>(RCL_ROS_TIME);
time_source.attachClock(custom_clock);

// 이제부터 custom_clock도 /clock 토픽으로 갱신된다.

이처럼 TimeSource는 노드와 연결되어 /clock 메시지를 받아, 등록한 Clock 객체들을 업데이트해준다. 이는 ROS1에서는 별도의 API로 제공되지 않았던 기능이다.

ROS2에서 Rate 사용

ROS1에서 자주 쓰였던 ros::Rate는 내부적으로 시스템 시계를 사용해 지정 주기로 sleep()을 수행했다. 예를 들어,

// ROS1 예시
ros::Rate loop_rate(10);  // 10Hz
while (ros::ok()) {
  // 작업 수행
  ros::spinOnce();
  loop_rate.sleep();
}

ROS2에서는 rclcpp::Rate로 유사한 기능을 제공하지만, 사용하는 Clock을 별도로 지정할 수 있다. 이는 시뮬레이션 시간, 스테디 시간 등 원하는 시계를 기반으로 주기 제어가 가능함을 의미한다.

// ROS2 예시
auto node = std::make_shared<rclcpp::Node>("rate_example");
rclcpp::Rate loop_rate(10.0); // 기본 system clock 사용(초 단위)
while (rclcpp::ok()) {
  // 작업 수행
  rclcpp::spin_some(node);
  loop_rate.sleep();
}

Rate와 Clock 타입

ROS2에서 rclcpp::Rate 생성자에 별도 인자를 주지 않으면 기본 시스템 시간(RCL_SYSTEM_TIME)을 사용한다. 만약 시뮬레이션 시간이나 스테디 시간을 기준으로 주기 동작을 원한다면 rclcpp::Rate 대신 GenericRate<Clock> 형태를 사용하거나, 내부적으로 create_rate() 함수를 이용하여 Clock 객체를 주입할 수 있다.

auto my_clock = std::make_shared<rclcpp::Clock>(RCL_STEADY_TIME);
rclcpp::Rate steady_rate(my_clock, 5.0); // 스테디 타임 기반 5Hz

while (rclcpp::ok()) {
  // ...
  steady_rate.sleep();
}

이렇게 스테디 타임을 기반으로 Rate를 사용하면, 시스템 시계나 시뮬레이션 시계가 바뀌어도 주기가 흔들리지 않는다. 실제 시뮬레이션이 멈추거나 시간을 뒤로 돌려도 Rate는 스테디 타임을 따라가므로, “실제 시간” 기준의 동작이 지속된다.

Rate의 내부 작동 방식

ROS2 rclcpp::Rate는 내부적으로 다음 로직을 수행한다.

  1. 설정된 주기(초)를 std::chrono::duration 등으로 기록
  2. 각 루프에서 현재 시간(Clock::now())을 얻어서, 이전 루프 시작 시간과의 간격을 확인
  3. 부족한 시간만큼 std::this_thread::sleep_for()를 호출하여 루프 주기가 맞춰지도록 만든다

시뮬레이션 타임을 사용하는 경우, /clock 토픽에서 들어오는 시간 메시지를 기반으로 now()가 갱신되므로, 시뮬레이션이 느려지거나 빠르게 진행되면 Rate도 해당 시뮬레이션 타임 흐름대로 맞춰서 동작한다.

rclpy(Python)에서의 Time, Clock, Rate

ROS1 파이썬(rospy)에서도 rospy.Time.now(), rospy.Rate() 등을 써서 시간과 주기를 제어했다. ROS2 파이썬(rclpy)에서도 유사한 개념을 제공하지만, C++ API와 마찬가지로 ClockTime 객체가 존재한다.

# ROS2 Python 예시
import rclpy
from rclpy.node import Node
from rclpy.clock import Clock, ClockType, ROSClock
from rclpy.time import Time

class TimeNode(Node):
    def __init__(self):
        super().__init__('time_node')
        # Node에는 기본으로 ROS Clock이 연결됨
        self.timer = self.create_timer(1.0, self.timer_callback)

    def timer_callback(self):
        now = self.get_clock().now()
        self.get_logger().info(f"Current time: {now.nanoseconds} ns")

def main(args=None):
    rclpy.init(args=args)
    node = TimeNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

위 코드에서 self.get_clock().now()는 노드 기본 Clock(ROS time or system time)을 이용해 현재 시간을 Time 객체로 돌려준다.

파이썬에서의 시뮬레이션 시간

ROS2 Python에서 시뮬레이션 시간을 사용하려면 마찬가지로 노드를 생성할 때 use_sim_time 파라미터를 true로 주어야 하고, 내부적으로 /clock이 구독되어야 한다. 런치파일이나 파라미터 설정을 통해 쉽게 활성화할 수 있다.

def main(args=None):
    rclpy.init(args=args)
    node = TimeNode()
    node.set_parameters([rclpy.parameter.Parameter('use_sim_time', 
                                                   rclpy.parameter.Parameter.Type.BOOL, True)])
    rclpy.spin(node)
    ...

이렇게 하면 node.get_clock()/clock을 구독하여 시뮬레이션 시간으로 동작한다.

Python Rate

rclpy에서는 Rate 객체 대신 rclpy.rate.Rate 클래스를 제공한다. 사용법은 ROS1 파이썬과 유사하다.

import rclpy
from rclpy.node import Node
from rclpy.rate import Rate

def main(args=None):
    rclpy.init(args=args)
    node = Node("rate_example_node")
    rate = Rate(node, 2.0)  # 2Hz
    while rclpy.ok():
        node.get_logger().info("Loop iteration")
        rclpy.spin_once(node, timeout_sec=0.0)
        rate.sleep()

    node.destroy_node()
    rclpy.shutdown()

Rate(node, 2.0)는 노드가 사용하는 Clock(대부분 ROS time)을 기준으로 2Hz 주기를 맞춘다. 시뮬레이션 시간으로 설정되어 있으면 /clock 속도에 따라 주기도 달라질 수 있다.

Real-Time 고려사항

ROS2는 일부 실시간(Real-Time) 사용 시나리오를 염두에 두고 설계되었다. 따라서 “정말 하드 실시간” 환경에서는 ROS1 방식의 시스템 시계가 예기치 않게 변동(예: NTP 동기화)될 경우 문제가 생길 수 있었는데, ROS2에서는 스테디 타임(monotonic clock)과의 결합을 통해 보다 안정적인 시각 참조를 구현할 수 있다. 예컨대:

다만, “실시간 커널 패치” 등을 이용해 정말 하드 실시간을 달성하려면 ROS2의 Executor나 DDS 계층 등 전체 파이프라인에서 추가 설정이 필요하기 때문에, 단순히 Clock를 선택하는 것만으로는 완벽한 하드 실시간이 보장되는 것은 아니다. 그래도 ROS1 대비 시계 선택 옵션이 더 많아졌고, 시계가 변경되는 이벤트(Jump) 감지도 가능하므로, 실시간 애플리케이션에 유리하다.

Rate vs. Timer

ROS2에서 타이머(create_timer)는 콜백 방식이고, Rate는 루프(while) 안에서 sleep을 호출하는 구조다. 둘 다 주기성을 구현하지만, Timer는 아래와 같은 장점이 있다.

  1. 콜백 중심 설계(콜백 함수가 주기마다 자동 호출)
  2. spin() 구조와 잘 어울림
  3. ROS2 Executor 스레드가 콜백을 분산 처리 가능

반면 Rate는 기존 ROS1 방식처럼, 개발자가 직접 루프를 돌리고 rate.sleep()을 호출해야 한다. 직관적이지만 멀티스레드 Executor와 결합할 때 주의가 필요하다.

// Timer 예시
auto timer = this->create_wall_timer(
  std::chrono::milliseconds(100), // 10Hz
  [this]() { RCLCPP_INFO(this->get_logger(), "Timer callback"); }
);

create_wall_timerWall Clock(실제 시스템 시간) 기준으로 작동한다. 시뮬레이션 시간을 사용하려면 create_timernode->get_clock() 등을 넘겨주면 된다.

시스템 시계(NTP 동기화)에 대한 고려

시스템 시계(RCL_SYSTEM_TIME)는 운영체제의 시간 정보를 사용하므로, NTP(Network Time Protocol) 등에 의해 시간이 조정될 수 있다. 이 경우 ROS1에서 ros::Time::now()가 갑자기 과거로 돌아가거나, 큰 폭으로 앞으로 튀는 현상이 발생할 수 있었다. ROS2에서는 NTP 동기화로 인한 시간 점프도 Jump Callback으로 잡을 수 있다(단, RCL_SYSTEM_TIME를 사용 중일 때).

auto sys_clock = std::make_shared<rclcpp::Clock>(RCL_SYSTEM_TIME);
// JumpThreshold 설정으로 시계 점프 감지

다만, 스테디 타임(RCL_STEADY_TIME)은 NTP 동기화의 영향을 받지 않으므로 이런 점프는 없다. 따라서 NTP로 인한 시간 재조정으로부터 애플리케이션을 보호하려면 스테디 타임을 쓰거나, 점프 이벤트를 감지해 별도 대처 로직을 구현해야 한다.

Zero Time(0초) 취급

ROS1에서는 시간 초기화가 안 된 경우 ros::Time(0)으로 나타나는 상황이 많았다. ROS2도 메시지 레벨에서 sec=0, nanosec=0으로 표현될 수 있는데, 이를 유효한 시간으로 볼지, “아직 설정되지 않은 시간”으로 볼지는 설계에 따라 다르다. Jump Callback이나 active 체크 등을 통해 “정상적으로 시간이 들어오고 있는지”를 확인하는 습관이 좋다.

Bagfile 재생(rosbag2)과 시뮬레이션 시간

ROS2에서 rosbag2로 기록된 로그를 재생할 때, 기본적으로 /clock 토픽이 퍼블리시되어 시뮬레이션 시간(RCL_ROS_TIME) 형태로 흐른다. 이는 ROS1의 rosbag play --clock와 유사한 개념이다. 이때 노드에서 use_sim_time을 활성화해 두면, 재생되는 로그의 타임스탬프에 맞춰서 시간을 받아올 수 있다.

# rosbag2 재생 시 clock 퍼블리시
ros2 bag play <my_bag> --clock

특정 배속 재생

배속 재생(예: 2배속, 0.5배속) 시 /clock 토픽의 발행 주기가 달라지므로, ROS2 노드의 Clock도 그에 따라 빠르게(또는 느리게) 진전된 시뮬레이션 시간을 받는다. 이를 통해 시간에 의존적인 알고리즘(예: SLAM, Path Planning) 테스트 시 빠른 검증이 가능하다.

ros2 bag play <my_bag> --clock --rate 2.0

이렇게 배속을 줄이거나 높이면 Clock이 “점진적”으로 빨라지거나 느려지는데, Jump Callback 대상은 아니다. Jump Callback은 “갑작스런 시계 점프(Backward/Forward)”를 감지하고, 단순 배속 조정은 지속적 스케일링이므로 Jump로 취급되지 않는다.

여러 개의 bagfile 동시 재생

여러 개의 rosbag2 데이터를 동시에 재생하면 /clock 토픽을 서로 중복해 퍼블리시할 수 있으니, 주의해야 한다. ROS1 시절에도 중복된 /clock 퍼블리시 문제로 혼란이 발생할 수 있었는데, ROS2에서는 QoS나 노드 구성에 따라 에러 혹은 경고가 날 수 있다. 보통은 한 번에 하나의 /clock 퍼블리셔만 사용하도록 설계하는 편이 안전하다.

Multi-robot 및 분산 시스템에서 시간 관리

서로 다른 시뮬레이터 사용 시

ROS2 멀티 로봇 시뮬레이션에서 각 로봇 시뮬레이터(예: Gazebo, Ignition, Unity 등)마다 /clock을 퍼블리시한다면, 단일 ROS 네트워크에서 시뮬레이션 타임이 충돌할 수 있다.

기본적으로 ROS2는 “단일 /clock 소스”를 가정하므로, 여러 시뮬레이터의 시간이 서로 달라지면 타임스탬프 혼선이 생길 수 있음에 유의해야 한다.

DDS와 WAN(광역 네트워크) 환경

ROS2 DDS(Domain) 설정으로 멀리 떨어진 로봇/컴퓨터 간 통신을 구성할 때, /clock이 네트워크 지연과 패킷 손실로 인해 불규칙하게 전달될 수 있다. 이 경우 노드 Clock이 갑작스러운 지연으로 인해 “Jump”처럼 느끼거나, 시계가 제대로 갱신되지 않아 시뮬레이션 시간과 실제 로직이 어긋날 수 있다.

따라서 분산 환경에서 시뮬레이션 시간 동기화를 시도할 때는, 노드 설계 시 “Clock 패킷 지연”을 어느 정도 감안한 알고리즘 구성이 필요하다.

Best Practice 요약 (코드 레벨)

  1. 노드 내부 시계 사용
  2. node->now(), this->get_clock()->now()를 사용해, 해당 노드 설정(시스템 시계/시뮬레이션 시계 등)에 맞춘 시간을 확실히 얻는다.
  3. Jump Callback 등록
  4. 시뮬레이션 환경에서 시간을 되감거나 크게 이동할 가능성이 있다면, Jump Callback을 등록해 알고리즘 혼란을 줄인다.
  5. Duration은 명시적으로 작성
  6. rclcpp::Duration::from_seconds(1.0) 혹은 (1, 0)처럼, 초·나노초를 정확히 지정해 단위 오해 방지
  7. Timer 사용 시 의도한 Clock인지 재확인
  8. create_wall_timer(실제 시스템 시계), create_timer(ROS/Steady 시계) 등 함수명을 혼동하지 않도록 주의
  9. ROS time 활성 체크
  10. ros_time_is_active()를 통해 노드가 시뮬레이션 시간을 정말 받고 있는지 확인.
  11. 멀티 시뮬레이터 시 주의
  12. 단일 /clock 소스가 이상적. 여러 시뮬레이터가 동시에 /clock을 퍼블리시하면 충돌 위험.