659.37 waitForTransform()을 이용한 변환 대기

1. 개요

waitForTransform()은 TF1 시대에 변환이 가용해질 때까지 블로킹(blocking) 방식으로 대기하는 핵심 메서드였다. ROS 2의 TF2에서는 이 함수의 역할이 canTransform()의 타임아웃 파라미터와 lookupTransform()의 타임아웃 오버로드로 흡수되어, 독립적인 waitForTransform() 메서드는 더 이상 공식 API에 포함되지 않는다. 본 절에서는 TF1에서의 waitForTransform()의 동작 원리를 기술하고, ROS 2 TF2에서 동일한 기능을 구현하는 대체 패턴을 상세히 설명한다.

2. TF1에서의 waitForTransform()

2.1 함수 시그니처 (ROS 1)

ROS 1의 tf::TransformListener 클래스에서 제공되었던 waitForTransform()의 시그니처는 다음과 같다.

// ROS 1 tf 라이브러리 (레거시)
bool waitForTransform(
    const std::string & target_frame,
    const std::string & source_frame,
    const ros::Time & time,
    const ros::Duration & timeout,
    const ros::Duration & polling_sleep_duration = ros::Duration(0.01),
    std::string * error_msg = nullptr) const;
파라미터자료형설명
target_frameconst std::string &타깃 좌표 프레임 이름
source_frameconst std::string &소스 좌표 프레임 이름
timeconst ros::Time &변환을 조회할 시간
timeoutconst ros::Duration &최대 대기 시간
polling_sleep_durationconst ros::Duration &폴링 간격 (기본값: 10 ms)
error_msgstd::string *(선택) 오류 사유 기록용 포인터

2.2 동작 원리

waitForTransform()은 내부적으로 다음과 같은 폴링 루프(polling loop)를 수행하였다.

  1. canTransform()을 호출하여 변환 가용성을 확인한다.
  2. 변환이 가용하면 즉시 true를 반환한다.
  3. 변환이 불가능하면 polling_sleep_duration만큼 대기한 후 다시 확인한다.
  4. 총 대기 시간이 timeout을 초과하면 false를 반환한다.

이 메커니즘의 의사 코드(pseudocode)는 다음과 같다.

function waitForTransform(target, source, time, timeout, poll_interval):
    start_time = current_time()
    while (current_time() - start_time) < timeout:
        if canTransform(target, source, time):
            return true
        sleep(poll_interval)
    return false

2.3 TF1 기반 시간 여행 오버로드

// ROS 1 tf 라이브러리 (레거시)
bool waitForTransform(
    const std::string & target_frame,
    const ros::Time & target_time,
    const std::string & source_frame,
    const ros::Time & source_time,
    const std::string & fixed_frame,
    const ros::Duration & timeout,
    const ros::Duration & polling_sleep_duration = ros::Duration(0.01),
    std::string * error_msg = nullptr) const;

이 오버로드는 두 프레임이 서로 다른 시간에서의 변환이 모두 가용해질 때까지 대기하였다.

3. ROS 2 TF2에서의 대체 방법

3.1 canTransform()의 타임아웃 파라미터

ROS 2 TF2에서는 canTransform()에 타임아웃 파라미터를 전달하여 waitForTransform()과 동일한 기능을 구현한다.

// ROS 2 TF2: canTransform() + timeout = waitForTransform()
std::string error_msg;
bool ready = tf_buffer_->canTransform(
    "map", "base_link",
    tf2::TimePointZero,
    tf2::durationFromSec(5.0),  // 5초 대기
    &error_msg);

if (ready) {
    auto t = tf_buffer_->lookupTransform(
        "map", "base_link", tf2::TimePointZero);
    processTransform(t);
} else {
    RCLCPP_ERROR(get_logger(),
        "Transform not available: %s", error_msg.c_str());
}

3.2 lookupTransform()의 타임아웃 오버로드

lookupTransform()에 타임아웃을 직접 전달하면, 대기와 조회를 한 번에 수행할 수 있다.

// ROS 2 TF2: lookupTransform() + timeout
try {
    auto t = tf_buffer_->lookupTransform(
        "map", "base_link",
        tf2::TimePointZero,
        tf2::durationFromSec(5.0));  // 5초 대기 후 조회
    processTransform(t);
} catch (const tf2::TransformException & ex) {
    RCLCPP_ERROR(get_logger(),
        "Transform failed after timeout: %s", ex.what());
}

이 방식은 waitForTransform() + lookupTransform()을 한 번의 호출로 대체하므로, TOCTOU 경합 조건을 완전히 제거한다.

