1296.38 포트를 통한 데이터 읽기 (getInput)

1. getInput() 메서드의 개요

getInput()은 BehaviorTree.CPP 라이브러리에서 노드가 입력 포트(InputPort) 또는 양방향 포트(BidirectionalPort)를 통해 블랙보드의 데이터를 읽기 위해 호출하는 메서드이다. 이 메서드는 TreeNode 기반 클래스에 정의되어 있으며, 모든 액션 노드, 조건 노드, 데코레이터 노드에서 사용 가능하다. getInput()은 포트 명칭을 인자로 받아, XML에서 해당 포트에 매핑된 블랙보드 키의 값 또는 리터럴 값을 반환한다(Faconti, 2022).

2. getInput()의 호출 형식

BehaviorTree.CPP 4.x에서 getInput()은 두 가지 오버로드를 제공한다.

2.1 참조 반환 형식

template <typename T>
bool getInput(const std::string& port_name, T& destination);

포트의 값을 destination에 대입하고, 성공 시 true, 실패 시 false를 반환한다.

geometry_msgs::msg::PoseStamped goal;
if (!getInput("goal_pose", goal))
{
    // 실패 처리
    return BT::NodeStatus::FAILURE;
}
// goal에 유효한 값이 대입됨

2.2 Expected 반환 형식

template <typename T>
BT::Expected<T> getInput(const std::string& port_name);

BT::Expected<T>를 반환하며, 성공 시 값을 포함하고 실패 시 오류 메시지를 포함한다.

auto result = getInput<double>("timeout_sec");
if (!result)
{
    RCLCPP_ERROR(node_->get_logger(),
        "timeout_sec 읽기 실패: %s",
        result.error().c_str());
    return BT::NodeStatus::FAILURE;
}
double timeout = result.value();

Expected 형식은 실패 원인을 문자열로 제공하므로 디버깅에 유리하다.

3. 내부 동작 과정

getInput() 호출 시의 내부 동작을 단계별로 기술한다.

3.1 단계: 포트 매핑 조회

노드의 포트 구성(NodeConfig)에서 지정된 포트 명칭의 매핑 정보를 조회한다. 매핑 정보는 XML 파싱 시 결정되며, 블랙보드 키 참조 또는 리터럴 값의 형태이다.

3.2 단계: 값의 해석

매핑 정보의 형태에 따라 다른 경로로 값을 해석한다.

getInput("port_name", destination)
    │
    ├─ 매핑 정보 조회
    │       │
    │       ├─ 블랙보드 키 참조 ("{key}")
    │       │       │
    │       │       ├─ 블랙보드에서 "key" 조회
    │       │       │       │
    │       │       │       ├─ 키 존재, 타입 일치 → 값 대입 → 성공
    │       │       │       │
    │       │       │       ├─ 키 존재, 타입 불일치 → 변환 시도
    │       │       │       │       │
    │       │       │       │       ├─ 변환 성공 → 값 대입 → 성공
    │       │       │       │       │
    │       │       │       │       └─ 변환 실패 → 실패
    │       │       │       │
    │       │       │       └─ 키 미존재 → 실패
    │       │       │
    │       │       └─ 결과 반환
    │       │
    │       ├─ 리터럴 값 ("30.0")
    │       │       │
    │       │       ├─ convertFromString<T>("30.0") 호출
    │       │       │       │
    │       │       │       ├─ 변환 성공 → 값 대입 → 성공
    │       │       │       │
    │       │       │       └─ 변환 실패 → 실패
    │       │       │
    │       │       └─ 결과 반환
    │       │
    │       ├─ 기본값 (XML 미지정, 기본값 존재)
    │       │       │
    │       │       └─ 기본값 문자열로 변환 → 위와 동일
    │       │
    │       └─ 미지정 (XML 미지정, 기본값 없음) → 실패
    │
    └─ 최종 결과

3.3 단계: 타입 변환

블랙보드에 저장된 값의 타입이 요청 타입 T와 정확히 일치하면 직접 대입된다. 일치하지 않는 경우, 블랙보드의 Any 타입 저장소가 convertFromString<T>() 또는 등록된 변환 함수를 통해 변환을 시도한다.

4. 실패 원인과 진단

getInput() 실패의 주요 원인을 분류한다.

