659.119 InvalidArgumentException 처리 (Handling InvalidArgumentException)

659.119 InvalidArgumentException 처리 (Handling InvalidArgumentException)

1. InvalidArgumentException의 정의와 발생 조건

tf2::InvalidArgumentException은 TF2 라이브러리의 함수에 잘못된 인자(argument)가 전달되었을 때 발생하는 예외이다. 이 예외는 tf2::TransformException으로부터 파생되며, 변환 트리의 상태나 시간 범위와는 무관하게 입력 값 자체의 유효성 위반을 나타낸다.

InvalidArgumentException은 TF2 예외 계층에서 가장 기본적인 입력 검증 오류에 해당하며, 대부분의 경우 개발자의 프로그래밍 오류(programming error)를 반영한다. 따라서 다른 TF2 예외와 달리, 이 예외는 일시적 상태 변화에 의해 해소되지 않으며 코드 수정을 통해서만 근본적으로 해결할 수 있다.

2. 구체적 발생 원인

2.1 빈 문자열 프레임 ID

lookupTransform(), canTransform(), setTransform() 등의 메서드에 빈 문자열("")이 프레임 ID로 전달되면 InvalidArgumentException이 발생한다. TF2는 프레임 ID가 비어 있는 변환을 저장하거나 조회하는 것을 허용하지 않는다.

// 다음 호출은 InvalidArgumentException을 발생시킨다
auto transform = tf_buffer->lookupTransform("", "base_link", 
                                             tf2::TimePointZero);

빈 프레임 ID가 전달되는 전형적 상황은 다음과 같다.

  1. 초기화되지 않은 변수: std::string 변수를 선언한 후 값을 할당하지 않은 채 프레임 ID로 전달하는 경우
  2. 파라미터 기본값 누락: ROS2 파라미터에서 프레임 이름을 읽어오되, 파라미터가 설정되지 않았을 때 기본값이 빈 문자열인 경우
  3. 메시지 필드 미설정: 수신된 메시지의 header.frame_id가 발행자 측에서 설정되지 않아 빈 문자열인 경우

2.2 선행 슬래시가 포함된 프레임 ID

TF2(ROS2)에서는 프레임 ID에 선행 슬래시(/)를 포함하는 것을 허용하지 않는다. ROS1의 TF에서는 /base_link와 같이 선행 슬래시를 포함하는 프레임 ID가 허용되었으나, ROS2의 TF2에서는 이를 InvalidArgumentException으로 거부한다.

// ROS2 TF2에서 다음은 InvalidArgumentException을 발생시킬 수 있다
geometry_msgs::msg::TransformStamped t;
t.header.frame_id = "/map";        // 선행 슬래시 포함
t.child_frame_id = "/base_link";   // 선행 슬래시 포함
tf_broadcaster->sendTransform(t);

2.3 동일한 프레임 ID의 부모-자식 관계

setTransform() 호출 시 header.frame_id(부모 프레임)와 child_frame_id(자식 프레임)가 동일한 문자열이면, 자기 자신으로의 변환을 정의하는 것이 되어 논리적으로 무의미하다. TF2는 이러한 자기 참조 변환을 거부하며 InvalidArgumentException을 발생시킨다.

geometry_msgs::msg::TransformStamped t;
t.header.frame_id = "base_link";
t.child_frame_id = "base_link";  // 부모와 자식이 동일
// tf_broadcaster->sendTransform(t);  // InvalidArgumentException 발생

2.4 비정규화된 쿼터니언

변환의 회전 성분을 나타내는 쿼터니언 \mathbf{q} = (x, y, z, w)의 노름(norm)이 1에서 크게 벗어나는 경우, setTransform() 또는 doTransform() 과정에서 InvalidArgumentException이 발생할 수 있다. 쿼터니언이 단위 쿼터니언(unit quaternion)이 아니면 회전 변환의 수학적 의미가 정의되지 않기 때문이다.

\|\mathbf{q}\| = \sqrt{x^2 + y^2 + z^2 + w^2} \neq 1

특히, 쿼터니언의 모든 성분이 0인 경우(x = y = z = w = 0)는 가장 빈번한 오류 사례이며, 이는 TransformStamped 메시지의 회전 필드를 명시적으로 설정하지 않았을 때 발생한다. geometry_msgs::msg::Quaternion의 기본 초기화 값은 모든 필드가 0이므로, 회전 성분을 설정하지 않으면 노름이 0인 비정규화 쿼터니언이 전달된다.

geometry_msgs::msg::TransformStamped t;
t.header.frame_id = "map";
t.child_frame_id = "base_link";
t.transform.translation.x = 1.0;
// 회전 미설정: rotation.x = 0, y = 0, z = 0, w = 0 (노름 = 0)
// tf_broadcaster->sendTransform(t);  // 잠재적 InvalidArgumentException

올바른 항등 회전(identity rotation)을 나타내려면 w = 1로 설정하여야 한다.

t.transform.rotation.w = 1.0;  // 항등 회전 (노름 = 1)

3. 예외 처리 패턴

3.1 방어적 입력 검증

InvalidArgumentException은 대부분 프로그래밍 오류에 기인하므로, 예외 처리보다는 사전 검증을 통한 방지가 더 적절하다. lookupTransform() 호출 전에 프레임 ID의 유효성을 확인하는 유틸리티 함수를 구현하여 적용한다.

bool isValidFrameId(const std::string& frame_id)
{
    if (frame_id.empty()) {
        return false;
    }
    if (frame_id[0] == '/') {
        return false;
    }
    return true;
}

