1317.23 플래너 플러그인 아키텍처
1. 플러그인 아키텍처의 설계 동기
자동화된 계획 수립(automated planning) 분야에는 POPF, TFD, Fast Downward, LAMA, Metric-FF 등 다양한 플래너가 존재하며, 각 플래너는 서로 다른 PDDL 요구사항 수준을 지원하고 상이한 탐색 전략을 사용한다. 로봇 시스템의 임무 특성에 따라 최적의 플래너가 달라질 수 있으므로, PlanSys2는 특정 플래너에 대한 하드코딩된 의존성을 제거하고 플러그인 아키텍처(plugin architecture)를 통해 플래너를 교체 가능한 구성 요소로 취급한다. 이 아키텍처는 ROS2 생태계의 pluginlib 프레임워크에 기반하며, 새로운 플래너의 통합이 기존 시스템 코드의 수정 없이 이루어질 수 있도록 설계되어 있다(Martín et al., 2021).
2. pluginlib 프레임워크의 기본 원리
pluginlib은 ROS2에서 제공하는 플러그인 관리 라이브러리로, C++ 클래스의 동적 로딩(dynamic loading)을 지원한다. 이 프레임워크의 핵심 원리는 다음과 같다.
- 기반 클래스 정의: 플러그인이 구현해야 하는 인터페이스를 추상 기반 클래스(abstract base class)로 정의한다.
- 플러그인 클래스 구현: 기반 클래스를 상속하여 구체적 기능을 구현한 파생 클래스(derived class)를 작성한다.
- 플러그인 등록:
PLUGINLIB_EXPORT_CLASS매크로를 통해 파생 클래스를pluginlib에 등록한다. - 플러그인 기술 파일: XML 형식의 플러그인 기술 파일(plugin description file)에 라이브러리 경로와 클래스 정보를 기재한다.
- 동적 로딩:
pluginlib::ClassLoader가 런타임에 공유 라이브러리(shared library)를 로딩하고 플러그인 인스턴스를 생성한다.
이 메커니즘은 호스트 애플리케이션(플래너 노드)이 플러그인의 구체적 구현을 컴파일 시점에 알 필요 없이, 실행 시점에 파라미터로 지정된 플러그인을 로딩할 수 있게 한다.
3. PlanSys2 플래너 플러그인의 기반 클래스
3.1 PlanSolverBase 인터페이스
PlanSys2에서 모든 플래너 플러그인은 plansys2::PlanSolverBase 추상 클래스를 상속해야 한다. 이 클래스는 플래너 플러그인이 반드시 구현해야 하는 인터페이스를 정의한다.
namespace plansys2
{
class PlanSolverBase
{
public:
virtual void configure(
rclcpp_lifecycle::LifecycleNode::SharedPtr node,
const std::string & plugin_name) = 0;
virtual std::optional<Plan> getPlan(
const std::string & domain,
const std::string & problem,
const std::string & node_namespace) = 0;
virtual ~PlanSolverBase() {}
};
}
각 메서드의 역할은 다음과 같다.
| 메서드 | 역할 |
|---|---|
configure | 플러그인 초기화 시 호출되며, 노드 핸들과 플러그인 이름을 전달받아 파라미터 로딩 등 초기 설정을 수행한다 |
getPlan | 도메인 문자열과 문제 문자열을 입력받아 계획을 생성하여 반환한다. 계획 수립 실패 시 std::nullopt을 반환한다 |
3.2 Plan 자료 구조
getPlan 메서드가 반환하는 Plan 자료 구조는 계획을 구성하는 개별 액션 항목(plan item)의 벡터로 정의된다. 각 항목은 다음과 같은 필드를 포함한다.
- time: 액션의 시작 시각(부동 소수점, 단위: 초)
- action: 액션의 PDDL 표현 문자열(예:
(move robot1 kitchen bedroom)) - duration: 액션의 지속 시간(부동 소수점, 단위: 초)
이 표현 구조는 시간적 계획(temporal plan)과 고전적 순차 계획(classical sequential plan)을 모두 수용할 수 있다. 순차 계획의 경우, 각 액션의 시작 시각은 이전 액션의 종료 시각 이후로 설정된다.
4. 플러그인 구현의 절차
4.1 클래스 구현
새로운 플래너 플러그인을 구현하려면, PlanSolverBase를 상속하는 클래스를 작성하고 getPlan 메서드 내에서 외부 플래너를 호출하는 로직을 구현한다. 일반적인 구현 패턴은 다음과 같다.
- 임시 파일 생성: 수신된 도메인 및 문제 PDDL 문자열을 임시 디렉터리의 파일로 저장한다.
- 외부 프로세스 실행: 플래너 실행 파일을 자식 프로세스(child process)로 생성하고, 도메인 파일과 문제 파일의 경로를 인자로 전달한다.
- 프로세스 대기: 자식 프로세스의 종료를 대기하며, 종료 코드를 확인한다.
- 출력 파싱: 플래너의 출력 파일을 읽어
Plan자료 구조로 변환한다. - 임시 파일 정리: 생성된 임시 파일을 삭제한다.
4.2 플러그인 등록
구현된 클래스를 pluginlib에 등록하기 위해, 소스 파일의 끝에 다음과 같은 매크로를 선언한다.
#include "pluginlib/class_list_macros.hpp"
PLUGINLIB_EXPORT_CLASS(
my_planner_plugin::MyPlanSolver,
plansys2::PlanSolverBase)
이 매크로는 컴파일러에게 해당 클래스가 PlanSolverBase 타입의 플러그인으로 등록되어야 함을 지시한다.
4.3 플러그인 기술 파일 작성
플러그인의 메타데이터를 기술하는 XML 파일을 작성한다. 이 파일은 pluginlib이 플러그인을 탐색하고 로딩하는 데 필요한 정보를 포함한다.
<library path="my_plan_solver">
<class type="my_planner_plugin::MyPlanSolver"
base_class_type="plansys2::PlanSolverBase">
<description>Custom planner plugin for PlanSys2</description>
</class>
</library>
path: 컴파일된 공유 라이브러리의 이름(확장자 제외)type: 플러그인 클래스의 전체 이름공간(namespace) 경로base_class_type: 기반 클래스의 전체 이름공간 경로
4.4 CMakeLists.txt 구성
빌드 시스템에 플러그인을 등록하기 위해, CMakeLists.txt에 다음과 같은 항목을 추가한다.
add_library(my_plan_solver SHARED
src/my_plan_solver.cpp)
target_link_libraries(my_plan_solver
${plansys2_core_LIBRARIES})
pluginlib_export_plugin_description_file(
plansys2_core my_plan_solver_plugin.xml)
install(TARGETS my_plan_solver
LIBRARY DESTINATION lib)
pluginlib_export_plugin_description_file 매크로는 플러그인 기술 XML 파일을 ROS2 패키지의 리소스 인덱스에 등록하여, pluginlib::ClassLoader가 이를 발견할 수 있게 한다.
5. 플래너 노드에서의 플러그인 로딩 과정
플래너 노드는 초기화 시 다음과 같은 절차를 통해 플래너 플러그인을 로딩한다.
- 파라미터 읽기:
plan_solver_plugins파라미터에서 사용할 플래너 플러그인의 이름 목록을 읽는다. - ClassLoader 생성:
pluginlib::ClassLoader<plansys2::PlanSolverBase>를 생성하여PlanSolverBase타입의 플러그인을 탐색할 수 있는 로더를 초기화한다. - 플러그인 인스턴스 생성: 각 플러그인 이름에 대해 하위 파라미터에서 플러그인 클래스의 전체 경로를 읽고,
ClassLoader::createSharedInstance를 호출하여 플러그인 인스턴스를 생성한다. - 플러그인 구성: 생성된 인스턴스의
configure메서드를 호출하여 노드 핸들과 플러그인 고유 파라미터를 전달한다. - 플러그인 저장: 로딩된 플러그인 인스턴스를 내부 맵(map)에 저장하여 계획 수립 요청 시 호출할 수 있도록 한다.
이 과정에서 플러그인 로딩에 실패하면(라이브러리가 존재하지 않거나 클래스 이름이 잘못된 경우), 플래너 노드는 오류 로그를 출력하고 해당 플러그인 없이 동작을 계속하거나, 모든 플러그인 로딩이 실패한 경우 비활성 상태를 유지한다.
6. 플러그인 간의 격리와 독립성
각 플래너 플러그인은 독립적인 인스턴스로 생성되며, 서로 간의 상태를 공유하지 않는다. 이러한 격리(isolation) 설계는 다음과 같은 이점을 제공한다.
- 구성 독립성: 각 플러그인은 고유한 파라미터 이름공간(parameter namespace)을 가지므로, 플래너별 구성(실행 파일 경로, 시간 제한, 탐색 옵션 등)을 독립적으로 설정할 수 있다.
- 장애 격리: 특정 플러그인의 실패가 다른 플러그인에 영향을 미치지 않는다.
- 동시 사용 가능성: 복수의 플러그인이 로딩된 경우, 임무 특성에 따라 적절한 플러그인을 선택하여 호출할 수 있다.
7. 아키텍처의 확장성
플래너 플러그인 아키텍처는 PlanSys2의 확장성(extensibility)을 보장하는 핵심 설계 요소이다. 새로운 플래너를 PlanSys2에 통합하기 위해 필요한 작업은 오직 플러그인 클래스의 구현, 플러그인 기술 파일의 작성, 빌드 시스템 등록으로 한정된다. 플래너 노드의 소스 코드나 PlanSys2의 핵심 패키지를 수정할 필요가 없다. 이는 소프트웨어 공학의 개방-폐쇄 원칙(Open-Closed Principle)을 충실히 구현한 것이며, 로봇 공학 연구자와 개발자가 자신의 플래너를 PlanSys2 프레임워크 내에서 쉽게 실험하고 평가할 수 있는 기반을 제공한다(Martín et al., 2021; Gamma et al., 1995).
이러한 플러그인 기반 접근법은 Nav2(Navigation2) 등 ROS2 생태계의 다른 주요 프레임워크에서도 동일하게 채택되고 있으며, ROS2 기반 로봇 소프트웨어의 모듈화와 재사용성을 촉진하는 표준적 패턴으로 자리잡고 있다(Macenski et al., 2020).
참고 문헌
- Martín, F., Cañas, J. M., Ginés, J., & Fuentetaja, R. (2021). “PlanSys2: A Planning System Framework for ROS2.” IEEE/RSJ International Conference on Intelligent Robots and Systems (IROS).
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1995). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Macenski, S., Martín, F., White, R., & Clavero, J. G. (2020). “The Marathon 2: A Navigation System.” IEEE/RSJ International Conference on Intelligent Robots and Systems (IROS).
버전: v1.0