1296.39 포트를 통한 데이터 쓰기 (setOutput)

1. setOutput() 메서드의 개요

setOutput()은 BehaviorTree.CPP 라이브러리에서 노드가 출력 포트(OutputPort) 또는 양방향 포트(BidirectionalPort)를 통해 블랙보드에 데이터를 기록하기 위해 호출하는 메서드이다. 이 메서드는 TreeNode 기반 클래스에 정의되어 있으며, 포트 명칭과 기록할 값을 인자로 받아, XML에서 해당 포트에 매핑된 블랙보드 키에 값을 저장한다(Faconti, 2022).

2. setOutput()의 호출 형식

template <typename T>
BT::Result setOutput(const std::string& port_name, const T& value);

BT::Result는 성공 또는 실패 상태를 나타내며, 실패 시 오류 메시지를 포함한다.

// 기본 사용
setOutput("computed_path", path);
setOutput("error_code", 0);
setOutput("distance_traveled", 15.3);

3. 내부 동작 과정

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

setOutput("path", computed_path)
    │
    ├─ 1단계: 포트 매핑 조회
    │       │
    │       ├─ 포트 "path"의 XML 매핑 확인
    │       │       │
    │       │       ├─ path="{nav_path}" → 블랙보드 키 "nav_path"
    │       │       │
    │       │       └─ XML에서 미지정 → 매핑 없음
    │       │
    │       └─ 매핑 정보 반환
    │
    ├─ 2단계: 블랙보드 기록
    │       │
    │       ├─ 매핑 존재 → 블랙보드["nav_path"] = computed_path
    │       │
    │       └─ 매핑 없음 → 아무 작업도 수행하지 않음 (무시)
    │
    └─ 결과: 성공 또는 실패

3.1 매핑 부재 시의 동작

출력 포트가 XML에서 블랙보드 키에 매핑되지 않은 경우, setOutput() 호출은 아무 작업도 수행하지 않고 무시된다. 이 동작은 출력 포트의 사용을 항상 선택적으로 만든다.

<!-- path 출력 포트가 매핑됨 → setOutput("path", ...) 유효 -->
<ComputePathToPose goal="{target}" path="{nav_path}" />

<!-- path 출력 포트가 매핑되지 않음 → setOutput("path", ...) 무시 -->
<ComputePathToPose goal="{target}" />

노드의 코드에서 setOutput()은 두 경우 모두 동일하게 호출된다. 매핑 여부에 따른 분기 처리는 필요하지 않다.

4. 반환값의 처리

setOutput()의 반환값인 BT::Result는 대부분의 사용 사례에서 검사하지 않는다. setOutput()이 실패하는 경우는 드물며, 주로 포트 명칭이 providedPorts()에 선언되지 않은 경우에 해당한다.

// 반환값 무시 (일반적 사용)
setOutput("path", computed_path);

// 반환값 검사 (디버깅 시)
auto result = setOutput("path", computed_path);
if (!result)
{
    RCLCPP_WARN(node_->get_logger(),
        "setOutput 실패: %s", result.error().c_str());
}

5. 기록 시점 패턴

5.1 완료 시 결과 기록

노드의 행동이 완료되어 SUCCESS 또는 FAILURE를 반환하기 직전에 최종 결과를 기록하는 패턴이다.

BT::NodeStatus onRunning() override
{
    if (isPathComputed())
    {
        auto path = getComputedPath();
        setOutput("path", path);
        setOutput("path_cost", computePathCost(path));
        setOutput("error_code", 0);
        return BT::NodeStatus::SUCCESS;
    }

    if (hasError())
    {
        setOutput("error_code", getErrorCode());
        return BT::NodeStatus::FAILURE;
    }

    return BT::NodeStatus::RUNNING;
}

5.2 진행 중 피드백 기록

비동기 노드에서 RUNNING 상태 동안 중간 피드백을 기록하는 패턴이다.

BT::NodeStatus onRunning() override
{
    if (isNavigationComplete())
    {
        setOutput("final_pose", getCurrentPose());
        return BT::NodeStatus::SUCCESS;
    }

    // 매 Tick마다 피드백 갱신
    setOutput("distance_remaining", getDistanceRemaining());
    setOutput("estimated_time_sec", getEstimatedTime());
    return BT::NodeStatus::RUNNING;
}

이 패턴은 Parallel 노드의 다른 자식이 진행 중인 노드의 상태를 참조하는 경우에 유용하다.

<Parallel success_count="1">
    <NavigateToPose goal="{target}"
                     distance_remaining="{dist}" />
    <MonitorProgress distance="{dist}"
                      threshold="0.5" />
</Parallel>

5.3 즉시 기록 (동기 노드)

SyncActionNode에서는 tick() 내에서 결과를 산출한 즉시 기록한다.

BT::NodeStatus tick() override
{
    double a, b;
    if (!getInput("value_a", a) || !getInput("value_b", b))
        return BT::NodeStatus::FAILURE;

    double result = a + b;
    setOutput("sum", result);
    return BT::NodeStatus::SUCCESS;
}

6. 노드 유형별 setOutput() 사용 패턴

노드 유형권장 기록 시점비고
SyncActionNodetick() 내 결과 산출 후반환 직전
StatefulActionNodeonRunning()에서 완료 시SUCCESS/FAILURE 직전
StatefulActionNodeonRunning()에서 매 Tick피드백 갱신용
ThreadedActiontick() 종료 직전스레드 안전성 고려
CoroActionNodetick() 내 결과 산출 후yield 전 또는 반환 전