void processData(const sensor_msgs::msg::PointCloud2::SharedPtr msg)
{
    if (!isValidFrameId(msg->header.frame_id)) {
        RCLCPP_ERROR(this->get_logger(),
            "수신 메시지의 frame_id가 유효하지 않음: '%s'",
            msg->header.frame_id.c_str());
        return;
    }
    
    try {
        auto transform = tf_buffer_->lookupTransform(
            target_frame_, msg->header.frame_id,
            tf2_ros::fromMsg(msg->header.stamp));
        // 변환 적용
    } catch (const tf2::TransformException& ex) {
        RCLCPP_WARN(this->get_logger(), "%s", ex.what());
    }
}

3.2 쿼터니언 정규화 검증

변환을 발행하기 전에 쿼터니언의 노름을 검증하고, 필요시 정규화를 수행하는 방어적 패턴을 적용한다.

void validateAndNormalizeQuaternion(
    geometry_msgs::msg::Quaternion& q)
{
    double norm = std::sqrt(q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w);
    
    if (norm < 1e-10) {
        // 영 쿼터니언: 항등 회전으로 대체
        RCLCPP_WARN(rclcpp::get_logger("tf_validator"),
            "영 쿼터니언 감지, 항등 회전으로 설정");
        q.x = 0.0;
        q.y = 0.0;
        q.z = 0.0;
        q.w = 1.0;
        return;
    }
    
    double tolerance = 1e-3;
    if (std::abs(norm - 1.0) > tolerance) {
        RCLCPP_WARN(rclcpp::get_logger("tf_validator"),
            "비정규화 쿼터니언 감지 (노름=%.6f), 정규화 수행", norm);
        q.x /= norm;
        q.y /= norm;
        q.z /= norm;
        q.w /= norm;
    }
}

3.3 Python에서의 InvalidArgumentException 처리

from tf2_ros import InvalidArgumentException

def transform_point(self, point_stamped):
    if not point_stamped.header.frame_id:
        self.get_logger().error('빈 frame_id가 감지됨')
        return None
    
    try:
        transformed = self.tf_buffer.transform(
            point_stamped, 'map')
        return transformed
    except InvalidArgumentException as ex:
        self.get_logger().error(
            f'잘못된 인자: {ex}')
        return None
    except Exception as ex:
        self.get_logger().warn(f'변환 실패: {ex}')
        return None

4. 선행 슬래시 자동 제거 유틸리티

ROS1 코드를 ROS2로 마이그레이션하는 과정에서 선행 슬래시가 포함된 프레임 ID가 잔존하는 경우가 빈번하다. 이를 자동으로 제거하는 유틸리티를 구현하여 마이그레이션 단계에서 활용할 수 있다.

std::string stripLeadingSlash(const std::string& frame_id)
{
    if (!frame_id.empty() && frame_id[0] == '/') {
        RCLCPP_WARN_ONCE(rclcpp::get_logger("tf_migration"),
            "선행 슬래시가 포함된 프레임 ID '%s' 감지. "
            "ROS2에서는 선행 슬래시를 제거하여야 합니다.",
            frame_id.c_str());
        return frame_id.substr(1);
    }
    return frame_id;
}

이 유틸리티는 마이그레이션 과정의 임시 조치이며, 최종적으로는 모든 소스 코드와 설정 파일에서 선행 슬래시를 완전히 제거하여야 한다.

5. 진단 및 디버깅

5.1 오류 메시지 분석

InvalidArgumentExceptionwhat() 메서드가 반환하는 메시지는 잘못된 인자의 구체적 내용을 포함한다.

오류 메시지 패턴원인
"" passed to lookupTransform argument target_frame목표 프레임이 빈 문자열
"" passed to lookupTransform argument source_frame원본 프레임이 빈 문자열
Invalid frame ID "..." passed to ...선행 슬래시 등 형식 오류
Transform ... has invalid quaternion쿼터니언 정규화 위반

5.2 정적 분석을 통한 사전 탐지

코드 리뷰 및 정적 분석 도구를 활용하여 프레임 ID가 빈 문자열로 전달될 가능성이 있는 코드 경로를 사전에 식별한다. 특히 파라미터로부터 프레임 이름을 읽어오는 코드에서 기본값이 빈 문자열로 설정되어 있는지 점검한다.

// 잠재적 오류: 파라미터 미설정 시 빈 문자열 반환
this->declare_parameter<std::string>("target_frame", "");

// 개선: 기본값을 명시적으로 설정하거나 필수 파라미터로 선언
this->declare_parameter<std::string>("target_frame", "map");

6. LookupException과의 구별 기준

InvalidArgumentExceptionLookupException은 모두 프레임 ID와 관련된 오류이나, 발생 원인과 의미론이 명확히 구별된다.

구분 항목InvalidArgumentExceptionLookupException
원인입력 인자 자체의 형식적 결함변환 트리에 프레임이 미등록
프레임 ID 상태빈 문자열, 슬래시 포함 등형식적으로 유효하나 미등록
복구 가능성코드 수정 필요발행 노드 기동으로 해소 가능
재시도 유효성재시도로 해결 불가재시도로 해결 가능
심각도높음 (프로그래밍 오류)중간 (구성 또는 타이밍 문제)

이 구분에 기반하여, InvalidArgumentExceptionRCLCPP_ERROR 또는 RCLCPP_FATAL 수준으로 기록하고, 해당 오류 경로를 개발 단계에서 즉각적으로 수정하는 것이 바람직하다.


참고 문헌 및 출처

  • ROS2 geometry2 리포지터리, tf2 패키지, 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 공식 문서, TF2 튜토리얼, https://docs.ros.org/en/humble/Tutorials/Intermediate/Tf2/Tf2-Main.html
  • REP 105 – Coordinate Frames for Mobile Platforms, https://www.ros.org/reps/rep-0105.html

버전: 1.0