659.115 TF2의 오류 처리와 예외 관리 (TF2 Error Handling and Exception Management)

659.115 TF2의 오류 처리와 예외 관리 (TF2 Error Handling and Exception Management)

1. TF2 예외 체계의 구조

TF2 라이브러리는 변환 조회 및 데이터 처리 중 발생하는 오류를 C++ 예외 메커니즘을 통해 보고한다. 모든 TF2 관련 예외는 tf2::TransformException 기저 클래스(base class)로부터 파생되며, 이 클래스는 다시 std::runtime_error를 상속한다. 이러한 계층적 예외 구조는 개발자가 오류 유형에 따라 세분화된 처리 전략을 적용하거나, 공통 기저 클래스를 통해 일괄적으로 처리할 수 있는 유연성을 제공한다.

TF2의 예외 계층 구조는 다음과 같다.

std::runtime_error
  └── tf2::TransformException
        ├── tf2::LookupException
        ├── tf2::ConnectivityException
        ├── tf2::ExtrapolationException
        └── tf2::InvalidArgumentException

각 예외 클래스의 의미론적 분류는 다음과 같다.

예외 클래스발생 원인의미
LookupException요청된 프레임이 변환 트리에 존재하지 않음프레임 식별 실패
ConnectivityException두 프레임 간의 경로가 변환 트리에 존재하지 않음트리 연결성 부재
ExtrapolationException요청 시각이 캐시된 변환의 시간 범위를 초과함시간적 범위 이탈
InvalidArgumentException잘못된 인자가 전달됨(예: 빈 프레임 ID)입력 유효성 위반

2. 예외 발생 메커니즘의 내부 동작

2.1 lookupTransform()에서의 예외 발생 과정

lookupTransform() 메서드는 요청된 목표 프레임(target frame)과 원본 프레임(source frame) 간의 변환을 계산하기 위해 다음의 단계를 순서대로 수행하며, 각 단계에서 오류 조건이 감지되면 해당 예외를 발생시킨다.

  1. 프레임 ID 유효성 검사: 목표 프레임 또는 원본 프레임의 ID가 빈 문자열이면 InvalidArgumentException을 발생시킨다.
  2. 프레임 존재 확인: 요청된 프레임 ID가 내부 프레임 테이블에 등록되어 있지 않으면 LookupException을 발생시킨다.
  3. 경로 탐색: 변환 트리에서 두 프레임을 연결하는 경로를 탐색한다. 두 프레임이 서로 다른 트리에 속하거나 경로가 존재하지 않으면 ConnectivityException을 발생시킨다.
  4. 시간 범위 검증: 요청된 시각(timestamp)이 경로상의 각 변환 채널에서 캐시된 시간 범위 내에 있는지 확인한다. 범위를 초과하면 ExtrapolationException을 발생시킨다.
  5. 보간 및 합성: 경로상의 각 변환을 요청 시각에 대해 보간하고, 경로를 따라 순차적으로 합성하여 최종 변환을 산출한다.

2.2 canTransform()과 lookupTransform()의 관계

canTransform() 메서드는 lookupTransform()과 동일한 검증 과정을 내부적으로 수행하되, 예외를 발생시키지 않고 부울(boolean) 값을 반환한다. 선택적 매개변수 error_msg를 전달하면, 변환이 불가능한 경우 오류 원인을 문자열로 제공받을 수 있다.

std::string error_msg;
bool available = tf_buffer->canTransform(
    "target_frame", "source_frame",
    tf2::TimePointZero,
    &error_msg);

if (!available) {
    RCLCPP_WARN(this->get_logger(), "변환 불가: %s", error_msg.c_str());
}

다만, canTransform()의 결과와 이후의 lookupTransform() 호출 사이에 시간적 간극이 존재하므로, 멀티 스레드 환경에서는 canTransform()true를 반환하였더라도 lookupTransform() 시점에서 변환이 불가능해질 수 있다. 이러한 TOCTOU(Time of Check to Time of Use) 경합을 방지하려면 예외 기반 처리 패턴을 채택하는 것이 권장된다.

3. 예외 처리의 설계 원칙

3.1 방어적 예외 처리 원칙

로봇 시스템에서 변환 조회 실패는 일시적이고 복구 가능한 상황인 경우가 많다. 시스템 기동 직후 변환 발행자가 아직 활성화되지 않은 경우, 네트워크 지연으로 인해 변환이 일시적으로 지연되는 경우, 또는 센서 데이터의 타임스탬프가 변환 캐시보다 미세하게 앞서는 경우 등이 대표적이다. 따라서 모든 lookupTransform() 호출은 반드시 예외 처리 블록으로 감싸야 하며, 예외를 포착하지 않아 노드가 비정상 종료되는 상황은 절대로 허용되어서는 안 된다.

try {
    auto transform = tf_buffer_->lookupTransform(
        "target", "source", tf2::TimePointZero);
    processTransform(transform);
} catch (const tf2::TransformException& ex) {
    RCLCPP_WARN_THROTTLE(
        this->get_logger(), *this->get_clock(), 1000,
        "변환 조회 실패: %s", ex.what());
}