6.1 ThreadedAction에서의 setOutput()

ThreadedAction에서 setOutput()tick() 스레드에서 호출된다. 블랙보드는 내부적으로 뮤텍스에 의해 보호되므로 setOutput() 호출 자체는 스레드 안전하다. 그러나 tick() 스레드의 종료 직전에 출력을 기록하는 것이 권장된다.

BT::NodeStatus tick() override
{
    // 입력 읽기 (시작부)
    std::string target;
    getInput("target", target);

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

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

    // 출력 기록 (종료 직전)
    setOutput("result", result.data);
    setOutput("status_code", result.code);
    return result.success
        ? BT::NodeStatus::SUCCESS
        : BT::NodeStatus::FAILURE;
}

7. 다수의 출력 포트에 대한 기록

7.1 성공 시 전체 출력 기록

BT::NodeStatus onRunning() override
{
    if (isDetectionComplete())
    {
        auto detection = getDetectionResult();
        setOutput("object_id", detection.id);
        setOutput("object_pose", detection.pose);
        setOutput("confidence", detection.confidence);
        setOutput("bounding_box", detection.bbox);
        return BT::NodeStatus::SUCCESS;
    }
    return BT::NodeStatus::RUNNING;
}

7.2 실패 시 오류 출력 기록

BT::NodeStatus onRunning() override
{
    if (hasError())
    {
        setOutput("error_code", getErrorCode());
        setOutput("error_message", getErrorMessage());
        return BT::NodeStatus::FAILURE;
    }
    // ...
}

7.3 상태별 출력 기록의 일관성

노드가 SUCCESS를 반환할 때 기록하여야 하는 출력과 FAILURE를 반환할 때 기록하여야 하는 출력을 명확히 구분한다.

반환 상태기록 대상예시
SUCCESS결과 데이터 + 성공 코드경로, 검출 결과, 오류 코드 0
FAILURE오류 정보오류 코드, 오류 메시지
RUNNING피드백 데이터 (선택적)남은 거리, 진행률

8. setOutput()과 블랙보드의 상호 작용

8.1 값의 덮어쓰기

setOutput()은 블랙보드의 해당 키에 저장된 기존 값을 무조건 덮어쓴다. 이전 값의 보존이 필요한 경우 양방향 포트(BidirectionalPort)를 사용하여 기존 값을 읽은 후 수정하여 기록한다.

8.2 타입 보존

setOutput()에 의해 기록된 값의 타입은 블랙보드의 Any 타입 저장소에 보존된다. 이후 다른 노드에서 getInput()으로 읽을 때, 저장된 타입과 요청 타입이 일치하면 직접 대입되고, 불일치하면 변환이 시도된다.

// 노드 A: nav_msgs::msg::Path 타입으로 기록
setOutput("path", computed_path);

// 노드 B: 동일한 타입으로 읽기 → 직접 대입
nav_msgs::msg::Path path;
getInput("path", path);  // 타입 일치 → 성공

8.3 블랙보드 키의 생성

setOutput()이 호출될 때 해당 블랙보드 키가 아직 존재하지 않으면, 키가 자동으로 생성된다. 따라서 출력 포트를 사용하는 노드가 입력 포트를 사용하는 노드보다 먼저 실행되어야 한다는 순서 제약이 형성된다.

9. setOutput()의 실패 원인

setOutput()이 실패하는 경우는 드물지만, 다음의 원인이 존재한다.

실패 원인상황
포트명 미등록providedPorts()에 해당 포트명이 선언되지 않음
포트 방향 불일치InputPort로 선언된 포트에 setOutput() 호출

이 실패는 개발 단계에서 검출되어야 하며, 런타임에서 발생하는 경우 포트 선언과 사용의 불일치를 나타내는 구현 오류이다.

10. getInput()과 setOutput()의 비대칭성

getInput()setOutput()은 대칭적으로 보이나, 실패 시의 동작에서 비대칭성을 가진다.

특성getInput()setOutput()
매핑 미지정 시실패 (기본값 없는 경우)무시 (정상)
블랙보드 키 미존재 시실패키 자동 생성
반환값 검사 필요성높음 (필수 권장)낮음 (선택적)
타입 변환필요 시 수행불필요 (원본 타입 저장)

이 비대칭성은 입력은 노드의 동작에 필수적인 반면, 출력은 다른 노드에 의한 소비가 선택적이라는 설계 철학을 반영한다.

11. 설계 지침

  1. SUCCESS 반환 전 결과 기록 완료: 노드가 SUCCESS를 반환하기 전에 모든 결과 출력 포트에 유효한 값을 기록한다.

  2. FAILURE 반환 전 오류 정보 기록: 노드가 FAILURE를 반환하기 전에 오류 코드 또는 오류 메시지를 출력 포트에 기록하여, 후속 노드의 오류 처리를 지원한다.

  3. 매핑 부재에 대한 견고성: setOutput()이 매핑 부재 시 무시됨을 활용하여, 코드에서 매핑 여부를 검사하지 않는다. 모든 출력 포트에 항상 setOutput()을 호출한다.

  4. 출력 기록의 시점 집중: StatefulActionNode에서 출력 기록을 SUCCESS/FAILURE 반환 직전에 집중하여, 출력의 시점을 명확히 한다.

  5. ThreadedAction에서 종료 직전 기록: tick() 스레드에서 블랙보드 접근을 최소화하고, 결과가 확정된 종료 직전에 출력을 기록한다(Faconti, 2022).