C++26 Reflection의 숨겨진 컴파일 타임 비용: 모듈은 우리를 구원하지 못한다


솔직히 말해서, 나는 C++ 컴파일 타임 최적화에 꽤나 집착하는 편이다. 15년 넘게 대규모 C++ 코드베이스를 다루다 보면, 컴파일 타임은 단순한 대기 시간이 아니라 엔지니어의 집중력과 생산성을 파괴하는 침묵의 살인자라는 것을 뼈저리게 느끼게 된다.

최근 C++26에 리플렉션(Reflection)이 도입된다는 소식을 들었을 때, 매크로 범벅이던 직렬화(Serialization) 코드와 보일러플레이트를 마침내 걷어낼 수 있겠다는 생각에 환호했다. 하지만 WG21 위원회가 리플렉션을 코어 언어 기능이 아닌 무거운 표준 라이브러리(STL)에 강하게 결합시키는 방향으로 결정했다는 것을 알았을 때, 등골이 서늘해졌다.

Vittorio Romeo가 최근 작성한 블로그 글을 보며 내 우려가 단순한 기우가 아니었음을 확인했다. 오늘은 C++26 리플렉션이 가져올 컴파일 타임의 재앙과, 우리가 직면한 현실적인 문제들을 깊게 파헤쳐 보려고 한다.

리플렉션 자체는 빠르다. 문제는 언제나 STL이다

Romeo의 벤치마크 결과를 보면 매우 흥미로운 사실을 발견할 수 있다. GCC 16(실험적 지원)을 사용해 측정된 결과를 분석해 보자.

  • Baseline:main 함수 컴파일은 약 43.9ms가 소요된다.
  • Flag Cost: -freflection 컴파일러 플래그만 켜는 것은 43.1ms로, 오버헤드가 전혀 없다.
  • The Culprit: #include <meta>를 추가하는 순간 컴파일 타임은 310.4ms로 폭증한다.

즉, 컴파일러가 리플렉션 메타데이터를 생성하고 처리하는 코어 언어적 기능 자체는 놀라울 정도로 가볍다. 진짜 문제는 단 하나의 구조체를 리플렉션하기 위해 우리가 무거운 <meta> 헤더를 끌고 들어와야 한다는 점이다.

다음은 벤치마크에 사용된 기본적인 구조체 리플렉션 코드다.

template <typename T> void reflect_struct(const T& obj) {
    template for (constexpr std::meta::info field :
        std::define_static_array(std::meta::nonstatic_data_members_of(
            ^^T, std::meta::access_context::current()))) {
        use(std::meta::identifier_of(field));
        use(obj.[:field:]);
    }
}

문법 자체는 훌륭하다. ^^T 연산자를 통해 메타 정보를 추출하고, [:field:]를 통해 필드에 접근하는 방식은 매우 직관적이다. 하지만 이 코드를 파싱하기 위해 컴파일러는 수많은 STL 헤더를 텍스트로 밀어 넣고 분석해야 한다. 프로젝트의 수백, 수천 개의 TU(Translation Unit)에서 이 헤더를 인클루드한다고 상상해 보라. 끔찍한 일이다.

의 합작이 만든 1.6초의 악몽

상황은 더 복잡한 메타프로그래밍을 할 때 급격히 악화된다. Barry Revzin이 작성한 ‘AoS(Array of Structures)를 SoA(Structure of Arrays)로 변환하는 예제’를 보면 현대 C++의 병폐가 그대로 드러난다.

해당 코드는 <meta>, <ranges>, <print>를 모두 사용하는데, 단일 TU 컴파일에 무려 1,622.0ms가 걸렸다. 여기서 범인을 색출해 보면 다음과 같다.

  • 제거: 1,622ms -> 540ms (약 1초 감소)
  • 제거: 540ms -> 391ms (약 150ms 감소)

나는 예전부터 <print>가 지나치게 무겁다고 비판해 왔다. 포매팅을 위해 fmtlib를 직접 가져다 쓰는 것이 컴파일 타임 측면에서 훨씬 유리한데도, 위원회는 표준 라이브러리를 끝없이 비대하게 만들고 있다.

”모듈이 우리를 구원할 것이다”라는 위원회의 거짓말

이쯤 되면 누군가는 반드시 이렇게 말할 것이다. “C++20 모듈(Modules)을 쓰면 해결되잖아요?”

나도 한때는 그렇게 믿었다. 하지만 Romeo가 import std;를 사용하여 진행한 추가 벤치마크 결과는 절망적이다.

  • 기본 리플렉션 (PCH): 208.7ms
  • 기본 리플렉션 (Modules): 352.8ms
  • AoS to SoA (PCH): 1.261s
  • AoS to SoA (Modules): 1.077s

<meta>만 필요한 단순한 상황에서는 오히려 구시대의 유물인 PCH(Precompiled Headers)가 모듈보다 압도적으로 빠르다. 복잡한 예제에서는 모듈이 미세하게 앞서지만, 여전히 1초가 넘어가는 재앙적인 수준이다. 모듈은 결코 마법의 탄환이 아니다. 적어도 현재의 컴파일러 구현 상태에서는 그렇다.

Hacker News의 반응: 커져가는 위기감

Hacker News 스레드에서도 나와 비슷한 피로감을 호소하는 시니어 엔지니어들의 반응을 쉽게 찾아볼 수 있었다.

가장 공감 갔던 의견은 libc++와 libstdc++의 피처 크립(Feature creep)을 지적하는 댓글이었다. 언어의 핵심 기능(리플렉션)이 거대한 STL에 종속되는 것은 생태계에 큰 해악을 끼친다. “모듈이 구원할 것”이라는 만트라는 순진한 생각이라는 비판도 정확히 핵심을 찔렀다.

재미있게도 어떤 30년 차 C++ 개발자는 Translation Unit의 약자인 ‘TU’라는 단어를 처음 봤다며 혼란스러워하기도 했다. WG21 주변이나 빌드 시스템을 깊게 파고드는 사람들이 아니면 의외로 생소할 수 있는 용어이긴 하다. 하지만 우리가 앞으로 리플렉션을 쓰려면 TU 단위의 빌드 최적화에 대해 뼛속까지 이해해야만 할 것이다.

결론: 우리는 무엇을 준비해야 하는가

C++26 리플렉션은 분명 강력하다. 매크로 떡칠이 된 레거시 코드를 우아하게 리팩토링할 수 있는 최고의 도구가 될 것이다. 하지만 그 대가는 혹독하다.

Jonathan Müller가 제안했던 P3429(<meta> 헤더의 STL 의존성 최소화) 제안서가 더 많은 지지를 받지 못한 것이 천추의 한으로 남는다. 코어 언어의 혁신이 라이브러리의 무게에 짓눌리는 꼴이다.

현업에서 C++26을 도입할 계획이라면 다음을 명심해야 한다. PCH나 Unity Build(Jumbo Build)의 적극적인 도입은 이제 선택이 아니라 생존을 위한 필수가 될 것이다. 서드파티 라이브러리가 <meta>를 헤더에서 함부로 인클루드하지 않도록 엄격하게 코드 리뷰를 해야 할지도 모른다.

새로운 장난감이 생겼다고 무턱대고 모든 곳에 리플렉션을 발라대기 전에, 여러분의 CI 빌드 시간이 얼마나 늘어날지 먼저 계산해 보길 바란다. 기능의 화려함 뒤에 숨겨진 영수증은 결국 엔지니어인 우리가 치러야 하니까.


References: