# tf2 구조와 동작 원리

#### 전체 구조 개요

tf2는 ROS2에서 좌표계 사이의 변환 관계를 실시간으로 추적하고 필요한 시점에 적절한 변환을 제공하기 위해 만들어졌다. 좌표계를 의미하는 frame이 서로 연결되어 있으면, 이를 트리 형태로 구성하여 한 frame에서 다른 frame으로의 변환을 즉시 계산할 수 있도록 한다. tf2는 이 변환 정보를 `TransformStamped` 메시지 형태로 송신하거나 수신하며, 내부적으로 캐싱된 버퍼를 통해 시간 축을 고려한 과거 상태의 변환도 조회할 수 있다.

좌표계는 대부분 root에 해당하는 월드 좌표계나 로봇 베이스 링크를 기준으로 잡고, 이후 조인트나 센서, 도구(tool) 좌표계 등을 자식 노드로 설정한다. ROS2의 tf2는 이 트리 구조가 순환(루프)을 만들어선 안 되도록 강제하며, 여러 노드들이 서로 다른 부분 트리를 방송하더라도 전체적으로 단일 트리로 합쳐지도록 한다.

tf2에서 제공되는 주요 구성 요소로는 transform 메시지를 송수신하는 브로드캐스터(transform broadcaster), 내부적으로 transform 정보를 캐싱하고 필요한 시점의 transform을 제공하는 버퍼(buffer), 이를 매개로 하여 실제 transform을 질의할 수 있게 하는 리스너(transform listener)가 있다. 이러한 구성은 ROS2 네트워크를 통해 확장 가능하며, 여러 노드가 동시에 다양한 좌표계를 브로드캐스팅해도 tf2 버퍼에서 이를 시간에 따라 일관성 있게 취합한다.

#### 좌표변환 표현

tf2가 다루는 좌표변환은 3차원 공간에서의 회전과 평행이동의 조합으로 구성된다. 이를 선형대수로 나타내면 회전행렬과 이동벡터로 표현되며, 실제 구현 단계에서는 쿼터니언과 3차원 벡터의 조합으로 사용된다. 일반적으로 동차변환행렬 형태는 다음과 같다.

$$
\mathbf{T}*{A \to B} =  \begin{pmatrix} R*{A \to B} & \mathbf{p}\_{A \to B} \ \mathbf{0} & 1 \end{pmatrix}
$$

위 식에서 $R\_{A \to B}$는 $A$ 좌표계를 $B$ 좌표계로 회전시키는 $3 \times 3$ 행렬이고, $\mathbf{p}\_{A \to B}$는 $B$ 좌표계 원점을 $A$ 좌표계 기준으로 표현한 3차원 위치 벡터다. tf2는 쿼터니언으로 회전을 표현하므로, 다음과 같은 관계로 변환된다.

$$
\mathbf{q}*{A \to B} = (w, x, y, z),  \quad \mathbf{p}*{A \to B} = (p\_x, p\_y, p\_z)
$$

쿼터니언은 회전을 내재적으로 표현해 짐벌락 문제를 피하고, 행렬보다 상대적으로 적은 저장 공간과 빠른 연산성 등을 제공한다. 다만 tf2 내부에서는 원하는 시점에 따라 이 쿼터니언과 벡터를 가져와서 동차변환행렬 형태로도 변환할 수 있다.

좌표계가 트리 구조로 연결되어 있다면, 임의의 $A$와 $C$ 좌표계 사이의 변환은 중간 노드 $B$를 통해 다음처럼 계산할 수 있다.

$$
\mathbf{T}*{A \to C} = \mathbf{T}*{B \to C} \cdot \mathbf{T}\_{A \to B}
$$

이 식은 행렬 곱셈으로 구현되거나, tf2의 API를 통해 단순한 함수 호출로도 얻을 수 있다. tf2에서는 이러한 변환 체인 계산 과정을 내부적으로 최적화하여, 신뢰할 수 있는 결과를 빠르게 반환한다.

#### 시간 동기화와 버퍼

tf2는 시간 축을 반드시 고려한다. 이는 로봇이 움직이거나 센서들이 서로 다른 주기로 데이터를 발행할 때, 일관된 좌표계 정보를 가져오기 위해 필수적이다. tf2 버퍼는 소정의 길이를 갖는 변환 히스토리를 보유한다. 사용자는 특정 시각의 transform을 질의하여, 그 시점에 올바른 변환 관계를 얻을 수 있다. 과거 시점의 tf 정보가 필요 없다면, 가장 최근 정보를 바로 얻어올 수 있다.

tf2는 다음과 같은 절차로 시간 정보를 처리한다. transform을 브로드캐스팅할 때, 현재 시각(ROS2의 시간 관리가 적용된 rclcpp::Time 등을 사용)을 $TransformStamped$ 메시지에 함께 담아 송신한다. 이 정보를 수신한 tf2 버퍼는 메시지에 담긴 시각을 기반으로 내부 캐시에 저장한다. 추후 사용자가 특정 시각의 변환을 요청할 경우, tf2 버퍼는 해당 시각과 가장 인접한 변환 또는 정확히 일치하는 타임스탬프가 있다면 그 정보를 반환한다.

버퍼의 길이는 파라미터로 설정 가능하다. 지나치게 길면 메모리 사용량과 검색 시간이 늘어나고, 너무 짧으면 과거 시점의 변환을 제대로 보관하지 못한다. 따라서 로봇 시스템의 동작 속도나 센서 주기 등을 고려하여 적절히 설정해야 한다.

#### tf2 데이터 흐름 예시

mermaid를 간단히 이용해 tf2가 좌표계를 어떻게 주고받는지 나타내면 다음과 같은 흐름이 나타난다.