4. 블로킹 대기의 내부 메커니즘

4.1 tf2_ros::Buffer의 타임아웃 처리

ROS 2의 tf2_ros::Buffer는 타임아웃이 지정된 경우 다음과 같은 내부 메커니즘을 사용한다.

  1. 조건 변수(condition variable) 기반 대기: 폴링 루프 대신, 새로운 변환이 버퍼에 추가될 때 조건 변수를 통해 대기 중인 스레드를 깨운다.
  2. 효율적인 자원 사용: 폴링에 비해 CPU 사용량이 현저히 낮다.
  3. 정밀한 타임아웃 제어: 나노초 단위의 타임아웃 해상도를 제공한다.

4.2 TF1 폴링 방식과의 비교

특성TF1 waitForTransform()TF2 타임아웃 메커니즘
대기 방식폴링 루프조건 변수 기반
CPU 사용량높음 (지속적 폴링)낮음 (이벤트 기반)
응답 지연폴링 간격에 의존즉시 응답
구현 복잡도단순내부적으로 복잡
TOCTOU 문제있음 (별도 조회 필요)없음 (lookupTransform() 통합)

5. rclpy에서의 변환 대기

5.1 기본 패턴

from rclpy.duration import Duration
from rclpy.time import Time

# canTransform()으로 대기
can_transform = self.tf_buffer.can_transform(
    'map',
    'base_link',
    Time(),
    timeout=Duration(seconds=5.0))

if can_transform:
    t = self.tf_buffer.lookup_transform(
        'map', 'base_link', Time())
    self.process_transform(t)
else:
    self.get_logger().error('Transform not available after 5s')

5.2 lookupTransform()에 타임아웃 직접 전달

from tf2_ros import TransformException

try:
    t = self.tf_buffer.lookup_transform(
        'map',
        'base_link',
        Time(),
        timeout=Duration(seconds=5.0))
    self.process_transform(t)
except TransformException as ex:
    self.get_logger().error(
        f'Transform failed after timeout: {ex}')

6. 비동기 변환 대기 패턴

6.1 문제: 블로킹 호출과 Executor

타임아웃이 지정된 canTransform()이나 lookupTransform()은 블로킹 호출이다. ROS 2의 단일 스레드 Executor(SingleThreadedExecutor)에서 콜백 내에 블로킹 호출을 배치하면, 해당 콜백이 완료될 때까지 다른 콜백(변환 수신 포함)이 처리되지 않아 교착 상태(deadlock)가 발생할 수 있다.

6.2 해결책 1: MultiThreadedExecutor 사용

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    auto node = std::make_shared<MyNode>();

    // 다중 스레드 Executor 사용
    rclcpp::executors::MultiThreadedExecutor executor;
    executor.add_node(node);
    executor.spin();

    rclcpp::shutdown();
    return 0;
}

MultiThreadedExecutor를 사용하면, 변환 수신 콜백과 변환 대기 콜백이 서로 다른 스레드에서 실행되므로 교착 상태를 회피할 수 있다.

6.3 해결책 2: 타이머 기반 재시도 패턴

블로킹 대기 대신, 타이머를 활용하여 주기적으로 변환 가용성을 확인하는 비동기 패턴이다.

class WaitForTransformNode : public rclcpp::Node
{
public:
    WaitForTransformNode()
    : Node("wait_node"), transform_ready_(false)
    {
        tf_buffer_ = std::make_unique<tf2_ros::Buffer>(get_clock());
        tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);

        // 100ms 간격으로 변환 가용성 확인
        check_timer_ = create_wall_timer(
            std::chrono::milliseconds(100),
            std::bind(&WaitForTransformNode::checkTransform, this));
    }

private:
    void checkTransform()
    {
        if (transform_ready_) {
            return;  // 이미 준비 완료
        }

        if (tf_buffer_->canTransform(
                "map", "base_link", tf2::TimePointZero)) {
            RCLCPP_INFO(get_logger(), "Transform is now available!");
            transform_ready_ = true;
            check_timer_->cancel();  // 타이머 중지
            startMainLoop();         // 주요 로직 시작
        } else {
            RCLCPP_DEBUG(get_logger(),
                "Waiting for transform...");
        }
    }

    void startMainLoop()
    {
        main_timer_ = create_wall_timer(
            std::chrono::milliseconds(50),
            std::bind(&WaitForTransformNode::mainCallback, this));
    }

    void mainCallback()
    {
        try {
            auto t = tf_buffer_->lookupTransform(
                "map", "base_link", tf2::TimePointZero);
            processTransform(t);
        } catch (const tf2::TransformException & ex) {
            RCLCPP_WARN(get_logger(), "%s", ex.what());
        }
    }

    void processTransform(
        const geometry_msgs::msg::TransformStamped & t)
    {
        // 변환 데이터 처리
    }

    std::unique_ptr<tf2_ros::Buffer> tf_buffer_;
    std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
    rclcpp::TimerBase::SharedPtr check_timer_;
    rclcpp::TimerBase::SharedPtr main_timer_;
    bool transform_ready_;
};

