21.4.1. CRTP(Curiously Recurring Template Pattern) 다형성 최적화

21.4.1. CRTP(Curiously Recurring Template Pattern) 다형성 최적화

PX4의 ModuleBase 코드를 열어보면 가장 먼저 눈을 의심하게 만드는 문장이 바로 클래스의 상속 선언부이다.

class CustomApp : public ModuleBase<CustomApp> 
{
    // ...
};

자식이 부모를 상속받는데, 부모에게 제물(템플릿 파라미터)로 다름 아닌 자기 자신(CustomApp)의 고유한 타입을 바치고 있다. 마치 시간을 거슬러 올라가 할아버지에게 손자의 유전자를 주입하는 듯한 이 기괴하고 꼬리를 무는 문법을 설계 패턴 용어로 **CRTP(Curiously Recurring Template Pattern, 기묘하게 재귀하는 템플릿 패턴)**라고 부른다.

왜 PX4 코어 개발진은 널리 알려진 평범한 C++ 가상 함수(virtual) 상속을 놔두고, 이런 난해한 템플릿 흑마술을 시스템 코어 설계에 도입한 것일까?

1. 전통적인 ’런타임 다형성(Virtual)’의 치명적 속도 저하

일반적인 객체지향 프로그래밍(OOP)에서 여러 모듈의 공통 부모를 설계할 때, 우리는 흔히 추상 클래스와 가상 함수(Virtual Function)를 사용한다.

// 전통적인 OOP 방식 (PX4가 쓰지 않는 방식)
class ModuleBase {
public:
    virtual void Run() = 0; // 자식아, 알아서 구현해라
};

class CustomApp : public ModuleBase {
public:
    void Run() override { /* 실제 로직 */ }
};

이 방식은 코딩하기엔 편하지만, 200~400MHz로 돌아가는 픽스호크의 가난한 MCU(Micro Controller Unit) 환경에서는 성능의 극악한 죄악을 낳는다.

C++ 컴파일러가 가상 함수(virtual)를 처리하기 위해 프로그램 내부 메모리에 **가상 함수 테이블(V-Table: Virtual Table)**이라는 거대한 룩업(Lookup) 지도를 몰래 만들어버리기 때문이다.
나중에 운영체제가 module->Run()을 호출하려고 할 때, CPU는 곧바로 Run()의 기계어 주소로 점프하지 못한다.
대신 CPU는 메모리 어딘가에 있는 V-Table 지도를 찾아가서(1차 메모리 접근), “지금 이 module 포인터 안의 실제 알맹이가 CustomApp인가, EKF2인가?“를 런타임(Runtime)에 뒤져본 다음(2차 계산), 비로소 진짜 함수의 주소를 알아내어 점프한다(3차 점프).

이러한 동적 바인딩(Dynamic Binding) 과정은 1초에 1000번씩(1000Hz) 자세 제어 알고리즘을 쳐내는 비행 컨트롤러의 런루프 속에서 엄청난 명령 주기(Instruction Cycle) 오버헤드와 CPU 파이프라인 버블(Pipeline Bubble)을 유발한다.

2. CRTP: 컴파일 타임(Compile-Time)에 다형성을 확정 짓다

위의 V-Table 런타임 지연을 0(Zero)으로 만들어 버리기 위해 고안된 천재적인 트릭이 바로 CRTP이다.

CRTP 패턴을 적용한 ModuleBase의 부모 클래스 내부 구현을 살짝 들여다보자.

template<class T>
class ModuleBase {
public:
    void RunWrapper() {
        // 자식 클래스의 Run()을 호출하기 위한 무시무시한 캐스팅
        static_cast<T*>(this)->Run();
    }
};

이 기기묘묘한 코드의 작동 원리는 다음과 같다.

  1. 컴파일러의 타입 숙지: 우리가 class CustomApp : public ModuleBase<CustomApp> 이라고 적는 순간, C++ 컴파일러는 ModuleBase 템플릿의 T 자리에 CustomApp을 쾅 하고 박아 넣어 세상에 단 하나뿐인 ModuleBase_CustomApp 이라는 맞춤형 부모 클래스를 컴파일 타임에 찍어낸다.
  2. static_cast의 분노의 질주: 부모 클래스의 메서드 안에서 this 포인터를 무조건 자식 타입(T*)으로 강제 캐스팅(static_cast)한 뒤 Run() 함수를 부른다.
  3. V-Table의 증발: 컴파일러는 이미 T가 정확히 CustomApp이라는 사실을 컴파일(번역) 시점에 100% 확신하고 있다. 따라서 런타임에 V-Table을 뒤져볼 필요가 아예 사라진다.

결과적으로 컴파일러는 방해물 없이 CustomApp::Run() 함수의 진짜 메모리 번지를 기계어 코드 뎁스에 সরাসরি(직접) 하드코딩(Static Binding) 해버린다.
PX4는 CRTP 패턴을 통해 우아한 객체지향의 상속(코드 재사용) 이점은 그대로 챙기면서, 런타임 속도는 C 언어의 로우 레벨 매크로 함수를 호출하는 것과 완벽하게 똑같은 **‘Zero-Overhead(오버헤드 제로)’ 다형성(Polymorphism)**을 달성하게 된 것이다.

이제 컴파일 타임에 타입이 확정된다는 것이 코드 레벨에서 어떤 치명적인 검증(Validation) 마법을 부리는지, 다음 단원 21.4.1.1에서 더 딥(Deep)하게 파고들어 보자.