3.2 예외 유형별 차별화 처리

기저 클래스인 TransformException으로 모든 예외를 일괄 처리하는 것은 간편하나, 각 예외의 원인에 따라 서로 다른 복구 전략이 요구될 수 있다. 예를 들어, LookupException은 프레임 이름의 오타나 누락된 변환 발행자를 암시하므로 즉각적인 구성 점검이 필요한 반면, ExtrapolationException은 일시적인 타이밍 불일치를 나타내므로 재시도로 해결될 가능성이 높다.

try {
    auto transform = tf_buffer_->lookupTransform(
        "map", "base_link", sensor_timestamp);
    processTransform(transform);
} catch (const tf2::LookupException& ex) {
    RCLCPP_ERROR(this->get_logger(),
        "프레임 조회 실패 (구성 오류 의심): %s", ex.what());
} catch (const tf2::ConnectivityException& ex) {
    RCLCPP_ERROR(this->get_logger(),
        "프레임 연결성 부재: %s", ex.what());
} catch (const tf2::ExtrapolationException& ex) {
    RCLCPP_WARN(this->get_logger(),
        "시간 범위 초과 (일시적): %s", ex.what());
} catch (const tf2::InvalidArgumentException& ex) {
    RCLCPP_FATAL(this->get_logger(),
        "잘못된 인자: %s", ex.what());
}

3.3 Python에서의 예외 처리

rclpy(Python)에서의 TF2 예외 처리도 동일한 계층적 예외 구조를 따른다. tf2_ros Python 바인딩은 C++ 예외를 대응하는 Python 예외로 변환하여 전달한다.

import rclpy
from tf2_ros import TransformException
from tf2_ros import LookupException, ConnectivityException, ExtrapolationException

try:
    transform = tf_buffer.lookup_transform(
        'target', 'source', rclpy.time.Time())
except LookupException as ex:
    self.get_logger().error(f'프레임 조회 실패: {ex}')
except ConnectivityException as ex:
    self.get_logger().error(f'프레임 연결성 부재: {ex}')
except ExtrapolationException as ex:
    self.get_logger().warn(f'시간 범위 초과: {ex}')
except TransformException as ex:
    self.get_logger().warn(f'변환 조회 실패: {ex}')

4. 오류 복구 전략

4.1 재시도 기반 복구

일시적인 변환 부재(특히 시스템 기동 시)에 대해서는 재시도 전략이 효과적이다. 타이머 콜백 내에서 변환 조회에 실패한 경우, 콜백을 종료하고 다음 주기에 재시도하는 방식은 단일 스레드 실행기 환경에서도 교착 상태 없이 안전하게 동작한다.

void controlLoopCallback()
{
    geometry_msgs::msg::TransformStamped transform;
    try {
        transform = tf_buffer_->lookupTransform(
            "map", "base_link", tf2::TimePointZero);
    } catch (const tf2::TransformException& ex) {
        RCLCPP_INFO_THROTTLE(
            this->get_logger(), *this->get_clock(), 2000,
            "변환 대기 중, 다음 주기에 재시도: %s", ex.what());
        return;  // 현재 주기 건너뛰기
    }
    
    // 변환을 이용한 제어 로직 실행
    executeControl(transform);
}

4.2 최근 유효 변환 기반 대체

변환 조회에 실패하였으나 이전에 성공적으로 조회된 변환이 존재하는 경우, 해당 이전 변환을 대체값(fallback)으로 사용하는 전략을 적용할 수 있다. 이 전략은 변환의 변화율이 낮은 프레임(예: mapodom)에서 유효하며, 고속 운동 중인 프레임에서는 과도한 오차를 유발할 수 있으므로 주의가 필요하다.

std::optional<geometry_msgs::msg::TransformStamped> last_valid_transform_;

void processCallback()
{
    try {
        auto transform = tf_buffer_->lookupTransform(
            "map", "base_link", tf2::TimePointZero);
        last_valid_transform_ = transform;
        process(transform);
    } catch (const tf2::TransformException& ex) {
        if (last_valid_transform_.has_value()) {
            RCLCPP_WARN(this->get_logger(),
                "최근 유효 변환으로 대체: %s", ex.what());
            process(last_valid_transform_.value());
        } else {
            RCLCPP_ERROR(this->get_logger(),
                "유효한 변환 없음, 처리 건너뛰기: %s", ex.what());
        }
    }
}

4.3 tf2::TimePointZero를 이용한 최신 가용 변환 활용

lookupTransform()tf2::TimePointZero(Python에서는 rclpy.time.Time())를 시간 인자로 전달하면, tf2::Buffer는 해당 프레임 쌍에 대해 가장 최근에 수신된 변환을 반환한다. 이 방법은 특정 시각의 정밀한 변환이 필요하지 않은 경우에 ExtrapolationException의 발생 가능성을 크게 줄여 준다.