{% @mermaid/diagram content="flowchart LR
A\["Robot State Publisher<br>(브로드캐스터)"] -->|transform 메시지| B\[tf2 Buffer]
C\["Sensor Node<br>(브로드캐스터)"] -->|transform 메시지| B\[tf2 Buffer]
B\[tf2 Buffer] -->|lookupTransform| D\[Application Node]
D\[Application Node] -->|질의 시점 지정| B\[tf2 Buffer]" %}

여기서 Robot State Publisher는 로봇의 조인트 변환이나 베이스 링크 정보를, Sensor Node는 센서의 장착 위치 또는 장착 각도 정보를 tf2에 브로드캐스팅한다고 가정한다. 이렇게 수집된 변환은 tf2 Buffer에 저장되어, Application Node가 필요로 할 때 원하는 시점의 변환을 조회한다. 필요한 변환은 브로드캐스팅된 여러 프레임 간 트리를 거슬러 올라가 연쇄적으로 합성하여 얻는다.

#### 코드 예시

ROS2 C++ 노드에서 tf2를 활용해 특정 좌표계 변환을 조회해 보는 예시는 아래와 비슷하다.

```c++
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <tf2_ros/buffer.h>
#include <tf2_ros/transform_listener.h>
#include <geometry_msgs/msg/transform_stamped.hpp>

class TransformLookupNode : public rclcpp::Node
{
public:
  TransformLookupNode()
  : Node("transform_lookup_node"),
    buffer_(std::make_shared<tf2_ros::Buffer>(this->get_clock())),
    listener_(std::make_shared<tf2_ros::TransformListener>(*buffer_))
  {
    timer_ = this->create_wall_timer(
      std::chrono::seconds(1),
      std::bind(&TransformLookupNode::onTimer, this));
  }

private:
  void onTimer()
  {
    try {
      auto transform_stamped =
        buffer_->lookupTransform("base_link", "camera_link", tf2::TimePointZero);
      RCLCPP_INFO(this->get_logger(), "Got transform from base_link to camera_link");
    } catch (const tf2::TransformException & ex) {
      RCLCPP_WARN(this->get_logger(), "Failed to get transform: %s", ex.what());
    }
  }

  std::shared_ptr<tf2_ros::Buffer> buffer_;
  std::shared_ptr<tf2_ros::TransformListener> listener_;
  rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<TransformLookupNode>());
  rclcpp::shutdown();
  return 0;
}
```

이 예시는 ROS2 rclcpp Node에서 `tf2_ros::Buffer`와 `tf2_ros::TransformListener`를 생성하여 일정 간격으로 특정 프레임 쌍에 대한 변환을 조회하고 있다. 필요한 시점이 명시되지 않으면 `tf2::TimePointZero`가 기본으로 사용되어, 가장 최근 타임스탬프를 기준으로 변환이 반환된다.

#### Transform Broadcaster

tf2에서 Transform Broadcaster는 좌표계 변환 정보를 외부로 전달하는 책임을 갖는다. 특정 노드 내부에서 `TransformStamped` 메시지를 생성한 뒤, 이 메시지를 ROS2 토픽(tf, tf\_static 등)에 발행하면, tf2 버퍼나 다른 노드들이 이를 수신하여 변환을 이용할 수 있다. 브로드캐스터를 통해 전달되는 정보에는 부모 프레임(frame\_id), 자식 프레임(child\_frame\_id), 변환 시점의 타임스탬프, 회전 및 평행이동 값이 포함된다.

각 노드는 보통 자신이 “소유”하거나 알고 있는 좌표계(예: 센서 좌표계, 로봇 베이스 링크, 조인트 링크 등)에 대한 변환만 브로드캐스팅한다. 로봇 내부에서 센서나 조인트 위치가 변경될 때마다 주기적으로 업데이트하여, 다른 노드가 변환을 올바르게 추적할 수 있도록 한다.

만약 변환 정보가 상대적으로 고정되어 있고 거의 변하지 않는다면, 정적 변환을 한번만 브로드캐스팅하여 전체 tf2 구조에서 이를 참조할 수도 있다. 이를 tf\_static 토픽으로 내보낼 수 있는데, 굳이 실시간으로 여러 번 반복 송신할 필요가 없는 상황에서 이 방법이 유용하다.

브로드캐스터는 코드에서 직접 `TransformStamped` 메시지를 생성해도 되고, ROS2에서 제공하는 `tf2_ros::TransformBroadcaster` 클래스를 사용하여 좀 더 간단한 API로 송신할 수도 있다.

예시 코드는 다음과 유사하게 작성할 수 있다.

```c++
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <tf2_ros/transform_broadcaster.h>
#include <geometry_msgs/msg/transform_stamped.hpp>

class MyBroadcasterNode : public rclcpp::Node
{
public:
  MyBroadcasterNode()
  : Node("my_broadcaster_node")
  {
    broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(this);
    timer_ = this->create_wall_timer(
      std::chrono::milliseconds(100),
      std::bind(&MyBroadcasterNode::broadcastTransform, this));
  }

private:
  void broadcastTransform()
  {
    geometry_msgs::msg::TransformStamped transform_msg;
    transform_msg.header.stamp = this->now();
    transform_msg.header.frame_id = "base_link";
    transform_msg.child_frame_id = "some_sensor_link";
    transform_msg.transform.translation.x = 0.5;
    transform_msg.transform.translation.y = 0.0;
    transform_msg.transform.translation.z = 1.2;
    transform_msg.transform.rotation.x = 0.0;
    transform_msg.transform.rotation.y = 0.0;
    transform_msg.transform.rotation.z = 0.0;
    transform_msg.transform.rotation.w = 1.0;

    broadcaster_->sendTransform(transform_msg);
  }

  rclcpp::TimerBase::SharedPtr timer_;
  std::shared_ptr<tf2_ros::TransformBroadcaster> broadcaster_;
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MyBroadcasterNode>());
  rclcpp::shutdown();
  return 0;
}
```

위 코드에서는 timer를 통해 주기적으로 `TransformStamped` 메시지를 생성하여 브로드캐스팅한다. `transform_msg` 내에는 시간, 부모 프레임, 자식 프레임, 변환(평행이동 + 회전)을 설정한다. tf2는 이러한 transform 메시지를 수신하여 내부 버퍼에 기록한다.

#### Transform Listener

Transform Listener는 반대로 tf2 버퍼가 발행 혹은 축적한 변환 정보를 읽어들이고, 노드에서 이를 쉽게 질의할 수 있도록 한다. 일반적으로 `tf2_ros::TransformListener` 클래스는 `tf2_ros::Buffer`를 인자로 받아, ROS2 토픽(tf, tf\_static 등)으로부터 변환 정보를 구독하고 버퍼에 저장한다. 사용자는 `buffer_->lookupTransform( ... )` 함수 등을 호출하여, 원하는 시점의 변환을 가져올 수 있다.

Listener는 ROS2 네트워크 상에서 들어오는 `TransformStamped` 메시지에 담긴 타임스탬프를 확인하여 해당 시점의 변환을 버퍼에 누적한다. 기존 변환의 자식 프레임, 부모 프레임 관계가 일치한다면 덮어쓰거나 보간 등의 처리를 수행해, 동일 프레임 쌍에 대해 시간이 달라진 변환을 동시에 보관한다. 이렇게 누적된 변환 히스토리를 통해 필요 시점에 대한 변환을 역산할 수 있다.

#### 트랜스폼 체인과 합성

tf2가 다루는 주요 작업은, 여러 프레임의 변환을 연결하여 임의의 두 프레임 사이의 변환을 구하는 것이다. $A$ 프레임에서 $C$ 프레임으로 가는 변환을 구하고 싶을 때, tf2는 내부적으로 $A \to B$, $B \to C$ 변환을 합성하여 $A \to C$로 만든다. 변환의 합성은 평행이동 벡터와 회전(쿼터니언 또는 행렬) 연산으로 실현되는데, 이때 두 변환을 연결하는 과정은 다음과 같은 행렬 곱 형태로 볼 수 있다.

$$
\mathbf{T}*{A \to C}(t) = \mathbf{T}*{B \to C}(t) \cdot \mathbf{T}\_{A \to B}(t)
$$

시점 $t$에서 정의된 변환을 모두 곱해 최종 결과를 구한다. 여기서 $B$ 프레임이 필요하지 않다면, 더 간단히 $A$에서 $C$로 직접 연결된 변환을 조회할 수 있다. 하지만 tf2 내부에서는 트리를 순회하여 최종 변환을 구하는 과정을 자동으로 처리해 주므로, 사용자는 최단 경로를 직접 계산할 필요가 없다.

#### 인터폴레이션과 엑스트라폴레이션

tf2에서 시간 축이 중요한 이유 중 하나가, 센서나 로봇 링크의 상태가 시시각각 변한다는 점이다. 동일한 프레임 쌍이라고 해도, 시점 $t\_1$과 $t\_2$ 사이에 로봇이나 센서가 움직이면 변환 값이 달라진다. tf2는 다음과 같은 방식을 지원한다.

현재 시점 $t$에 대해 정확히 일치하는 타임스탬프의 변환이 존재하면 바로 반환한다. 만약 $t$ 사이에 존재하는 변환 정보가 없다면, 이전 시점 $t\_1$과 이후 시점 $t\_2$ 중 가장 가까운 변환을 반환하거나, 두 변환을 선형 보간(interpolate)할 수 있다. 만약 요청한 시점 $t$이 tf2 버퍼에 기록된 가장 최근 시점보다 더 앞서 있거나, 훨씬 미래 시점이라면 엑스트라폴레이션(extrapolation) 오류가 발생할 수 있다.

tf2 내부는 보간을 통해 시간 축에서 부드럽게 변환을 계산하기 때문에, 자잘한 타임스탬프 차이로 인한 불일치를 줄이는 효과가 있다. 단, 데이터가 너무 오래되었거나, 미래 시점으로부터 변환을 조회한다면 신뢰할 수 없는 결과로 판단하여 예외를 던진다. 이는 로봇 운영에서 대단히 중요한 안정장치이기도 하다.

#### 고급 활용: 동적 프레임 추가

로봇 구동 중에 새로이 좌표계를 추가해야 하는 상황이 생길 수 있다. 예를 들어, AGV(무인 운반 로봇)가 특정 지점에 접근해 새로운 장치를 장착했다면, 그 장치의 좌표계를 tf2에 편입해야 한다. tf2는 네트워크 전역에서 한 번이라도 브로드캐스팅된 좌표계를 인식하면, 트리 구조의 일부로 편입한다.

이때 주의해야 할 점은, 프레임 이름이 중복되지 않아야 하며, 부모-자식 관계가 순환 고리를 만들지 않도록 적절히 배치해야 한다. 로봇이 장치를 착탈하는 시점에 프레임을 새롭게 생성하거나 제거해도, 전체 tf2 구조에 혼선이 없도록 계획적으로 구현하는 것이 중요하다.

#### tf2 내의 동기화 기제

tf2는 ROS2의 퍼블리셔-서브스크라이버 모델을 기반으로 동작하지만, 변환 히스토리를 자체적으로 관리한다는 특징이 있다. `tf2_ros::Buffer` 객체는 수신된 `TransformStamped` 메시지들을 타임스탬프에 맞춰 보관하고, 사용자가 임의 시점의 변환을 요청했을 때 가장 근접하거나 적절한 시점의 변환을 반환한다.

프레임 트리 관점에서는, tf2가 네트워크 상의 모든 transform 메시지를 모아 하나의 방향성 비순환 그래프(DAG) 형태로 유지한다고 볼 수 있다. 실제로는 트리 구조이지만, 동적 상황에서 임시적으로 DAG 형태로 남을 수 있다. tf2 버퍼가 쿼리에 대응하여 필요한 경로를 찾고, 보간 혹은 정확히 매칭되는 시간 정보를 찾아 최종 변환을 만들어 낸다.

동기화를 위해 ROS2의 콜백 큐 또는 Executor가 메시지를 순차적으로 처리하며, tf2 버퍼가 이 과정에서 최신 transform 정보를 쌓는다. lookupTransform 요청이 들어오면, 이미 쌓인 정보 중 적합한 타임스탬프를 찾거나, 적정한 보간 값을 계산하여 반환한다.

#### tf2 Debugging

tf2를 디버그하거나 트리 구조를 확인할 때, ROS2에는 tf2\_tools 등 다양한 패키지나 명령줄 도구가 제공된다. 예를 들어 다음과 유사한 명령을 통해 현재 tf 트리 상태를 텍스트로 확인할 수 있다.

```
ros2 run tf2_tools view_frames
```

이는 tf2 버퍼가 인식하고 있는 프레임들을 그래프 형태로 SVG 파일 등에 출력해주어, 연결 구조와 시간 지연 등을 파악하기 쉽게 해준다. 순환 구조가 있는지, 특정 프레임이 고립되어 있는지를 시각적으로 쉽게 확인할 수 있다.

또한 `$lookupTransform$를 반복적으로 호출하는 노드에서 tf2::TransformException`이 발생하면, 잘못된 프레임 이름이거나, 시점이 버퍼 범위를 벗어났거나, 아직 transform이 수신되지 않았다는 신호일 수 있다. 예외 메시지를 통해 원인을 파악하고, 브로드캐스터 측에서 해당 변환을 제대로 송신하고 있는지, 시간을 맞게 설정했는지 확인해야 한다.

#### 추가 예시: 브로드캐스터와 리스너를 동시에 쓰는 노드

특정 노드가 외부에서 들어오는 센서 변환을 수신하고, 이를 토대로 로봇의 다른 요소를 계산한 뒤 새로운 transform을 다시 브로드캐스팅해야 할 수도 있다. C++ 예시를 보이면 다음과 같은 형태가 가능하다.

```c++
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <tf2_ros/buffer.h>
#include <tf2_ros/transform_listener.h>
#include <tf2_ros/transform_broadcaster.h>
#include <geometry_msgs/msg/transform_stamped.hpp>
#include <tf2/LinearMath/Quaternion.h>

class HybridNode : public rclcpp::Node
{
public:
  HybridNode()
  : Node("hybrid_node")
  {
    buffer_ = std::make_shared<tf2_ros::Buffer>(this->get_clock());
    listener_ = std::make_shared<tf2_ros::TransformListener>(*buffer_);
    broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(this);

    timer_ = this->create_wall_timer(
      std::chrono::milliseconds(100),
      std::bind(&HybridNode::onTimer, this));
  }

private:
  void onTimer()
  {
    try {
      auto transform_in =
        buffer_->lookupTransform("base_link", "tool_frame", tf2::TimePointZero);

      // 임의 계산 예시: tool_frame에 대한 새 프레임을 10cm 전방에 생성
      geometry_msgs::msg::TransformStamped transform_out;
      transform_out.header.stamp = this->now();
      transform_out.header.frame_id = "tool_frame";
      transform_out.child_frame_id = "extension_frame";
      transform_out.transform.translation.x = 0.1;
      transform_out.transform.translation.y = 0.0;
      transform_out.transform.translation.z = 0.0;

      tf2::Quaternion q;
      q.setRPY(0.0, 0.0, 0.0);
      transform_out.transform.rotation.x = q.x();
      transform_out.transform.rotation.y = q.y();
      transform_out.transform.rotation.z = q.z();
      transform_out.transform.rotation.w = q.w();

      broadcaster_->sendTransform(transform_out);
    } catch (const tf2::TransformException & ex) {
      RCLCPP_WARN(this->get_logger(), "Lookup failed: %s", ex.what());
    }
  }

  std::shared_ptr<tf2_ros::Buffer> buffer_;
  std::shared_ptr<tf2_ros::TransformListener> listener_;
  std::shared_ptr<tf2_ros::TransformBroadcaster> broadcaster_;
  rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<HybridNode>());
  rclcpp::shutdown();
  return 0;
}
```

위 노드는 외부에서 “base\_link”와 “tool\_frame” 사이의 변환을 조회한 다음, 그 정보를 활용하여 새 프레임 “extension\_frame”을 “tool\_frame” 아래에 붙여 브로드캐스팅한다. 실제 상황에서는 이처럼 들어오는 좌표계를 기반으로 동적 계산이나 로봇 팔 움직임 예측 등을 수행한 뒤, 결과 좌표계를 추가적으로 전파할 수 있다.

#### 다중 로봇 시나리오와 tf2

tf2는 단일 로봇에 한정되지 않고, 여러 대의 로봇이 동시에 움직이는 상황에서도 유용하다. 예컨대 두 로봇이 서로 다른 위치에서 작업할 때, 각각의 로봇 내부에는 별도의 좌표계 트리가 존재할 수 있다. 이 경우 두 로봇이 서로 독립적인 tf2 프레임 세트를 유지하면서도, 필요하다면 공통 월드 좌표계(world\_frame 등)를 공유하여 전체 프레임 트리를 상호 연결할 수 있다.

만약 여러 로봇 간의 상호 추적이 필요한 경우, 각 로봇이 자신이 인지한 월드 프레임 또는 로컬 맵 프레임(local\_map 등)을 브로드캐스팅하고, 이를 서로 교환하거나 합성하여 공동 작업이 가능해진다. 단, 로봇 간 통신 지연이나 위치 추정 오차가 있으면 tf2에서 두 로봇의 좌표계를 일관성 있게 합치기가 까다로울 수 있다. 따라서 전역에서 공유되는 정확한 월드 프레임(예: GPS 기반, SLAM에서 추정된 전역 맵 프레임 등)을 마련하고, 각 로봇이 이를 기준으로 자신의 위치를 tf2로 송신하는 방식을 자주 사용한다.

#### 네이밍 규약과 프레임 계층 구조

tf2 프레임 이름을 지을 때는 중복을 피하고, 해당 프레임이 어떤 역할을 하는지 명확히 알 수 있도록 하는 것이 좋다. 일반적으로 다음과 같은 양식이 권장된다. 예를 들어 로봇 베이스 링크를 "base\_link"라 명명하고, 이 로봇의 휠 좌표계를 "left\_wheel", "right\_wheel" 등으로 지정한다. 센서가 달려 있다면 "lidar\_link", "camera\_link"처럼 센서 종류를 명시한다. 기본적으로 가장 상위에 “map”이나 “odom” 프레임이 있고, 그 아래에 “base\_link”가 위치하며, 조인트나 센서 좌표계가 순차적으로 연결되는 계층 구조가 일반적이다. 이렇게 하면 transform 요청 시 직관적으로 부모-자식 관계를 파악하기 쉽다.

#### tf2의 성능과 최적화

tf2는 내부적으로 변환 정보를 버퍼링하고, 시간 축에 따른 보간을 수행해야 하므로 성능이 중요해지는 경우가 많다. 로봇이 빠르게 움직이거나, 조인트 변환 정보가 짧은 간격(고주파)으로 들어올 때 tf2 버퍼와 연산이 병목이 되지 않도록 관리해야 한다.

버퍼 히스토리의 길이를 과도하게 길게 잡으면 많은 양의 변환 정보를 저장하느라 메모리를 많이 소비하고, 보간 시에도 검색 범위가 넓어져 연산 비용이 증가한다. 반대로 히스토리 길이가 너무 짧으면 과거 시점의 변환을 제대로 조회할 수 없게 된다. 로봇이나 센서가 요구하는 시간 동기화의 정확도, 데이터 흐름의 특성 등을 고려하여 buffer length(기본값은 보통 몇 초 정도)와 브로드캐스팅 주기를 조정하는 것이 좋다.

tf2는 멀티스레딩 환경에서 동작 가능하다. ROS2 자체가 콜백 함수들을 여러 스레드로 분산할 수 있기 때문에, 브로드캐스터와 리스너가 동시에 작동해도 서로 간섭하지 않고 메시지를 주고받을 수 있다. 단, 사용자가 $lookupTransform$ 같은 tf2 API를 호출할 때는, 해당 함수가 던지는 예외처리를 잘 처리해주어야 한다. 특정 시점에 tf2 버퍼에 해당 변환이 없으면 TransformException을 발생시킬 수 있다.

#### tf2와 시뮬레이션 환경

Gazebo나 Isaac Sim 등의 시뮬레이터에서 로봇 모델을 구동할 때도 tf2는 동일하게 사용된다. 시뮬레이터가 로봇의 링크 상태(조인트, 부착된 센서 등)을 실제 물리 엔진에 맞춰 주기적으로 업데이트하여, robot\_state\_publisher 등이 tf2로 transform 메시지를 내보낸다. 시뮬레이션 환경에서는 실제 하드웨어와 달리 시계(clock)가 가상 시간으로 동작하며, tf2도 이 가상 시간에 맞춰 변환을 축적한다.

시뮬레이터에서는 실제보다 훨씬 빠른 시간배속 속도로 진행하거나, 반대로 천천히 시뮬레이션할 수도 있으므로 tf2가 사용하는 시간 소스가 rclcpp::Clock를 통해 시뮬레이터와 동기화되는지 확인해야 한다. 로봇이 시뮬레이터 내부에서 빠르게 움직이게 설정하면 tf2 브로드캐스팅 주기를 높여야 과도한 위치 오차나 보간 오류가 발생하지 않는다.

#### tf2에서의 RViz 시각화

ROS2의 시각화 툴인 RViz에서 tf2 프레임을 시각적으로 확인할 수 있다. RViz 내에서 “TF” 디스플레이를 활성화하면, 각 프레임을 좌표축 형태로 그려준다. 이를 통해 로봇이 움직이거나 조인트가 회전할 때 프레임들이 어떻게 변환되는지를 직관적으로 볼 수 있다.

RViz는 또 특정 프레임을 기준으로 다른 프레임을 바라보는 시점(camera view)을 설정할 수 있다. 예컨대 “base\_link”를 기준으로 카메라를 위치시키면, 로봇의 몸체 좌표계를 기준으로 주변 프레임이 어떻게 펼쳐지는지 확인 가능하다. 이는 디버깅이나 로봇 움직임 이해에 큰 도움을 준다.

#### tf2의 정적 변환(static transform)

일부 좌표계 변환은 움직이지 않는 고정값으로 알려져 있을 수 있다. 예를 들어, 로봇 본체에 영구적으로 부착된 레이저 센서의 설치 오프셋은 바뀌지 않는다고 가정할 수 있다. 이런 경우에는 static transform 기능을 사용하여 한 번만 브로드캐스팅하고, 이후 변경이 없도록 처리할 수 있다.

ROS2에서는 정적 변환을 `static_transform_publisher`라는 실행파일 또는 `tf2_ros::StaticTransformBroadcaster` 클래스를 통해 송신할 수 있다. 이 메시지는 토픽 `tf_static`으로 발행되며, tf2가 별도의 버퍼 처리를 하지 않고 상수 변환으로 간주해 쌓아두게 된다. 동적 브로드캐스팅보다 리소스를 적게 쓰고, 불필요한 통신량을 줄일 수 있다는 장점이 있다.

예시로 셸에서 다음 명령을 실행하면, “base\_link”와 “camera\_link” 사이의 고정 변환을 매번 반복적으로 퍼블리시할 필요 없이 단 한 번 지정할 수 있다.

```
ros2 run tf2_ros static_transform_publisher 0 0 1 0 0 0 1 base_link camera_link
```

위 값은 순서대로 $x$, $y$, $z$, $q\_x$, $q\_y$, $q\_z$, $q\_w$, `frame_id`, `child_frame_id`를 의미하며, $q\_x, q\_y, q\_z, q\_w$는 쿼터니언이다.

#### tf2를 사용할 때의 주의사항

tf2로 좌표계를 다룰 때 가장 흔히 발생하는 문제 중 하나는 시간 관련 오류다. 브로드캐스팅 시 타임스탬프를 잘못 설정하면, 예컨대 과거 시점이나 미래 시점으로 변환이 표기되어 `lookupTransform`이 제대로 동작하지 않을 수 있다. ROS2의 시계가 wall clock, system clock, sim time 등 원하는 방식으로 동작하고 있는지 반드시 확인해야 한다.

프레임이 누락되거나 잘못된 이름을 사용해도 tf2가 에러를 발생시킨다. 이를 해결하려면 브로드캐스팅 중인 모든 프레임 이름을 점검하고, RViz나 tf2\_tools 같은 툴로 현재 트리 구조를 점검해본다. 특정 프레임이 중간에서 끊겨 있으면 그 하위 프레임들까지 변환을 볼 수 없으므로, 트리 상에서 부모를 잃어버린 경우가 없는지 살펴야 한다.

ROS2에서 executor나 콜백 함수가 지연되는 경우, 브로드캐스트 주기와 수신 주기가 맞지 않아 tf2 버퍼가 특정 시점의 변환을 놓칠 수도 있다. 이를 방지하기 위해서는 transform 메시지의 발행 주기와 노드의 스레드 할당, QoS 설정 등을 튜닝해야 한다. 고정된 real-time 시스템에서 tf2를 적용할 때는 특히 이 부분을 주의 깊게 설계해야 한다.

#### tf2의 추상화 레벨

tf2는 ROS2의 메시지 통신을 기반으로 동작하지만, 그 자체로는 좌표 변환이라는 추상화된 로직을 제공한다. 내부에서는 quaternions, rotation matrices, homogenous transformation matrices 등을 자유롭게 변환할 수 있고, 시간 보정 및 보간도 수행한다. 단순히 `TransformStamped`의 회전, 평행이동만 다루는 게 아니라, 2D/3D 포인트, 벡터, 레이(ray) 등을 특정 프레임에서 다른 프레임으로 변환하는 기능도 제공한다.

예를 들어 `tf2::doTransform` 함수 계열을 사용하면, `geometry_msgs::msg::Point` 같은 메시지를 다른 프레임 좌표계로 간단히 변환할 수 있다.

```c++
#include <tf2_geometry_msgs/tf2_geometry_msgs.hpp>
// ...
geometry_msgs::msg::Point p_in, p_out;
p_in.x = 1.0;
p_in.y = 2.0;
p_in.z = 3.0;

geometry_msgs::msg::TransformStamped transform_stamped;
transform_stamped = buffer_->lookupTransform("target_frame", "source_frame", tf2::TimePointZero);

tf2::doTransform(p_in, p_out, transform_stamped);
// 이제 p_out은 target_frame 기준 좌표로 나타낸 3차원 점이 됨
```

이는 로봇 내에서 센서 측정 결과를 특정 프레임 기준으로 다시 변환하거나, 로봇 팔 끝단에서 바라본 물체 좌표를 월드 프레임으로 변환하는 등의 작업을 간편히 수행하도록 해준다.

#### tf2와 좌표 변환 라이브러리의 결합

tf2 자체도 Eigen, KDL(Kinematics and Dynamics Library) 등 다양한 수학 라이브러리를 내부 또는 외부 래퍼로 사용한다. 회전 변환 시 쿼터니언을 사용하거나, KDL을 활용해 로봇 조인트 변환을 해석학적으로 풀어내는 등, 필요한 수준에 맞춰 확장 가능하다.

예를 들어, ROS2 Kinematics/Dynamics에 tf2가 결합되면 URDF(Universal Robot Description Format)에서 정의된 로봇 조인트 파라미터를 실시간으로 업데이트해, tf2를 통해 각 링크의 위치를 브로드캐스팅할 수 있다. 이를 기반으로 상위 애플리케이션은 운동학 연산(IK, FK)이나 궤적 계획을 수행하고, 그 결과를 다시 tf2로 반영해 시각화하는 식의 작업이 가능해진다.

***

tf2는 ROS2에서 좌표계 변환을 효율적이고 일관성 있게 관리하기 위한 핵심 툴이다. 트리 구조로 연결된 모든 프레임을 시간 축과 함께 관리하여, 임의 시점과 임의 프레임 쌍에 대한 변환을 신속하고 정확하게 계산해준다. 이를 통해 로봇의 조인트 상태나 센서 측정 데이터를 원하는 좌표계로 쉽게 변환하고, 여러 노드가 동일한 공간적 기준을 공유하면서 유기적으로 협업할 수 있다.

#### tf2와 센서 퓨전

tf2는 다양한 센서에서 들어오는 데이터를 특정 공통 프레임으로 변환하여 퓨전(fusion)하는 작업에 자주 사용된다. 예를 들어 로봇에 LIDAR, 카메라, IMU 등이 동시에 장착되어 있다면, 각 센서가 발행하는 측정값을 하나의 기준 좌표계(예: base\_link, map 등)로 일치시킨 뒤, 센서 퓨전 알고리즘(Kalman Filter, Particle Filter 등)에 적용할 수 있다.

이를 위해서는 각 센서가 로봇 본체에 어떻게 장착되어 있는지, 해당 센서가 움직이는 조인트에 부착되어 있지는 않은지, 그리고 해당 조인트가 어떤 주기로 변환 정보를 브로드캐스팅하는지가 명확해야 한다. tf2가 이런 변환 정보를 시점별로 관리하면, 센서 퓨전 노드는 간단한 transform lookup만으로 모든 데이터를 한 좌표계에 맞춰 정렬하여 처리할 수 있다.

#### tf2와 ROS2 액션(Action) 연동

ROS2에는 토픽, 서비스와 더불어 액션(Action)이라는 통신 방식이 존재한다. 이는 길게 걸리는 작업(예: 로봇 이동, 그리퍼 동작 등)에 대해 피드백과 목표 상태를 주고받기 위해 쓰인다. tf2가 액션과 직접적으로 결합되어 있는 것은 아니지만, 예를 들어 로봇 팔이 특정 위치로 이동하는 액션을 수행하는 동안 tf2를 통해 로봇 팔 끝단(end-effector)의 실시간 위치를 모니터링할 수 있다.

액션 서버는 내부에서 조인트 상태를 모니터링하고, 해당 정보를 tf2 브로드캐스팅할 수 있다. 액션 클라이언트는 tf2 버퍼를 통해 현재 팔 끝단의 자세(orientation)나 위치(position)를 확인해, 목표지점과 얼마나 가까워졌는지를 판단할 수 있다. 이와 같이 tf2는 ROS2 액션의 운용을 보조하는 필수 인프라로 자리 잡는다.

#### tf2와 인공지능(AI) 응용

최근 로봇 분야에서 AI 모델(딥러닝 기반)을 활용할 때도, tf2가 중요한 역할을 한다. 예를 들어 객체 인식 모델이 카메라 이미지에서 특정 물체의 픽셀 좌표를 추출했다면, 이를 카메라 링크 좌표계를 기반으로 3D 투영(Projection)하여 물체의 실제 위치를 추정해야 할 수도 있다. 이때 tf2가 “camera\_link”와 “base\_link”, 또는 “map” 프레임 사이의 변환을 제공해주면, 쉽게 물체의 실제 좌표를 다른 프레임 기준으로 변환할 수 있다.

특히 AI 모델 출력이 2D 이미지 좌표라면, 로봇 내의 깊이 카메라(depth camera)나 LIDAR 정보를 결합해서 3D 위치를 구한 다음, tf2를 통해 최종 프레임(예: manipulator\_link)으로 변환하여 로봇 팔이 잡을 수 있는 형태로 좌표를 넘겨줄 수 있다. 이러한 과정에서 tf2가 자동으로 시간 동기화와 프레임 간 변환을 처리해주므로, AI 모델 쪽에서는 단순히 픽셀 좌표 → 실제 좌표 변환 로직만 구현하면 된다.

#### tf2와 고정밀(High Precision) 요구사항

일부 로봇 응용에서는 서보 모터나 센서가 매우 정밀하게 동작해야 하고, mm 단위 이하의 오차를 허용하지 않을 수도 있다. tf2는 로봇 OS에서 비교적 범용으로 쓰이는 패키지이므로, 실시간성 보장이나 극단적인 고정밀 요구사항에는 추가적인 주의가 필요하다.

ROS2 Real-Time용 디스트리뷰션이나 RT 커널 환경에서 tf2를 사용할 때는, transform 메시지가 가능한 한 낮은 지연으로 전달되도록 Executor 설정, QoS 설정, 스레드 우선순위 등을 조율해야 한다. 또한 tf2가 내부적으로 사용하는 부동소수점 연산에서 발생할 수 있는 작은 오차(쿼터니언 정규화나 행렬 곱 연산 등)에 대해서도 점검해볼 필요가 있다. 보통은 double precision 정도로 충분하지만, 극단적 환경이라면 별도의 정밀 연산 라이브러리를 고려하기도 한다.

#### tf2와 URDF, SRDF

ROS2에서 로봇을 기술하는 방식으로 URDF(Universal Robot Description Format)가 널리 사용된다. URDF에는 링크와 조인트(회전, 직선 이동 등) 정보가 정의되는데, robot\_state\_publisher 등의 노드가 URDF 파라미터를 읽어 각 링크의 변환을 계산하고, 이를 tf2로 퍼블리시한다. 이렇게 되면 로봇의 실제 조인트각과 URDF에 정의된 운동학 모델이 결합하여, 각 링크 프레임이 시간에 따라 업데이트된다.

SRDF(Semantic Robot Description Format)는 주로 MoveIt 등에서 로봇 팔의 작업영역, 조인트 제약사항 등을 정의하는데, tf2와 직접 연동되는 부분은 적지만, 결국에는 URDF와 함께 구동되어 로봇의 전체 kinematic chain을 tf2로 반영하게 된다. 따라서 tf2 구조를 설계할 때 URDF 상의 링크 트리가 곧 tf 프레임 트리로 대응된다는 점을 이해하고 구성하는 것이 중요하다.

#### tf2와 다른 좌표 변환 라이브러리 비교

ROS1 시절에는 tf, tf2가 모두 존재했으며, tf2가 더 발전된 구조와 성능을 제공하기 위해 개발되었다. ROS2에서는 기본적으로 tf2만 사용한다. 한편, ROS 생태계 바깥에서 흔히 사용하는 라이브러리(Eigen, Ceres, Sophus 등)와 비교했을 때, tf2는 ROS 메시지 형태(`TransformStamped`, `geometry_msgs::msg::Pose` 등)와 밀접하게 연동되어 있다는 점이 가장 큰 차이점이다.

또한 tf2는 “시간” 개념을 핵심으로 포함하고, 네트워크를 통한 브로드캐스팅/리스닝 모델을 제공한다는 점이 일반적인 선형대수 라이브러리와 다르다. 예를 들어 Eigen은 행렬, 벡터 연산을 매우 효율적으로 제공하지만, ROS 메시지 통신을 자동 처리해주지는 않는다. 반면 tf2는 네트워크 상에서 각 프레임 변환을 주고받고, 시간 축을 고려하며, 트리 순회를 통해 임의의 프레임 쌍에 대한 변환을 연쇄적으로 구해주는 통합 솔루션을 제공한다.

#### tf2 수학적 배경: 쿼터니언과 행렬

쿼터니언은 4차원 실수공간에서의 한 원소로, 3차원 공간 회전을 불변축(rotation axis)과 회전각을 통해 효율적으로 표현한다. 행렬 변환과 달리 짐벌락(gimbal lock) 문제를 회피하고, 연산이 비교적 단순하다는 장점이 있다. tf2는 내부에서 회전을 주로 쿼터니언 형태로 저장하고, 변환 계좌를 필요하면 $3 \times 3$ 회전행렬로 바꿔 쓸 수 있도록 한다.

회전행렬 $R$을 쿼터니언 $\mathbf{q}$로 변환하기 위해서는 특성 방정식과 고윳값을 이용하거나, 공식화된 수식을 쓸 수도 있다. 일반적으로 $R$의 대각원소를 활용해 $q\_w$를 구한 뒤, 나머지 성분($q\_x, q\_y, q\_z$)을 역추론한다. tf2는 이러한 변환을 C++ 템플릿 함수로 제공하고 있어, 사용자가 직접 구현할 필요가 크게 줄어든다.

#### tf2와 스케일링, 비정규(Non-Uniform) 변환

tf2는 주로 순수 회전(rotation)과 평행이동(translation)에 집중한다. 로봇 공학에서 좌표계를 스케일링(크기 축소/확대)하거나 비정규 직교변환(예: 전단/shear)을 적용하는 경우는 드물다. 따라서 tf2는 이러한 스케일링이나 전단 변환을 다룰 수 있는 API를 제공하지 않는다. 이는 KDL이나 Eigen 등을 직접 사용해 구현해야 한다.

만약 특정 CAD 모델이나 그래픽 표현에서 스케일 조정이 필요하다면, tf2 밖에서 처리하거나, URDF에서 링크를 정의할 때 해당 스케일을 미리 반영하는 식으로 구현해야 한다. 결국 tf2는 “직교변환(회전 + 평행이동)”을 전제로 하므로, 이 범위를 넘어서는 변환은 지원 대상이 아니다.

#### tf2와 멀티 도메인(해양, 항공, 우주)

ROS2는 지상 로봇 뿐만 아니라, 해양(수중 드론), 항공(드론), 우주(위성, 로버) 등 다양한 도메인에서도 사용될 수 있다. 이때 tf2를 활용해 물리적 좌표계를 일관성 있게 유지할 수 있다. 예를 들어 무인 잠수정(ROV)이 해류로 인해 흔들리는 상태를 IMU 센서가 측정해서 브로드캐스팅하면, tf2 버퍼는 “world” 프레임(해수면 기준)과 “vehicle\_link” 프레임(ROV 본체)을 매 시점마다 기록한다. 수중 카메라나 로봇 팔을 달았다면, “vehicle\_link”의 하위 프레임으로 추가할 수도 있다.

항공 드론에서도 GPS, IMU, 바람 센서 등이 서로 다른 갱신 주기로 데이터를 내보낼 때, tf2가 시간 축을 동기화해준다. 다만 GPS 좌표계를 UTM 등 지리좌표로 변환해야 하거나, 드론이 고속으로 이동해 시간 지연이 더욱 민감해지는 등 특수한 상황이 있으므로, tf2 사용 시 QoS 설정과 변환 주기, 버퍼 길이에 더 신경 써야 한다.

#### tf2와 에러 모델

tf2는 이상적인 수학적 변환 모델을 사용하지만, 실제 로봇 현장에서는 센서 노이즈, 조인트 마모, 조립 편차 등으로 인해 오차가 발생한다. tf2가 보관하는 회전, 평행이동 값은 “측정 기반 또는 모델 기반으로 추정된 값”이라 가정해야 하며, 실제 값과는 일정 오차가 있을 수 있다.

이 오차가 크면, 서로 다른 센서나 로봇 팔이 내놓은 좌표계가 일치하지 않아, 물체를 잡으려 해도 실제 위치와 다르게 팔을 뻗는 문제가 발생할 수 있다. 이를 보완하려면, 센서 캘리브레이션 과정이나 로봇 링크 파라미터 튜닝, 오차 모델링 기법 등을 병행해야 한다. tf2 자체는 오차 보정 로직을 제공하지 않으며, 어디까지나 “현재까지 인식된 혹은 추정된 변환”을 시간 축과 함께 저장하고 조회하는 역할에 집중한다.

#### tf2의 Python 활용

ROS2는 C++뿐 아니라 Python(rclpy) 환경도 공식 지원한다. tf2 역시 Python API를 통해 동일한 기능을 제공한다. Python 노드에서 transform을 브로드캐스팅하거나 리스닝할 때는, `tf2_ros` 모듈의 `TransformBroadcaster`, `Buffer`, `TransformListener` 등을 사용할 수 있다. 다만 C++과 달리 Python에서는 언어 특성상 실행 성능이 조금 낮을 수 있으므로, 매우 빈번한 transform 조회가 필요한 경우 주의가 필요하다.

아래 예시는 rclpy 기반으로 tf2 버퍼와 리스너를 활용하는 방법을 간단히 보여준다.

```c++
import rclpy
from rclpy.node import Node
from tf2_ros import Buffer, TransformListener
from tf2_ros import TransformException
from geometry_msgs.msg import TransformStamped

class Tf2LookupNode(Node):
    def __init__(self):
        super().__init__('tf2_lookup_node')
        self.buffer = Buffer()
        self.listener = TransformListener(self.buffer, self)
        self.timer = self.create_timer(1.0, self.timer_callback)

    def timer_callback(self):
        try:
            now = rclpy.time.Time()
            trans = self.buffer.lookup_transform(
                'base_link', 
                'camera_link', 
                now
            )
            self.get_logger().info(f'Received transform: {trans}')
        except TransformException as ex:
            self.get_logger().warn(f'Lookup failed: {ex}')

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

if __name__ == '__main__':
    main()
```

위 코드에서 `TransformListener`는 메시지 토픽(tf, tf\_static)을 자동으로 구독하고, `self.buffer`에 변환 정보를 저장한다. 이후 `lookup_transform` 메서드를 통해 특정 시점(`now`)에 ‘base\_link’→‘camera\_link’ 변환을 가져온다. C++ 예시와 비슷한 구조이지만, Python에서는 `rclpy.time.Time()`을 사용하여 현재 시각을 구한다.

만약 브로드캐스터를 Python으로 구현한다면, `TransformBroadcaster` 클래스를 임포트하고, `TransformStamped` 메시지를 주기적으로 만들어 `sendTransform` 메서드로 내보내면 된다.

#### tf2 상에서의 좌표계 통합 예

ROS2 환경에서 이동 로봇과 로봇 팔(매니퓰레이터)이 결합된 복합 시스템을 생각해 볼 수 있다. 이동 로봇의 좌표계는 대개 “odom”→“base\_link”로 이어지는 루트를 가지며, 로봇 팔 쪽은 “base\_link” 아래로 “shoulder\_link”→“elbow\_link”→…→“wrist\_link”→“tool\_frame”이 연결된다. 센서는 “lidar\_link”나 “camera\_link” 등으로 추가된다.

이 시스템이 유연하게 확장되려면, 이동 로봇 쪽 상태를 브로드캐스팅하는 node(예: wheel odometry node, robot\_state\_publisher)가 있고, 매니퓰레이터 조인트 각을 브로드캐스팅하는 node(예: joint\_state\_publisher)가 별도로 존재하며, 센서는 센서 노드가 그 장착 좌표계 정보를 내보내는 식으로 구성된다. 이렇게 분산된 브로드캐스팅 정보를 tf2 버퍼가 종합하여 전체 좌표계 트리를 만들어 낸다.

이동 로봇이 전역 맵 좌표계(“map”)를 가지고 있고, SLAM이나 localization 알고리즘이 이를 주기적으로 업데이트한다고 하면, “map”→“odom” 변환도 브로드캐스팅된다. 그러면 최종적으로 “map”→“odom”→“base\_link”→…→“tool\_frame” 식으로, 로봇 팔 끝단이 전역 맵 상에서 어디에 해당하는지까지 연쇄적으로 추적 가능하다.

#### tf2와 복합 메시지 변환

tf2는 기본적으로 `geometry_msgs/msg/Pose`, `geometry_msgs/msg/Point`, `geometry_msgs/msg/TransformStamped` 등 기본 메시지 타입을 지원하지만, 복합 메시지(Custom message) 안에 좌표가 담겨 있을 때도 유사한 접근이 가능하다. tf2에 “conversion function”을 등록하거나, `tf2_ros::MessageFilter` 등을 사용해 특정 토픽 메시지를 자동 변환해볼 수도 있다.

예컨대 “검출된 물체 리스트” 같은 메시지가 있고, 그 안에 각 물체의 위치가 ‘camera\_link’를 기준으로 표현된다고 하자. 이걸 ‘map’ 프레임으로 변환하려면, 메시지 내 각 물체 좌표에 대해 `lookupTransform`을 이용해 tf2 변환을 얻은 후, tf2::doTransform 함수를 반복 호출하는 방식을 쓸 수 있다. 혹은 더 고급 기법으로, 메시지 타입에 대한 변환 함수를 오버로드하여 한 번에 변환 처리하는 방법이 있다.

#### tf2 MessageFilter

tf2\_ros 패키지에는 `tf2_ros::MessageFilter`라는 편리한 유틸리티가 있다. 특정 메시지(예: 센서 데이터)에 포함된 타임스탬프와 tf2 버퍼의 변환 정보가 모두 준비되어 있을 때에만 콜백을 불러 주는 기능이다. 이를 통해 tf가 준비되지 않은 상태에서 발생하는 TransformException을 줄이거나, 메시지 재수신 로직을 구현하지 않고도 스무스하게 동기화할 수 있다.

예시로, 라이다 스캔 메시지(`sensor_msgs/msg/LaserScan`)에 담긴 헤더 타임스탬프와, “laser\_link”→“base\_link” 변환이 모두 준비된 순간을 기다렸다가 콜백을 실행하도록 구성할 수 있다. 이때 `tf2_ros::MessageFilter`는 내부적으로 tf2 버퍼를 조회하여, 특정 시점의 transform이 준비되지 않았으면 잠시 대기하거나, 곧 들어올 transform 메시지를 기다린다. 이렇게 하면 콜백 함수가 실행되는 시점에는 이미 필요한 tf 변환이 확보되어 있어, `lookupTransform` 호출 실패를 최소화할 수 있다.

#### tf2와 보간(Interpolation) 상세

tf2가 시간축 보간을 수행하는 과정은 다음과 같은 수학적 원리에 기반한다. 예를 들어 $t\_1$ 시점에서의 변환 $\mathbf{T}(t\_1)$과 $t\_2$ 시점에서의 변환 $\mathbf{T}(t\_2)$가 모두 주어져 있고, $t$가 $t\_1$과 $t\_2$ 사이에 있을 때, 보간된 변환 $\mathbf{T}(t)$는 회전, 평행이동 각각에 대해 선형 또는 스페리컬 보간(slerp)으로 계산된다.

평행이동은 3차원 벡터의 선형 보간이므로,

$$
\mathbf{p}(t) = \mathbf{p}(t\_1)  + \frac{t - t\_1}{t\_2 - t\_1}  \bigl\[\mathbf{p}(t\_2) - \mathbf{p}(t\_1)\bigr]
$$

회전은 쿼터니언에 대해 slerp(spherical linear interpolation)를 수행한다. 두 쿼터니언 $\mathbf{q}\_1, \mathbf{q}\_2$와 보간 계수 $\alpha$가 주어졌을 때,

$$
\mathbf{q}(t) = \mathbf{q}\_1 , \otimes \bigl(\mathbf{q}\_1^{-1} \otimes \mathbf{q}\_2\bigr)^\alpha
$$

$\otimes$는 쿼터니언 곱, $\alpha = \frac{t - t\_1}{t\_2 - t\_1}$ (0과 1 사이)로 정의된다. tf2는 이러한 보간을 자동으로 수행하기 때문에, 사용자가 직접 쿼터니언 보간 공식을 구현할 필요가 없다.

#### tf2 Plug-ins과 확장성

tf2는 로봇 파이프라인 전반에 깊이 관여하기 때문에, 필요한 경우 다양한 형태로 확장이 이루어진다. 예를 들어 `tf2_ros` 아래에는 `tf2_eigen`, `tf2_kdl`, `tf2_geometry_msgs`처럼 다른 라이브러리와 메시지 타입을 지원하기 위한 모듈이 이미 존재한다. 사용자는 이를 통해 `Eigen::Isometry3d`, `KDL::Frame`, `geometry_msgs::msg::PoseStamped` 같은 타입을 tf2에서 자유롭게 변환 가능하다.

새로운 타입을 만들었다면(예: 특정 센서에서 정의한 6D pose 배열) tf2 변환을 적용할 수 있도록 변환 함수를 추가로 구현할 수도 있다. 이렇게 하면 `tf2::doTransform(커스텀 메시지, …)` 형태로도 변환 로직을 일관되게 재사용할 수 있다.

#### tf2 Troubleshooting: 흔한 오류

ROS2 초심자가 tf2를 사용할 때 가장 자주 만나는 오류는, 다음과 같은 메시지다.

```
Lookup would require extrapolation into the past
```

혹은

```
Lookup would require extrapolation into the future
```

이는 `lookupTransform`에서 요청한 시점이 tf2 버퍼에 기록된 시점 범위를 벗어났음을 의미한다. 보통 transform 브로드캐스팅과 센서 메시지 발행 주기, 그리고 노드가 실행되는 실제 시간의 불일치 때문에 발생한다. 해결책으로는:

* transform 브로드캐스팅 노드가 실제 현재 시간을 정확히 사용하고 있는지 확인
* 센서 메시지와 transform 메시지의 타임스탬프가 같은 시계(clock)를 참조하고 있는지 점검
* tf2 버퍼의 히스토리 길이를 적절히 늘리거나, 불필요하게 오래된 시점을 요청하지 않도록 조정
* 메시지 필터를 사용해 tf2가 준비된 이후에 콜백이 불리도록 설계

등을 고려할 수 있다.

또 다른 흔한 문제는 “프레임 이름을 잘못 입력”하는 것이다. ROS2 tf2는 프레임 이름이 완벽히 일치해야 한다. 대소문자나 언더스코어, prefix 등에서 하나라도 틀리면 서로 다른 프레임으로 인식해버린다. “base\_link”와 “Base\_Link”는 완전히 다른 프레임명이다.

#### tf2와 미래 전망

tf2는 ROS2의 대표적인 좌표 변환 솔루션이자, 로봇 애플리케이션 개발에서 사실상 필수 요소가 되었다. 앞으로 ROS2 생태계가 더 넓어지고, 하드 리얼타임 시스템이나 클라우드 로봇, 분산 로봇 등이 발전함에 따라, tf2 역시 성능과 확장성을 계속 강화해 나갈 것으로 예상된다. 더 나은 QoS 프로파일 설정, 분산 버퍼 구조, 시뮬레이션-실기 혼합 환경에서의 일관성 유지 등 다양한 주제들이 연구되고 있다.
