1293.49 Halt 시 자원 정리 순서
1. 자원 정리의 필요성
행동 트리(Behavior Tree)에서 Halt가 호출되면, RUNNING 상태의 노드가 보유하고 있는 자원을 체계적으로 해제해야 한다. 자원 정리가 불완전하면 자원 누수(resource leak), 외부 시스템의 일관성 위반, 후속 실행에서의 충돌 등이 발생할 수 있다. 자원 정리의 순서는 자원 간의 의존 관계에 따라 결정되며, 이 순서를 올바르게 설정하는 것이 안전한 중단 처리의 핵심이다(Colledanchise & Ogren, 2018).
2. 자원 정리의 일반적 순서
비동기 액션 노드에서 Halt 시 자원 정리는 다음 순서를 따르는 것이 권장된다.
2.1 단계: 외부 작업 취소
가장 먼저 외부 시스템에 위임된 작업의 취소를 요청한다. 이 단계가 우선되어야 하는 이유는, 외부 작업이 물리적 세계에 영향을 미치므로 가능한 한 빨리 중단시켜야 안전이 보장되기 때문이다.
void onHalted() override {
// 1단계: 외부 작업 취소
if (goal_handle_) {
action_client_->async_cancel_goal(goal_handle_);
}
if (velocity_publisher_) {
publishZeroVelocity(); // 이동 정지 명령
}
2.2 단계: 통신 자원 정리
구독, 타이머, 서비스 클라이언트 등 통신 관련 자원을 정리한다. 이 단계에서 불필요한 콜백의 실행을 방지한다.
// 2단계: 통신 자원 정리
if (timer_) {
timer_->cancel();
}
// 일시적 구독자 해제 (필요 시)
2.3 단계: 내부 상태 리셋
노드의 내부 상태 변수(카운터, 플래그, 캐시 등)를 초기값으로 리셋한다.
// 3단계: 내부 상태 리셋
goal_handle_.reset();
retry_count_ = 0;
is_initialized_ = false;
cached_result_.reset();
2.4 단계: 블랙보드 정리 (선택적)
Halt 시 블랙보드에 기록된 중간 결과를 정리해야 하는 경우, 이 단계에서 수행한다. 모든 경우에 필요한 것은 아니며, 중간 결과가 후속 실행에 부정적 영향을 미치는 경우에만 수행한다.
// 4단계: 블랙보드 정리 (선택적)
// setOutput("partial_result", std::nullopt);
}
3. 제어 노드의 자원 정리 순서
제어 노드의 Halt 시 자원 정리는 자식 노드에 대한 Halt 전파를 포함한다.
1. 자식 노드에 Halt 전파 (하위 서브트리의 자원 정리)
2. 내부 상태 리셋 (메모리 인덱스, 카운터 등)
3. 자신의 상태를 IDLE로 설정
자식에 대한 Halt 전파가 가장 먼저 수행되어야 하는 이유는, 자식의 자원 정리가 완료된 후에야 부모의 상태 리셋이 의미를 가지기 때문이다.
void SequenceNode::halt() {
// 1. 자식에 Halt 전파
haltRunningChildren();
// 2. 내부 상태 리셋
current_child_index_ = 0;
// 3. 상태 리셋
resetStatus();
}
4. 중첩 트리에서의 자원 정리 순서
중첩된 트리 구조에서 Halt가 전파될 때, 자원 정리는 깊이 우선(depth-first) 순서로 수행된다. 리프 노드(액션 노드)의 자원이 먼저 정리되고, 그 후 상위 제어 노드의 내부 상태가 리셋된다.
Halt 전파 및 자원 정리 순서:
ReactiveSequence.halt()
1. Sequence.halt() (자식에 Halt 전파)
1a. Navigate.halt() (리프 노드 자원 정리)
→ async_cancel_goal()
→ goal_handle_.reset()
→ resetStatus() → IDLE
1b. current_index = 0 (Sequence 내부 상태 리셋)
1c. resetStatus() → IDLE
2. resetStatus() → IDLE (ReactiveSequence 상태 리셋)
이 순서에서 Navigate의 외부 작업 취소가 가장 먼저 실행되고, 그 후 Sequence의 메모리 인덱스가 리셋되며, 마지막으로 ReactiveSequence의 상태가 리셋된다.
5. 자원 의존 관계에 따른 정리 순서 결정
자원 간 의존 관계가 존재하는 경우, 의존되는 자원보다 의존하는 자원을 먼저 정리해야 한다.
의존 관계: A → B (A가 B에 의존)
정리 순서: A 먼저 정리, 그 다음 B 정리
예를 들어, 네비게이션 액션이 특정 토픽 구독에 의존하는 경우:
void onHalted() override {
// 1. 네비게이션 취소 (구독에 의존)
action_client_->async_cancel_goal(goal_handle_);
// 2. 구독 해제 (네비게이션이 더 이상 필요 없으므로)
// subscription_.reset(); // 필요 시
// 3. 내부 상태 리셋
goal_handle_.reset();
}
6. 자원 정리의 원자성
이상적으로 자원 정리는 원자적(atomic)으로 수행되어야 한다. 즉, 정리 과정의 중간에서 다른 Tick이나 콜백이 실행되지 않아야 한다. BehaviorTree.CPP의 단일 스레드 실행 모델에서 onHalted()는 Tick 실행 스레드 내에서 동기적으로 호출되므로, 다른 Tick이 onHalted() 실행 중에 개입할 수 없다. 그러나 ROS2 콜백이 별도 스레드에서 실행되는 경우, onHalted() 실행 중에 콜백이 실행될 수 있으므로 적절한 동기화가 필요하다.
7. 정리 실패 시의 안전 장치
자원 정리 과정에서 일부 단계가 실패하더라도, 나머지 단계는 계속 수행되어야 한다. 이를 위해 각 정리 단계를 독립적으로 실행하고, 예외를 개별적으로 처리한다.
void onHalted() override {
// 각 단계를 독립적으로 실행
try {
action_client_->async_cancel_goal(goal_handle_);
} catch (...) {
RCLCPP_ERROR(logger_, "Goal cancellation failed");
}
try {
publishZeroVelocity();
} catch (...) {
RCLCPP_ERROR(logger_, "Zero velocity publish failed");
}
// 내부 상태는 항상 리셋
goal_handle_.reset();
retry_count_ = 0;
}
이 패턴에서 목표 취소가 실패하더라도 속도 정지 명령은 전송되며, 내부 상태도 리셋된다. 하나의 정리 단계 실패가 전체 정리 과정을 중단시키지 않는다.
참고 문헌
- 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/