21.4. ModuleBase 템플릿 기반 PX4 표준 모듈 설계 기법
초창기 PX4의 C++ 모듈들은 각자도생(各自圖生) 그 자체였다. 어떤 모듈은 POSIX 스레드(pthreads)를 직접 띄워 while(1) 루프를 돌렸고, 다른 모듈은 타겟 보드 종속적인 타이머 인터럽트를 썼으며, 백그라운드 태스크 제어나 상태 출력(status 명령어) 방식마저 개발자마다 전부 제각각이었다.
세월이 흘러 PX4가 거대한 항공우주 소프트웨어 플랫폼으로 진화하면서, 코어 메인테이너들은 이러한 난잡한 아키텍처를 하나로 통일할 **‘표준 규격’**의 필요성을 절감하게 되었다.
그 결과 탄생한 최고의 걸작이 바로 ModuleBase 템플릿 클래스이다. 현재 PX4에 존재하는 거의 모든 공식 백그라운드 모듈(Commander, Navigator, EKF2 등)은, 그리고 앞으로 여러분이 짤 커스텀 모듈 역시 이 ModuleBase라는 거대한 거인의 어깨 위에서 상속(Inheritance)을 받아 작성된다.
1. ModuleBase: 보이지 않는 스케줄러의 요람
ModuleBase를 상속받는다는 것은, 단순히 편의성 함수 몇 개를 갖다 쓰는 수준이 아니다. 이것은 **PX4 커널(NuttX/SITL)이 내 모듈을 생성하고, 재우고, 깨우고, 주기적으로 호출하며, 안전하게 죽일 수 있는 완벽한 통제권(Control 권한)**을 운영체제에 양도하는 숭고한 계약이다.
1.1 거인의 어깨 위에 올라타기 위한 계약서
여러분이 짠 순수한 CustomApp 클래스를 ModuleBase 시스템 안으로 편입시키려면 다음과 같은 문법을 따라야 한다.
#include <px4_platform_common/module.h>
class CustomApp : public ModuleBase<CustomApp>
{
public:
// 1. ModuleBase가 나를 '생성'할 때 부를 생성자
CustomApp();
// 2. 모듈의 런루프가 실제로 도는 메인 톱니바퀴 (인터페이스 구현 의무)
void Run() override;
// 3. NSH 셸 명령어 처리기 (인터페이스 구현 의무)
static int task_spawn(int argc, char *argv[]);
static int custom_command(int argc, char *argv[]);
static int print_usage(const char *reason = nullptr);
};
위의 형태에 주목하라. CustomApp이 부모인 ModuleBase를 상속받는데, 그 부모의 템플릿 파라미터로 다시 자기 자신인 CustomApp을 집어넣고 있다(public ModuleBase<CustomApp>).
도대체 왜 이런 기괴하게 꼬리를 무는 상속 문법을 써야 하며, 내 클래스의 포인터를 부모에게 바스쳐야 하는 것일까? C++ 객체지향 설계의 진수이자, 펌웨어의 속도 한계(Virtual Table Overhead)를 박살 내 버린 컴파일 타임 흑마술인 **CRTP(Curiously Recurring Template Pattern)**의 경이로운 메커니즘을 다음 21.4.1 하위 단원에서 낱낱이 해부해 보자.