1294.102 Sequence와 Fallback 설계의 안티패턴
1. 안티패턴의 정의와 식별 기준
행동 트리에서 안티패턴이란 겉보기에는 합리적이지만, 실제 운용에서 예기치 않은 동작, 성능 저하, 또는 안전 위험을 초래하는 설계 구조를 의미한다. 이러한 안티패턴은 트리가 정상 상황에서는 올바르게 동작하지만, 경계 조건이나 비정상 상황에서 문제를 드러내는 경우가 많다. Sequence와 Fallback 제어 노드의 안티패턴을 식별하고 회피하는 것은 견고한 로봇 행동 설계의 핵심이다(Colledanchise & Ogren, 2018).
2. 안티패턴 1: 변형 혼동
2.1 문제
SequenceNode(WithMemory)와 ReactiveSequence의 차이를 인식하지 못하고 잘못된 변형을 사용하는 경우이다.
<!-- 안티패턴: 안전 조건을 SequenceNode에 배치 -->
<Sequence>
<Condition ID="IsBatteryOK"/> <!-- 최초 1회만 평가 -->
<Action ID="LongMission"/> <!-- 수 분간 RUNNING -->
</Sequence>
IsBatteryOK는 최초 Tick에서 SUCCESS를 반환하면 Memory에 의해 건너뛰어진다. 임무 수행 중 배터리가 위험 수준으로 감소해도 조건이 재평가되지 않는다.
2.2 수정
<ReactiveSequence>
<Condition ID="IsBatteryOK"/> <!-- 매 Tick 재평가 -->
<Action ID="LongMission"/>
</ReactiveSequence>
안전 조건은 반드시 ReactiveSequence에 배치하여 매 Tick 재평가되도록 한다.
3. 안티패턴 2: 최후 수단 미포함 Fallback
3.1 문제
<!-- 안티패턴: 최후 수단 없음 -->
<Fallback>
<Action ID="Navigate"/>
<Action ID="AlternativeNavigate"/>
</Fallback>
두 대안이 모두 FAILURE를 반환하면 Fallback이 FAILURE를 반환하고, 이 FAILURE가 상위 트리로 전파된다. 상위 트리에 적절한 처리가 없으면 로봇이 정의되지 않은 상태에 놓일 수 있다.
3.2 수정
<Fallback>
<Action ID="Navigate"/>
<Action ID="AlternativeNavigate"/>
<Action ID="SafeStop"/> <!-- 항상 SUCCESS 반환 -->
</Fallback>
마지막 자식에 항상 SUCCESS를 반환하는 안전 행동을 배치하여 Fallback이 절대 FAILURE를 반환하지 않도록 보장한다.
4. 안티패턴 3: ReactiveSequence에서의 고비용 조건
4.1 문제
<!-- 안티패턴: 고비용 조건의 매 Tick 재평가 -->
<ReactiveSequence>
<Condition ID="IsFullPathCollisionFree"/> <!-- 10ms -->
<Condition ID="IsMapConsistent"/> <!-- 5ms -->
<Action ID="FollowPath"/>
</ReactiveSequence>
고비용 조건이 매 Tick 재평가되면 Tick 주기의 상당 부분을 소비한다. Tick 주기 100ms 기준으로 조건 평가만 15ms를 소비한다.
4.2 수정
<ReactiveSequence>
<Condition ID="IsPathClearCached"/> <!-- 캐시 조회 <0.1ms -->
<Condition ID="IsMapConsistentCached"/> <!-- 캐시 조회 <0.1ms -->
<Action ID="FollowPath"/>
</ReactiveSequence>
고비용 계산은 별도 스레드에서 비동기적으로 수행하고, 조건 노드는 캐시된 결과만 조회한다.
5. 안티패턴 4: 과도한 자식 수
5.1 문제
<!-- 안티패턴: 자식 15개의 Sequence -->
<Sequence>
<Action ID="Step01"/>
<Action ID="Step02"/>
...
<Action ID="Step15"/>
</Sequence>
과도한 자식 수는 가독성을 저하시키고, 디버깅을 어렵게 하며, 최악 Tick 시간을 증가시킨다.
5.2 수정
<Sequence>
<SubTree ID="Preparation"/> <!-- Step01-05 -->
<SubTree ID="Execution"/> <!-- Step06-10 -->
<SubTree ID="Finalization"/> <!-- Step11-15 -->
</Sequence>
논리적 단위로 서브트리를 분해하여 각 제어 노드의 자식 수를 줄인다.
6. 안티패턴 5: 부적절한 onHalted() 구현
6.1 문제
// 안티패턴: onHalted()에서 고비용 동기 작업 수행
void onHalted() override {
robot_.stopMotors(); // 동기 I/O, 50ms
robot_.parkArm(); // 동기 I/O, 100ms
logger_.flushToDisk(); // 디스크 I/O, 200ms
}
ReactiveSequence에서 조건 변화에 의해 자식이 Halt될 때, 이 콜백이 Tick 시간에 추가된다. 350ms의 Halt 비용은 대부분의 Tick 주기를 초과한다.
6.2 수정
void onHalted() override {
robot_.requestStopAsync(); // 비동기 정지 요청, 즉시 반환
// 리소스 해제는 별도 스레드에서 수행
}
onHalted() 콜백은 즉시 반환해야 한다. 시간이 걸리는 정리 작업은 비동기적으로 수행한다.
7. 안티패턴 6: 단일 자식 제어 노드
7.1 문제
<!-- 안티패턴: 자식 1개의 Sequence -->
<Sequence>
<Action ID="DoSomething"/>
</Sequence>
<!-- 안티패턴: 자식 1개의 Fallback -->
<Fallback>
<Action ID="DoSomething"/>
</Fallback>
자식이 1개인 Sequence나 Fallback은 자식의 반환 상태를 그대로 전달할 뿐, 제어 흐름에 기여하지 않는다. 불필요한 노드가 트리를 복잡하게 만든다.
7.2 수정
자식을 직접 부모 노드의 자식으로 배치하거나, 단일 자식 제어 노드를 제거한다.
8. 안티패턴 7: 조건 없는 ReactiveSequence
8.1 문제
<!-- 안티패턴: 조건 없이 모든 자식이 행동 노드 -->
<ReactiveSequence>
<Action ID="Action1"/>
<Action ID="Action2"/>
<Action ID="Action3"/>
</ReactiveSequence>
ReactiveSequence의 존재 이유는 앞쪽 조건의 매 Tick 재평가이다. 조건 노드 없이 행동 노드만 배치하면, 이전에 SUCCESS를 반환한 행동이 매 Tick 재실행되어 부작용이 반복 발생한다.
8.2 수정
조건 재평가가 필요 없는 순차 행동에는 SequenceNode를 사용한다:
<Sequence>
<Action ID="Action1"/>
<Action ID="Action2"/>
<Action ID="Action3"/>
</Sequence>
9. 안티패턴 8: Sequence와 Fallback의 역할 혼동
9.1 문제
<!-- 안티패턴: 복구 동작을 Sequence로 구성 -->
<Sequence>
<Action ID="Method1"/> <!-- 실패 시 즉시 전체 FAILURE -->
<Action ID="Method2"/>
<Action ID="Method3"/>
</Sequence>
대안적 방법을 Sequence로 구성하면, 첫 번째 방법의 실패가 전체를 중단시킨다. 대안 시도에는 Fallback이 적합하다.
9.2 수정
<Fallback>
<Action ID="Method1"/> <!-- 실패 시 다음 대안 시도 -->
<Action ID="Method2"/>
<Action ID="Method3"/>
</Fallback>
10. 안티패턴 9: 조건과 행동의 순서 역전
10.1 문제
<!-- 안티패턴: 행동 후 조건 검사 -->
<Sequence>
<Action ID="StartMotors"/>
<Condition ID="IsHardwareReady"/> <!-- 모터 시작 후에 검사 -->
</Sequence>
행동 노드가 조건 노드보다 앞에 배치되면, 조건 불충족 시 이미 수행된 행동의 부작용(모터 시작)이 남는다.
10.2 수정
<Sequence>
<Condition ID="IsHardwareReady"/> <!-- 먼저 검사 -->
<Action ID="StartMotors"/>
</Sequence>
조건을 행동보다 앞에 배치하여, 조건 불충족 시 행동의 부작용이 발생하지 않도록 한다.
11. 안티패턴 10: 무한 Fallback 체인
11.1 문제
<!-- 안티패턴: 동일 동작의 반복적 Fallback -->
<Fallback>
<Action ID="Navigate"/>
<Action ID="Navigate"/> <!-- 동일 동작 반복 -->
<Action ID="Navigate"/> <!-- 동일 동작 반복 -->
</Fallback>
동일한 행동을 여러 대안으로 배치해도, 동일한 이유로 동일하게 실패할 가능성이 높다. 대안은 서로 다른 전략을 구현해야 의미가 있다.
11.2 수정
<Fallback>
<Action ID="Navigate"/>
<Sequence>
<Action ID="ClearCostmap"/> <!-- 복구 행동 후 재시도 -->
<Action ID="Navigate"/>
</Sequence>
<Action ID="SafeStop"/>
</Fallback>
각 대안이 이전 대안의 실패 원인을 해결하는 복구 행동을 포함하여, 서로 다른 전략을 구현한다.
12. 안티패턴 식별 체크리스트
| 안티패턴 | 증상 | 확인 방법 |
|---|---|---|
| 변형 혼동 | 조건 변화 미감지 | XML 태그 확인 |
| 최후 수단 미포함 | 간헐적 전체 FAILURE | Fallback 마지막 자식 확인 |
| 고비용 조건 재평가 | Tick 시간 초과 | 프로파일링 |
| 과도한 자식 수 | 가독성 저하, 디버깅 곤란 | 자식 수 계수 |
| 부적절한 onHalted() | Halt 시 지연 | 타이밍 측정 |
| 단일 자식 제어 노드 | 불필요한 복잡성 | 트리 구조 검토 |
| 조건 없는 Reactive | 부작용 반복 | Reactive 자식 유형 확인 |
| Sequence/Fallback 혼동 | 의도와 다른 제어 흐름 | 의미론 검증 |
| 조건-행동 순서 역전 | 불필요한 부작용 | 자식 순서 검토 |
| 무한 Fallback 체인 | 동일 실패 반복 | 대안 다양성 검증 |
이 체크리스트를 설계 검토 시 적용하여 안티패턴을 사전에 식별하고 수정한다(Faconti, 2022).
참고 문헌
- 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/