1296.7 동기 액션 노드의 즉시 반환 특성

1. 즉시 반환의 정의

동기 액션 노드(SyncActionNode)의 “즉시 반환(immediate return)” 특성이란, tick() 메서드가 호출되면 행동을 완료하고 SUCCESS 또는 FAILURE를 반환한 후 제어를 즉시 부모 노드에 반환하는 것을 의미한다. 중간에 RUNNING 상태로 실행을 유보하지 않으며, tick() 호출과 반환 사이에 다른 노드가 Tick될 기회가 없다.

2. 즉시 반환의 동작 모델

동기 액션 노드의 실행 흐름을 시간 축에서 도식화하면 다음과 같다.

시간 →
─────────────────────────────────────────
  부모 Tick   │  SyncAction tick()  │  부모에 결과 반환
              │←  실행 시간 (~μs)  →│
─────────────────────────────────────────

비동기 액션 노드와 비교하면 차이가 명확하다.

시간 →
─────────────────────────────────────────────────────────
Tick 1:  부모 Tick  │ StatefulAction onStart() │ RUNNING 반환
Tick 2:  부모 Tick  │ StatefulAction onRunning()│ RUNNING 반환
Tick 3:  부모 Tick  │ StatefulAction onRunning()│ SUCCESS 반환
─────────────────────────────────────────────────────────

동기 액션은 단일 Tick에서 시작과 완료가 동시에 이루어지며, 비동기 액션은 다수의 Tick에 걸쳐 진행된다.

3. 즉시 반환이 트리 실행에 미치는 영향

3.1 Sequence에서의 즉시 진행

Sequence의 자식으로 SyncActionNode가 배치되면, 해당 자식이 SUCCESS를 반환하는 즉시 다음 자식으로 진행한다. 이는 동일 Tick 내에서 다수의 SyncActionNode가 순차적으로 실행될 수 있음을 의미한다.

<Sequence>
    <SetSpeed value="0.5"/>         <!-- SyncAction: 즉시 SUCCESS -->
    <SetNavigationMode mode="fast"/><!-- SyncAction: 즉시 SUCCESS -->
    <NavigateToGoal goal="{target}"/><!-- StatefulAction: RUNNING -->
</Sequence>

이 Sequence에서 첫 번째 Tick 시 SetSpeedSetNavigationMode가 동일 Tick 내에서 순차적으로 실행되고, NavigateToGoal이 RUNNING을 반환하면 Sequence도 RUNNING을 반환한다. 두 SyncActionNode는 각각 별도의 Tick을 소비하지 않는다.

3.2 Fallback에서의 즉시 진행

Fallback에서 SyncActionNode가 FAILURE를 반환하면 동일 Tick 내에서 다음 자식으로 진행한다.

<Fallback>
    <TryPlanA/>  <!-- SyncAction: FAILURE → 즉시 다음으로 -->
    <TryPlanB/>  <!-- SyncAction: SUCCESS → Fallback SUCCESS -->
</Fallback>

3.3 Parallel에서의 동작

Parallel 노드에서 SyncActionNode는 매 Tick에서 재실행된다. 이미 SUCCESS를 반환한 경우라도, Parallel의 구현에 따라 다시 Tick될 수 있다.

4. 즉시 반환의 성능적 함의

4.1 단일 Tick 내 다중 실행

SyncActionNode의 즉시 반환 특성은 단일 Tick 내에서 다수의 경량 작업을 연속으로 수행할 수 있음을 의미한다. 이는 초기화, 설정, 전처리 등의 작업을 별도의 Tick 오버헤드 없이 수행할 수 있는 장점을 제공한다.

<Sequence>
    <!-- 이하 3개의 SyncAction이 동일 Tick에서 실행 -->
    <ClearCostmap service="global_costmap/clear"/>
    <UpdateGoal goal="{new_goal}"/>
    <LogMessage message="Starting navigation"/>
    <!-- 이 행동부터 별도 Tick -->
    <NavigateToGoal goal="{new_goal}"/>
</Sequence>

4.2 Tick 시간에 대한 누적 영향

다수의 SyncActionNode가 동일 Tick에서 실행되면, 각 노드의 실행 시간이 누적되어 전체 Tick 시간에 영향을 미친다. 개별 SyncActionNode의 실행 시간이 수 마이크로초이면 수십 개가 누적되어도 문제가 되지 않으나, 개별 실행 시간이 수백 마이크로초 이상이면 누적 영향을 고려하여야 한다.

T_{\text{tick}} \geq \sum_{i=1}^{k} T_{\text{sync}_i} + T_{\text{async}}

여기서 k는 동일 Tick에서 실행되는 SyncActionNode의 수이다.

즉시 반환과 부작용

SyncActionNode의 즉시 반환은 부작용(side effect)이 즉시 발생함을 의미한다. 예를 들어, 토픽 발행 SyncActionNode가 SUCCESS를 반환하면 해당 메시지가 이미 발행된 상태이다. 이는 Sequence의 후속 자식이 실패하여 전체 Sequence가 FAILURE를 반환하더라도, 이미 발행된 메시지를 취소할 수 없다는 것을 의미한다.

<Sequence>
    <PublishStartSignal/>  <!-- 즉시 발행: 취소 불가 -->
    <NavigateToGoal/>      <!-- 실패 시: 시작 신호는 이미 발행됨 -->
</Sequence>

이러한 비가역적 부작용을 가진 SyncActionNode는 Sequence에서의 배치 위치를 신중히 결정하여야 한다. 가능하면 부작용이 큰 작업을 후반에 배치하거나, 실패 시 보상(compensation) 행동을 Fallback으로 구성한다.

RUNNING 반환 금지의 강제

BehaviorTree.CPP는 SyncActionNode가 RUNNING을 반환하는 것을 컴파일 타임이 아닌 런타임에서 검출한다. executeTick() 내부에서 반환값을 검사하고, RUNNING이면 LogicError 예외를 발생시킨다.

NodeStatus SyncActionNode::executeTick()
{
    auto status = ActionNodeBase::executeTick();
    if (status == NodeStatus::RUNNING)
    {
        throw LogicError(
            "SyncActionNode::executeTick() must not return RUNNING. "
            "Did you forget to use StatefulActionNode?");
    }
    return status;
}

이 예외 메시지는 개발자에게 StatefulActionNode로의 전환을 안내한다. SyncActionNode에서 비동기 작업이 필요한 경우 StatefulActionNode를 선택하여야 한다.