실패 원인상황Expected 오류 메시지 예시
포트 미지정XML에서 해당 포트가 설정되지 않고 기본값도 없음"port 'X' not found"
블랙보드 키 미존재참조한 블랙보드 키에 값이 저장되지 않음"key 'X' not found in blackboard"
타입 변환 실패리터럴 값의 문자열-타입 변환 실패"cannot convert 'abc' to double"
타입 불일치블랙보드 값과 포트 타입 불일치"type mismatch for key 'X'"

4.1 실패 진단 패턴

BT::NodeStatus onStart() override
{
    auto goal_result = getInput<
        geometry_msgs::msg::PoseStamped>("goal_pose");

    if (!goal_result)
    {
        // 오류 메시지로 실패 원인 진단
        std::string error = goal_result.error();
        RCLCPP_ERROR(node_->get_logger(),
            "goal_pose 읽기 실패: %s", error.c_str());

        // 실패 원인에 따른 구체적 로그
        if (error.find("not found") != std::string::npos)
        {
            RCLCPP_ERROR(node_->get_logger(),
                "블랙보드에 값이 설정되지 않음");
        }
        return BT::NodeStatus::FAILURE;
    }

    auto goal = goal_result.value();
    // ...
}

5. 노드 유형별 getInput() 사용 패턴

5.1 SyncActionNode

class ComputeDistance : public BT::SyncActionNode
{
public:
    static BT::PortsList providedPorts()
    {
        return {
            BT::InputPort<geometry_msgs::msg::Point>("point_a"),
            BT::InputPort<geometry_msgs::msg::Point>("point_b"),
            BT::OutputPort<double>("distance")
        };
    }

    BT::NodeStatus tick() override
    {
        geometry_msgs::msg::Point a, b;
        if (!getInput("point_a", a) || !getInput("point_b", b))
            return BT::NodeStatus::FAILURE;

        double dist = std::hypot(b.x - a.x, b.y - a.y);
        setOutput("distance", dist);
        return BT::NodeStatus::SUCCESS;
    }
};

SyncActionNode에서는 tick() 시작부에서 입력을 읽는다. tick()은 매 호출마다 독립적이므로, 매번 최신 블랙보드 값을 읽는다.

5.2 StatefulActionNode

class NavigateToPose : public BT::StatefulActionNode
{
    geometry_msgs::msg::PoseStamped goal_;
    double timeout_;

    BT::NodeStatus onStart() override
    {
        // onStart()에서 모든 입력을 읽어 멤버에 저장
        if (!getInput("goal_pose", goal_))
            return BT::NodeStatus::FAILURE;

        getInput("timeout_sec", timeout_);

        sendNavigationGoal(goal_);
        start_time_ = std::chrono::steady_clock::now();
        return BT::NodeStatus::RUNNING;
    }

    BT::NodeStatus onRunning() override
    {
        // onRunning()에서는 저장된 멤버 사용
        // getInput()을 다시 호출하지 않음
        if (isComplete())
            return BT::NodeStatus::SUCCESS;

        auto elapsed = std::chrono::steady_clock::now()
                     - start_time_;
        if (elapsed > std::chrono::duration<double>(timeout_))
            return BT::NodeStatus::FAILURE;

        return BT::NodeStatus::RUNNING;
    }

    void onHalted() override { cancelNavigation(); }
};

StatefulActionNode에서는 onStart()에서 입력을 읽어 멤버 변수에 저장하고, onRunning()에서는 저장된 값을 사용한다. 이 패턴은 실행 중 블랙보드 값의 변경에 의한 비일관성을 방지한다.

5.3 ThreadedAction

class BlockingAction : public BT::ThreadedAction
{
    BT::NodeStatus tick() override
    {
        // tick() 시작부에서 입력을 지역 변수에 복사
        std::string target;
        if (!getInput("target", target))
            return BT::NodeStatus::FAILURE;

        double timeout;
        getInput("timeout_sec", timeout);

        // 차단 호출 수행 (블랙보드 미접근)
        auto result = performBlockingCall(target, timeout);

        if (isHaltRequested())
            return BT::NodeStatus::FAILURE;

        return result.success
            ? BT::NodeStatus::SUCCESS
            : BT::NodeStatus::FAILURE;
    }
};

ThreadedAction에서는 tick() 시작부에서 입력을 지역 변수에 복사한 후, 이후 차단 호출 동안에는 블랙보드에 접근하지 않는다. 이는 스레드 안전성을 확보하기 위한 패턴이다.

6. 다중 입력 포트의 읽기

다수의 입력 포트를 읽을 때의 패턴을 제시한다.