6.4 해결책 3: tf2_ros::MessageFilter 활용

tf2_ros::MessageFilter는 특정 토픽의 메시지가 수신될 때, 해당 메시지의 타임스탬프에 대한 변환이 가용해질 때까지 자동으로 대기한 후 콜백을 호출한다. 이는 센서 데이터 처리 파이프라인에서 가장 권장되는 방법이다.

#include <tf2_ros/message_filter.h>
#include <message_filters/subscriber.h>

class SensorProcessorNode : public rclcpp::Node
{
public:
    SensorProcessorNode()
    : Node("sensor_processor")
    {
        tf_buffer_ = std::make_unique<tf2_ros::Buffer>(get_clock());
        tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);

        // 메시지 필터 서브스크라이버
        point_sub_.subscribe(this, "point_topic");

        // TF2 메시지 필터: 변환 가용 시에만 콜백 실행
        tf_filter_ = std::make_shared<tf2_ros::MessageFilter<
            geometry_msgs::msg::PointStamped>>(
                point_sub_,
                *tf_buffer_,
                "map",       // 타깃 프레임
                10,          // 큐 크기
                this->get_node_logging_interface(),
                this->get_node_clock_interface(),
                tf2::durationFromSec(1.0));  // 타임아웃

        tf_filter_->registerCallback(
            std::bind(&SensorProcessorNode::pointCallback,
                      this, std::placeholders::_1));
    }

private:
    void pointCallback(
        const geometry_msgs::msg::PointStamped::SharedPtr msg)
    {
        // 이 콜백은 변환이 가용할 때에만 호출된다
        auto t = tf_buffer_->lookupTransform(
            "map", msg->header.frame_id, msg->header.stamp);
        // 변환 적용 및 처리
    }

    std::unique_ptr<tf2_ros::Buffer> tf_buffer_;
    std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
    message_filters::Subscriber<geometry_msgs::msg::PointStamped> point_sub_;
    std::shared_ptr<tf2_ros::MessageFilter<
        geometry_msgs::msg::PointStamped>> tf_filter_;
};

7. 타임아웃 설정 가이드라인

상황권장 타임아웃근거
시스템 초기화5~30초네트워크 및 노드 시작 지연 고려
실시간 제어 루프0초 (제로 타임아웃)제어 주기 위반 방지
센서 콜백50~200 ms센서 주기의 1~2배
일회성 조회1~5초합리적 대기 시간
진단 점검100~500 ms주기적 상태 확인

8. 요약

TF1의 waitForTransform()은 ROS 2 TF2에서 독립적인 API로 존재하지 않으며, canTransform()의 타임아웃 파라미터, lookupTransform()의 타임아웃 오버로드, 또는 tf2_ros::MessageFilter를 통해 동일한 기능을 구현한다. 블로킹 대기를 사용할 때에는 Executor의 스레딩 모델과의 호환성을 반드시 고려해야 하며, 가능하면 비동기 패턴을 선호하는 것이 시스템의 안정성과 응답성을 보장하는 데 유리하다.


참고 문헌 및 출처

  • ROS 2 공식 문서, “tf2_ros::Buffer Class Reference,” https://docs.ros2.org/latest/api/tf2_ros/classtf2__ros_1_1Buffer.html (ROS 2 Humble Hawksbill)
  • ROS Wiki, “tf/Overview/Using Published Transforms,” https://wiki.ros.org/tf/Overview/Using%20Published%20Transforms (ROS 1 Legacy)
  • Open Robotics, “tf2 Migration Guide,” https://docs.ros.org/en/humble/Tutorials/Intermediate/Tf2/Migration.html
  • Foote, T., “tf: The Transform Library,” Proceedings of IEEE Conference on Technologies for Practical Robot Applications (TePRA), 2013.
  • ROS 2 소스 코드, “geometry2 리포지토리,” https://github.com/ros2/geometry2