1292.30 Tick과 노드 비활성화 (Halt)의 관계
1. 노드 비활성화의 정의
행동 트리(Behavior Tree)에서 노드의 비활성화(deactivation)란, 현재 Running 상태에 있는 노드가 진행 중인 작업을 중단하고 Idle 상태로 복귀하는 과정을 의미한다. 이 비활성화 과정을 halt라 한다. Halt는 노드가 점유하고 있는 자원을 해제하고, 내부 상태를 초기화하여 노드를 재사용 가능한 상태로 되돌리는 역할을 수행한다 (Colledanchise & Ögren, Behavior Trees in Robotics and AI: An Introduction, 2018).
tick이 노드를 활성화하는 유일한 메커니즘이라면, halt는 Running 상태의 노드를 명시적으로 비활성화하는 보완적 메커니즘이다. 노드가 Success 또는 Failure를 반환하여 자연적으로 종료되는 경우는 halt에 해당하지 않으며, halt는 외부의 명시적 요청에 의해 Running 상태의 노드가 강제적으로 중단되는 경우에만 적용된다.
2. Halt가 발생하는 조건
tick과 halt의 관계를 이해하기 위해서는 halt가 발생하는 조건을 정확히 파악하여야 한다. Halt는 다음의 상황에서 발생한다.
2.1 제어 흐름 노드의 정책 변경에 의한 Halt
제어 흐름 노드가 자신의 정책에 따라 실행 흐름을 변경할 때, 이전에 Running 상태에 있던 자식 노드에 halt를 요청한다. 대표적인 사례는 다음과 같다.
Sequence 노드에서의 halt: Sequence 노드의 자식 c_i가 이전 tick에서 Running을 반환하였으나, 후속 tick에서 c_i 이전의 자식(ReactiveSequence의 경우)이 Failure를 반환하면, Sequence 노드는 c_i에 halt를 요청한다. 이는 선행 조건이 더 이상 성립하지 않으므로 진행 중인 작업을 중단하여야 함을 의미한다.
Fallback 노드에서의 halt: Fallback 노드의 자식 c_i가 이전 tick에서 Running을 반환하였으나, 후속 tick에서 c_i 이전의 자식(ReactiveFallback의 경우)이 Success를 반환하면, Fallback 노드는 c_i에 halt를 요청한다. 이는 더 높은 우선순위의 대안이 성공하였으므로 현재 실행 중인 대안을 중단하여야 함을 의미한다.
Parallel 노드에서의 halt: Parallel 노드의 성공 또는 실패 임계값이 도달되면, 아직 Running 상태에 있는 나머지 자식 노드에 halt를 요청한다.
2.2 데코레이터 노드에 의한 Halt
데코레이터 노드 중 Timeout 데코레이터는 제한 시간이 경과하면 자식 노드에 halt를 요청하고 Failure를 반환한다. 이는 시간 제약 조건에 의한 강제적 비활성화이다.
2.3 트리 전체의 Halt
행동 트리의 실행을 전체적으로 중단하는 경우, 루트 노드에 halt가 요청되며, 이 halt 요청은 현재 Running 상태에 있는 모든 노드에 재귀적으로 전파된다. 이는 로봇의 비상 정지(emergency stop)와 같은 상황에서 발생한다.
3. Tick 부재와 Halt의 관계
tick과 halt의 관계에서 핵심적인 원리는 다음과 같다: Running 상태의 노드가 후속 tick에서 tick을 수신하지 못하면, 해당 노드는 반드시 halt되어야 한다.
이 원리를 형식적으로 기술하면 다음과 같다. 노드 N_i가 tick t_k에서 Running을 반환하였으나 tick t_{k+1}에서 tick을 수신하지 못하면, tick t_{k+1}의 실행 과정에서 N_i에 halt가 요청된다.
\text{status}(N_i, t_k) = Running \wedge \neg\text{ticked}(N_i, t_{k+1}) \implies \text{halted}(N_i, t_{k+1})
이 규칙은 Running 상태의 노드가 tick을 수신하지 못한 채로 방치되는 것을 방지한다. 방치된 Running 노드는 자원을 불필요하게 점유하며, 행동 트리의 상태 일관성을 훼손할 수 있다 (Faconti, BehaviorTree.CPP Documentation, 2024).
Halt의 실행 과정
Halt 요청의 하향 전파
halt 요청은 tick의 전파와 동일하게 상위 노드에서 하위 노드로 하향 전파된다. 제어 흐름 노드에 halt가 요청되면, 해당 노드는 현재 Running 상태에 있는 모든 자식 노드에 재귀적으로 halt를 요청한다. 이 재귀적 전파는 해당 서브트리 내의 모든 Running 노드가 Idle 상태로 복귀할 때까지 계속된다.
halt(node):
if node.status == Running:
if node is control_flow:
for each child in node.children:
halt(child)
node.onHalted()
node.status = Idle
onHalted 콜백의 호출
halt 요청을 수신한 노드는 onHalted() 콜백을 통해 비활성화 절차를 수행한다. 이 콜백에서는 다음과 같은 작업이 수행된다.
- 진행 중인 비동기 작업의 취소 요청
- 점유한 하드웨어 자원(액추에이터, 센서 등)의 해제
- 내부 상태 변수의 초기화
- 블랙보드에 기록한 임시 데이터의 정리
onHalted() 콜백의 실행이 완료되면 노드의 상태는 Idle로 설정된다.
Reactive 노드에서의 Tick과 Halt의 상호작용
반응형(reactive) 제어 흐름 노드에서 tick과 halt의 상호작용은 특히 빈번하게 발생한다. ReactiveSequence 노드는 매 tick마다 첫 번째 자식부터 재평가하므로, 이전에 Running 상태에 있던 자식이 halt되고 다른 자식이 새로 활성화되는 전환이 단일 tick 내에서 발생할 수 있다.
다음 예제를 통해 이를 설명한다.
Root
└─ ReactiveSequence [RS1]
├─ Condition [C1: 배터리 잔량 확인]
└─ Action [A1: 경로 추종]
Tick t_k: C1이 Success를 반환하고, A1이 Running을 반환한다. RS1도 Running을 반환한다.
Tick t_{k+1}: RS1은 첫 번째 자식 C1부터 재평가한다. C1이 Failure를 반환한다(배터리 부족). RS1은 즉시 Failure를 반환하며, Running 상태에 있는 A1에 halt를 요청한다. A1의 onHalted() 콜백이 호출되어 경로 추종 작업이 중단된다.
이 과정에서 단일 tick t_{k+1} 내에 C1의 평가, A1의 halt, RS1의 Failure 반환이 순차적으로 발생한다. 이는 반응형 노드가 환경 변화에 즉각적으로 대응하는 메커니즘의 핵심이다 (Colledanchise & Ögren, 2018).
Tick과 Halt의 시간적 관계
tick과 halt는 동일한 tick 주기 내에서 동기적으로 발생한다. 즉, halt 요청은 별도의 비동기 이벤트로 발생하는 것이 아니라, tick 전파 과정의 일부로서 동기적으로 처리된다. 이 동기적 처리는 행동 트리의 상태 일관성을 보장한다.
단일 tick 내에서 발생할 수 있는 활성화-비활성화 시퀀스는 다음과 같다.
| 시간 순서 | 사건 | 영향받는 노드 |
|---|---|---|
| 1 | tick 전파 시작 | — |
| 2 | 조건 노드 평가 | C1 활성화 |
| 3 | 제어 흐름 노드 정책 변경 결정 | — |
| 4 | 이전 Running 노드에 halt 요청 | A1 비활성화 |
| 5 | 새로운 자식에 tick 전달 | A2 활성화 |
| 6 | 반환 상태 상향 전파 | — |
이 시퀀스에서 halt(단계 4)와 새로운 활성화(단계 5)가 동일한 tick 내에서 순차적으로 발생함을 확인할 수 있다.
Halt의 안전성 요구사항
로봇 공학에서 halt의 구현은 안전성(safety) 측면에서 엄격한 요구사항을 충족하여야 한다.
첫째, halt는 신속하게 완료되어야 한다. halt 처리가 tick 주기를 초과하면 시스템의 실시간성이 훼손된다. 특히 비동기 액션 노드에서 진행 중인 작업의 취소가 지연되면, 로봇이 이미 중단된 것으로 간주되는 행동을 물리적으로 계속 수행할 수 있다.
둘째, halt는 멱등적(idempotent)이어야 한다. 동일한 노드에 halt가 중복으로 요청되더라도 부작용이 발생하지 않아야 한다.
셋째, halt 후 노드는 완전한 Idle 상태로 복귀하여야 한다. 후속 tick에서 onStart() 콜백이 호출될 때, 이전 실행의 잔여 상태가 남아 있지 않아야 한다. 잔여 상태의 존재는 비결정론적(non-deterministic) 행동의 원인이 될 수 있다 (Faconti, 2024).
활성화와 비활성화의 생명주기 통합
tick에 의한 활성화와 halt에 의한 비활성화를 통합하면, 노드의 완전한 생명주기(lifecycle)가 구성된다.
Idle \xrightarrow{\text{tick (onStart)}} Running \xrightarrow{\text{tick (onRunning)}} Running \xrightarrow{\text{Success/Failure}} Idle
Running \xrightarrow{\text{halt (onHalted)}} Idle
첫 번째 경로는 노드가 tick을 통해 활성화되고, 작업을 수행하며, 자연적으로 종료되는 정상 경로이다. 두 번째 경로는 halt에 의해 강제적으로 비활성화되는 중단 경로이다. 이 두 경로 모두 최종적으로 Idle 상태에 도달하며, 이후 새로운 tick에 의해 다시 활성화될 수 있다.
참고 문헌
- Colledanchise, M. & Ögren, P. (2018). Behavior Trees in Robotics and AI: An Introduction. CRC Press.
- Faconti, D. (2024). BehaviorTree.CPP Documentation. https://www.behaviortree.dev/