1293.25 노드의 Running 유지와 재진입

1. RUNNING 유지의 개념

RUNNING 유지란 행동 트리(Behavior Tree)의 비동기 노드가 복수의 연속적인 Tick에 걸쳐 RUNNING 상태를 유지하는 동작을 의미한다. 비동기 작업은 일반적으로 단일 Tick 내에서 완료되지 않으므로, 작업이 진행되는 동안 매 Tick마다 RUNNING을 반환하여 아직 완료되지 않았음을 부모 노드에 보고한다(Colledanchise & Ogren, 2018).

RUNNING 상태의 유지는 행동 트리가 동기적 폴링(synchronous polling) 모델에서 비동기 작업을 처리하는 핵심 메커니즘이다. 노드는 Tick을 수신할 때마다 작업의 현재 상태를 확인하고, 완료되었으면 SUCCESS 또는 FAILURE를 반환하고, 아직 진행 중이면 RUNNING을 반환한다.

2. 재진입의 정의

재진입(re-entry)이란 이전 Tick에서 RUNNING을 반환한 노드에 다음 Tick이 다시 전달되는 것을 의미한다. 재진입 시 노드는 새로운 작업을 시작하는 것이 아니라, 이전에 시작한 작업의 현재 상태를 확인하고 그 결과를 반환한다. 이는 IDLE 상태에서의 첫 Tick(초기 진입)과 구별되는 동작이다.

BehaviorTree.CPP의 StatefulActionNode에서 이 구별은 onStart()onRunning() 콜백의 분리를 통해 명시적으로 표현된다(Faconti, 2022).

class AsyncAction : public BT::StatefulActionNode {
    // 초기 진입: IDLE에서 처음 Tick될 때
    BT::NodeStatus onStart() override {
        start_async_operation();
        return BT::NodeStatus::RUNNING;
    }
    
    // 재진입: RUNNING 상태에서 다시 Tick될 때
    BT::NodeStatus onRunning() override {
        if (is_operation_complete()) {
            return operation_succeeded() 
                ? BT::NodeStatus::SUCCESS 
                : BT::NodeStatus::FAILURE;
        }
        return BT::NodeStatus::RUNNING;
    }
    
    void onHalted() override {
        cancel_async_operation();
    }
};

3. RUNNING 유지의 시간적 패턴

비동기 노드가 RUNNING 상태를 유지하는 기간은 작업의 특성에 따라 다양하다.

작업 유형일반적 RUNNING 유지 기간예시
네비게이션수 초 ~ 수 분목표 위치까지 이동
매니퓰레이션수 초물체 파지 및 배치
대기 (Wait)지정 시간일정 시간 대기 후 진행
센서 스캔수 초360도 LiDAR 스캔 완료
원격 서비스 호출수백 ms ~ 수 초클라우드 기반 인식 요청

RUNNING 유지 기간 동안의 Tick 횟수는 N_{running} = \lceil D_{task} / T_{tick} \rceil로 추정되며, 여기서 D_{task}는 작업의 소요 시간이다.

4. 재진입 시의 상태 보존

RUNNING 상태를 유지하는 동안 노드는 내부 상태를 보존해야 한다. 재진입 시 이전 Tick에서의 작업 진행 상황, 누적된 데이터, 외부 시스템과의 연결 상태 등이 유지되어야 하며, 매 Tick마다 이들을 재초기화해서는 안 된다.

이를 위해 비동기 노드의 내부 변수는 onStart()에서 초기화하고, onRunning()에서는 읽기 및 갱신만 수행하는 패턴을 따른다.

class TrackObject : public BT::StatefulActionNode {
    BT::NodeStatus onStart() override {
        // 초기 진입: 상태 초기화
        tracking_started_ = std::chrono::steady_clock::now();
        lost_count_ = 0;
        return BT::NodeStatus::RUNNING;
    }
    
    BT::NodeStatus onRunning() override {
        // 재진입: 누적 상태 활용
        if (is_object_visible()) {
            lost_count_ = 0;
            update_tracking();
            
            if (is_tracking_complete()) {
                return BT::NodeStatus::SUCCESS;
            }
        } else {
            lost_count_++;
            if (lost_count_ > max_lost_frames_) {
                return BT::NodeStatus::FAILURE;
            }
        }
        return BT::NodeStatus::RUNNING;
    }
    
private:
    std::chrono::steady_clock::time_point tracking_started_;
    int lost_count_ = 0;
    int max_lost_frames_ = 30;
};