6.1 순차적 검증

BT::NodeStatus onStart() override
{
    Pose target;
    if (!getInput("goal_pose", target))
    {
        RCLCPP_ERROR(logger_, "'goal_pose' 누락");
        return BT::NodeStatus::FAILURE;
    }

    double speed;
    if (!getInput("speed", speed))
    {
        RCLCPP_ERROR(logger_, "'speed' 누락");
        return BT::NodeStatus::FAILURE;
    }

    // 모든 입력 유효
    startAction(target, speed);
    return BT::NodeStatus::RUNNING;
}

6.2 일괄 검증

BT::NodeStatus onStart() override
{
    bool valid = true;
    Pose target;
    double speed;
    std::string frame;

    if (!getInput("goal_pose", target))
    { RCLCPP_ERROR(logger_, "'goal_pose' 누락"); valid = false; }

    if (!getInput("speed", speed))
    { RCLCPP_ERROR(logger_, "'speed' 누락"); valid = false; }

    if (!getInput("frame_id", frame))
    { RCLCPP_ERROR(logger_, "'frame_id' 누락"); valid = false; }

    if (!valid)
        return BT::NodeStatus::FAILURE;

    startAction(target, speed, frame);
    return BT::NodeStatus::RUNNING;
}

일괄 검증 패턴은 다수의 필수 입력이 누락된 경우 모든 누락 항목을 한 번에 보고하므로, 디버깅 효율이 높다.

7. 블랙보드의 스레드 안전성과 getInput()

BehaviorTree.CPP 4.x의 블랙보드는 내부적으로 std::mutex에 의해 보호된다. 따라서 getInput() 호출 자체는 스레드 안전하다. 그러나 getInput()과 후속 처리의 조합은 원자적이지 않다.

노드 유형getInput() 안전성비고
SyncActionNode안전메인 스레드 단독
StatefulActionNode안전메인 스레드 단독
CoroActionNode안전메인 스레드 단독
ThreadedAction호출 자체는 안전복합 연산은 비원자적

ThreadedAction에서 getInput()tick() 시작부에서 호출하여 지역 변수에 복사한 후, 이후에는 블랙보드에 접근하지 않는 것이 권장되는 패턴이다.

8. getInput()과 타입 변환

8.1 기본 타입 변환

기본 타입(int, double, bool, std::string 등)은 BehaviorTree.CPP에 내장된 convertFromString<T>() 함수에 의해 자동으로 변환된다.

// XML: timeout_sec="30.0"
double timeout;
getInput("timeout_sec", timeout);  // "30.0" → 30.0

8.2 사용자 정의 타입 변환

사용자 정의 타입은 BT::convertFromString<T>() 템플릿 특수화를 등록하여야 한다.

namespace BT
{
template <>
inline MyPose convertFromString(StringView str)
{
    auto parts = splitString(str, ';');
    MyPose p;
    p.x = convertFromString<double>(parts[0]);
    p.y = convertFromString<double>(parts[1]);
    return p;
}
}

8.3 블랙보드 키 참조 시의 타입

블랙보드 키 참조({key})를 통해 값을 읽는 경우, 블랙보드에 저장된 값의 실제 타입과 getInput()에서 요청한 타입 T가 일치하여야 한다. 일치하지 않는 경우 Any 타입 저장소가 convertFromString<T>() 또는 등록된 변환 함수를 통해 변환을 시도하며, 변환이 불가능하면 실패한다.

9. 설계 지침

  1. 반환값의 필수적 검사: 모든 getInput() 호출의 반환값을 검사한다. 반환값 무시는 유효하지 않은 데이터에 의한 미정의 동작을 유발할 수 있다.

  2. 입력 읽기의 시점 집중: 입력 포트의 읽기를 콜백의 시작부에 집중하여, 데이터 의존성을 명확히 하고 블랙보드 접근을 최소화한다.

  3. Expected 형식의 활용: 디버깅이 필요한 경우 Expected 반환 형식을 사용하여 실패 원인을 구체적으로 파악한다.

  4. StatefulActionNode에서 onStart() 읽기: onStart()에서 입력을 읽어 멤버 변수에 저장하고, onRunning()에서는 블랙보드에 재접근하지 않는다.

  5. ThreadedAction에서 지역 복사: tick() 시작부에서 입력을 지역 변수에 복사하고, 이후 차단 호출 동안에는 블랙보드에 접근하지 않는다(Faconti, 2022).