1296.98 액션 노드 설계의 모범 사례

1. 개요

액션 노드 설계의 모범 사례(best practice)는 행동 트리 기반 로봇 시스템에서 재사용성, 유지보수성, 안전성, 테스트 용이성을 극대화하기 위한 검증된 설계 지침이다. 이러한 모범 사례는 BehaviorTree.CPP 프레임워크의 설계 철학, Nav2 프로젝트의 실무 경험, 그리고 소프트웨어 공학의 일반적 원칙에 기반한다.

2. 노드 설계 원칙

2.1 단일 책임 원칙 준수

하나의 액션 노드는 하나의 명확한 작업만을 수행해야 한다. 복합 작업을 하나의 노드에 구현하면 재사용성이 저하되고, 오류 격리가 어려워진다.

<!-- 모범 사례: 분리된 노드 -->
<Sequence>
    <CaptureImage image="{img}" />
    <DetectObject image="{img}" detections="{det}" />
    <PlanGrasp detections="{det}" grasp="{grasp}" />
    <ExecuteGrasp grasp="{grasp}" />
</Sequence>

<!-- 안티패턴: 단일 노드에 복합 기능 -->
<CaptureDetectAndGrasp />

2.2 파라미터화를 통한 재사용

하드코딩된 상수 대신 입력 포트를 통해 파라미터를 외부화하여, 동일 노드를 다양한 맥락에서 재사용한다.

// 모범 사례: 파라미터화된 포트
static BT::PortsList providedPorts()
{
    return {
        BT::InputPort<std::string>("action_server",
            "navigate_to_pose",
            "액션 서버 이름"),
        BT::InputPort<double>("timeout", 30.0,
            "타임아웃 (초)"),
        BT::InputPort<double>("tolerance", 0.5,
            "목표 허용 오차 (미터)")
    };
}

2.3 명시적 포트 선언

노드가 사용하는 모든 입출력 데이터를 providedPorts()에 명시적으로 선언한다. 블랙보드에 직접 접근하여 포트 선언 없이 데이터를 읽고 쓰는 것은 노드의 인터페이스를 불투명하게 만든다.

// 모범 사례: 명시적 포트 선언
static BT::PortsList providedPorts()
{
    return {
        BT::InputPort<double>("target_altitude",
            "목표 고도"),
        BT::OutputPort<double>("reached_altitude",
            "도달 고도")
    };
}

// 안티패턴: 직접 블랙보드 접근
BT::NodeStatus tick() override
{
    auto bb = config().blackboard;
    double alt;
    bb->get("some_hidden_key", alt);  // 포트 선언 없음
}

3. 상태 반환 규칙

3.1 SUCCESS와 FAILURE의 명확한 기준

SUCCESSFAILURE의 반환 기준을 명확히 정의하고 문서화한다.

반환 상태기준
SUCCESS요청된 작업이 의도한 결과를 달성함
FAILURE작업 수행이 불가하거나 실패 조건을 충족함
RUNNING작업이 진행 중이며 아직 완료되지 않음

탐지 결과가 0개인 경우에도 탐지 알고리즘이 정상적으로 실행되었다면 SUCCESS를 반환하는 것이 올바르다. 탐지된 물체의 존재 여부는 후속 조건 노드에서 판단한다.

3.2 RUNNING의 제한적 사용

SyncActionNode에서는 RUNNING을 절대 반환하지 않는다. StatefulActionNode에서는 작업이 실제로 진행 중일 때만 RUNNING을 반환하며, 무한 RUNNING을 방지하기 위해 타임아웃을 반드시 구현한다.

4. Halt 구현

4.1 완전한 onHalted() 구현

비동기 노드에서 onHalted()는 반드시 다음을 수행해야 한다.

  1. 진행 중인 외부 작업 취소 (ROS2 액션 취소 등)
  2. 모든 자원 해제 (구독, 타이머, 파일 핸들 등)
  3. 내부 상태 변수 초기화
  4. 필요시 안전 상태 전환
void onHalted() override
{
    if (goal_handle_)
    {
        action_client_->async_cancel_goal(goal_handle_);
        goal_handle_.reset();
    }
    subscription_.reset();
    goal_completed_ = false;
    goal_succeeded_ = false;
}

4.2 Halt 후 재진입 보장

onHalted() 이후 onStart()가 다시 호출되었을 때, 이전 실행의 잔여 상태가 영향을 미치지 않도록 완전한 초기화를 수행한다.

5. ROS2 연동 설계

5.1 노드 인스턴스 주입