5. 재진입 경로와 부모 노드의 메모리

RUNNING 상태의 노드에 Tick이 재전달되는 경로는 부모 노드의 메모리 속성(WithMemory 또는 WithoutMemory)에 따라 결정된다.

5.1 WithMemory 모드에서의 재진입

WithMemory 속성의 Sequence 노드는 이전 Tick에서 RUNNING을 반환한 자식의 인덱스를 기억하고, 다음 Tick에서 해당 자식부터 직접 재진입한다. 이전에 SUCCESS를 반환한 자식 노드는 건너뛴다.

Tick k: Sequence → Child_0(SUCCESS) → Child_1(RUNNING)
Tick k+1: Sequence → Child_1(재진입, Child_0 건너뜀)

이 방식은 이미 완료된 작업을 불필요하게 재평가하지 않으므로 효율적이지만, 중간에 조건이 변화하였을 때 이를 감지하지 못하는 제약이 있다.

5.2 WithoutMemory (Reactive) 모드에서의 재진입

Reactive 모드의 제어 노드는 매 Tick마다 첫 번째 자식부터 재평가한다. RUNNING 상태의 자식에 도달하면 해당 자식을 재진입시킨다.

Tick k: ReactiveSequence → Child_0(SUCCESS) → Child_1(RUNNING)
Tick k+1: ReactiveSequence → Child_0(재평가) → Child_1(재진입)

Reactive 모드에서는 조건 노드가 매 Tick마다 재평가되므로, 조건이 FAILURE로 변경되면 RUNNING 중인 자식이 Halt되어 작업이 중단된다.

6. 재진입 시의 비용 고려

재진입 시 onRunning() 콜백의 실행 비용은 Tick 실행 시간의 일부를 구성한다. onRunning()이 수행하는 작업이 가벼우면(예: 플래그 확인, 완료 여부 폴링) 재진입 비용이 미미하지만, 무거운 작업(예: 센서 데이터 처리, 경로 재계획)을 수행하면 Tick 실행 시간이 증가하여 Tick 오버런의 위험이 높아진다.

이상적인 onRunning() 구현은 비동기 작업의 완료 여부만 확인하는 경량 폴링(lightweight polling)으로 설계하고, 무거운 연산은 별도의 스레드에서 수행하도록 분리한다.

7. RUNNING 유지의 장기화에 대한 대응

비동기 작업이 예상보다 오래 RUNNING 상태를 유지하는 경우, 이를 감지하고 대응하는 메커니즘이 필요하다.

7.1 내부 타임아웃

노드 자체적으로 최대 RUNNING 유지 시간을 설정하고, 이를 초과하면 FAILURE를 반환한다.

BT::NodeStatus onRunning() override {
    auto elapsed = std::chrono::steady_clock::now() - start_time_;
    if (elapsed > max_duration_) {
        cancel_operation();
        return BT::NodeStatus::FAILURE;
    }
    // 작업 상태 확인...
    return BT::NodeStatus::RUNNING;
}

7.2 외부 타임아웃 데코레이터

Timeout 데코레이터 노드를 사용하여 외부에서 시간 제한을 적용할 수 있다. 자식 노드가 지정된 시간 내에 SUCCESS 또는 FAILURE를 반환하지 않으면, 데코레이터가 자식을 Halt하고 FAILURE를 반환한다.

<Timeout msec="5000">
    <NavigateToGoal goal="{target_pose}"/>
</Timeout>

7.3 진행률 모니터링

RUNNING 유지 중 작업의 진행률(progress)을 모니터링하여, 진전이 없는 정체 상태(stagnation)를 감지할 수 있다. 일정 시간 동안 진행률의 변화가 없으면 작업이 교착 상태에 빠진 것으로 판단하고 FAILURE를 반환한다.


참고 문헌

  • Colledanchise, M., & Ogren, P. (2018). Behavior Trees in Robotics and AI: An Introduction. CRC Press.
  • Faconti, D. (2022). BehaviorTree.CPP documentation and API reference. https://www.behaviortree.dev/