그러나 TimePointZero를 사용하면 센서 데이터의 타임스탬프와 변환의 시각 간에 불일치가 발생할 수 있으므로, 센서 데이터의 좌표 변환에는 센서 데이터의 타임스탬프를 직접 사용하는 것이 정확도 측면에서 바람직하다.

5. 로깅 전략과 오류 보고

5.1 로그 스로틀링

변환 조회 실패는 주기적 콜백 내에서 연속적으로 발생할 수 있으며, 매 실패마다 로그 메시지를 출력하면 로그 파일이 급속히 비대해지고 유의미한 메시지가 묻히는 문제가 발생한다. RCLCPP_WARN_THROTTLE 또는 RCLCPP_INFO_THROTTLE 매크로를 사용하여 동일한 오류 메시지의 출력 빈도를 제한하는 것이 바람직하다.

RCLCPP_WARN_THROTTLE(
    this->get_logger(), *this->get_clock(), 5000,  // 5초 간격
    "변환 조회 실패: %s", ex.what());

5.2 진단 시스템 연동

ROS2 진단 시스템(diagnostic_updater)을 활용하여 변환 조회 실패의 통계적 정보(실패 빈도, 연속 실패 횟수, 최종 성공 시각 등)를 진단 메시지로 발행하면, 시스템 모니터링 도구(rqt_robot_monitor 등)를 통해 변환 관련 오류 상태를 실시간으로 추적할 수 있다.

#include <diagnostic_updater/diagnostic_updater.hpp>

void tfDiagnostic(diagnostic_updater::DiagnosticStatusWrapper& stat)
{
    if (consecutive_failures_ == 0) {
        stat.summary(diagnostic_msgs::msg::DiagnosticStatus::OK,
                     "TF 변환 정상");
    } else if (consecutive_failures_ < 10) {
        stat.summary(diagnostic_msgs::msg::DiagnosticStatus::WARN,
                     "TF 변환 간헐적 실패");
    } else {
        stat.summary(diagnostic_msgs::msg::DiagnosticStatus::ERROR,
                     "TF 변환 지속적 실패");
    }
    stat.add("연속 실패 횟수", consecutive_failures_);
    stat.add("총 실패 횟수", total_failures_);
}

6. 시스템 기동 시의 예외 관리

로봇 시스템의 기동 과정에서는 각 노드의 초기화 순서에 따라 변환 발행자가 아직 활성화되지 않은 상태에서 변환 소비자가 먼저 조회를 시도하는 상황이 빈번하게 발생한다. 이러한 기동 시 과도 현상(transient behavior)에 대한 체계적인 대응 전략은 다음과 같다.

  1. 초기화 대기 루프: 노드의 주 기능을 시작하기 전에 필수 변환이 모두 가용해질 때까지 대기한다.
bool waitForRequiredTransforms(double timeout_sec)
{
    auto start = this->get_clock()->now();
    while (rclcpp::ok()) {
        if (tf_buffer_->canTransform("map", "base_link", 
                                      tf2::TimePointZero) &&
            tf_buffer_->canTransform("base_link", "lidar_link",
                                      tf2::TimePointZero)) {
            return true;
        }
        
        auto elapsed = (this->get_clock()->now() - start).seconds();
        if (elapsed > timeout_sec) {
            RCLCPP_ERROR(this->get_logger(),
                "필수 변환 대기 시간 초과 (%.1f초)", timeout_sec);
            return false;
        }
        
        RCLCPP_INFO_THROTTLE(this->get_logger(),
            *this->get_clock(), 1000,
            "필수 변환 대기 중...");
        rclcpp::sleep_for(std::chrono::milliseconds(100));
    }
    return false;
}
  1. 라이프사이클 노드의 활용: ROS2의 관리형 라이프사이클 노드(managed lifecycle node)를 사용하면, on_activate() 콜백에서 필수 변환의 가용성을 확인하고, 모든 조건이 충족된 경우에만 Active 상태로 전이하는 체계적인 초기화 흐름을 구현할 수 있다.

7. 오류 처리 패턴의 체계적 비교

패턴장점단점적용 상황
기저 클래스 일괄 처리구현 간결원인별 대응 불가프로토타이핑, 단순 애플리케이션
파생 클래스 개별 처리세밀한 복구 전략 가능코드 복잡도 증가실운영 시스템
canTransform() 사전 검사예외 발생 방지TOCTOU 경합 가능단일 스레드 환경
TimePointZero 활용ExtrapolationException 방지시간 정밀도 포기실시간성이 낮은 조회
최근 유효 변환 대체연속성 보장오차 누적 가능저동적 변환

참고 문헌 및 출처

  • ROS2 geometry2 리포지터리, tf2tf2_ros 패키지, https://github.com/ros2/geometry2
  • Foote, T. (2013). “tf: The transform library.” IEEE International Conference on Technologies for Practical Robot Applications (TePRA), pp. 1–6.
  • ROS2 공식 문서, Error Handling 가이드, https://docs.ros.org/en/humble
  • Stroustrup, B. (2013). The C++ Programming Language, 4th Edition. Addison-Wesley.

버전: 1.0