ROS2 노드 인스턴스를 생성자를 통해 주입(injection)하여, 테스트 시 모의 노드로 대체할 수 있도록 한다.

class MyAction : public BT::StatefulActionNode
{
public:
    MyAction(const std::string& name,
             const BT::NodeConfiguration& config,
             rclcpp::Node::SharedPtr node)
        : BT::StatefulActionNode(name, config),
          node_(node)
    {
        // ROS2 클라이언트 초기화
    }
};

// 팩토리 등록 시 빌더 패턴 사용
factory.registerBuilder<MyAction>(
    "MyAction",
    [node](const%20std::string&%20name,
%20%20%20%20%20%20%20%20%20%20%20const%20BT::NodeConfiguration&%20config)
    {
        return std::make_unique<MyAction>(
            name, config, node);
    });

5.2 QoS 일관성

센서 데이터 토픽은 SensorDataQoS(), 명령 토픽은 ReliableQoS()를 사용하여 발행자-구독자 간 QoS 호환성을 보장한다.

5.3 서버 가용성 검사

onStart()에서 ROS2 서비스 또는 액션 서버의 가용성을 확인하고, 연결 불가 시 즉시 FAILURE를 반환한다.

if (!action_client_->wait_for_action_server(
        std::chrono::seconds(3)))
{
    RCLCPP_ERROR(node_->get_logger(),
        "서버 연결 실패: %s", server_name.c_str());
    return BT::NodeStatus::FAILURE;
}

6. 오류 처리

6.1 실패 원인 보고

FAILURE 반환 시 출력 포트에 오류 코드와 메시지를 기록하여, 상위 노드에서 실패 원인에 따른 분기를 수행할 수 있도록 한다.

6.2 예외 안전 보장

모든 외부 호출을 try-catch로 감싸고, 예외 발생 시에도 자원이 올바르게 해제되도록 RAII 패턴을 활용한다.

6.3 로깅 전략

  • onStart() 진입 시: DEBUG 레벨로 입력 파라미터 기록
  • FAILURE 반환 시: ERROR 레벨로 실패 원인 기록
  • SUCCESS 반환 시: DEBUG 레벨로 결과 요약 기록
  • 주기적 진행 상황: DEBUG 레벨로 빈도 제한(throttle) 로깅

7. 테스트 설계

7.1 테스트 용이한 구조

외부 의존성을 인터페이스로 추상화하여, 테스트 시 모의 객체로 대체할 수 있도록 설계한다.

7.2 필수 테스트 항목

  • 정상 입력에 대한 SUCCESS 반환
  • 잘못된 입력에 대한 FAILURE 반환
  • 서버 미응답 시 타임아웃
  • Halt 후 재시작
  • 입출력 포트 정확성

8. 네이밍 규칙

8.1 노드 이름

동사 + 명사 형태로 작업의 내용을 명확히 표현한다.

좋은 이름나쁜 이름
NavigateToPoseDoNavigation
CaptureImageCameraAction
DetectObjectPerception
PublishStatusStatusNode
ComputePathToPosePathPlanner

8.2 포트 이름

소문자 스네이크 케이스(snake_case)를 사용하고, 데이터의 의미를 명확히 표현한다.

좋은 이름나쁜 이름
target_altitudealt
acceptance_radiusr
error_messagemsg
detection_countcnt

9. 성능 고려

9.1 tick() 실행 시간 최소화

SyncActionNodetick()은 수 밀리초 이내에 반환해야 한다. 장시간 블로킹 연산은 StatefulActionNode로 전환한다.

9.2 불필요한 자원 생성 방지

매 tick마다 ROS2 발행자나 구독자를 새로 생성하지 않고, 생성자에서 한 번만 초기화하거나 캐싱하여 재사용한다.

9.3 블랙보드 대용량 데이터 주의

이미지나 포인트 클라우드와 같은 대용량 데이터를 블랙보드에 저장할 때는 공유 포인터(std::shared_ptr)를 사용하여 불필요한 복사를 방지한다.

10. 참고 문헌

  • Colledanchise, M. and Ögren, P., “Behavior Trees in Robotics and AI: An Introduction,” CRC Press, 2018.
  • Faconti, D. and Contributors, “BehaviorTree.CPP: A C++ library to build Behavior Trees,” GitHub Repository, https://github.com/BehaviorTree/BehaviorTree.CPP.
  • Macenski, S. et al., “The Marathon 2: A Navigation System,” arXiv preprint arXiv:2003.00368, 2020.
  • Martin, R. C., “Clean Code: A Handbook of Agile Software Craftsmanship,” Prentice Hall, 2008.

버전: 2026-04-04