C++20 모듈을 사용해 라이브러리/네임스페이스를 캡슐화하는 방법, 모듈 파티션과 전역 모듈 프래그먼트로의 점진적 이행, 그리고 컴파일 시간 개선과 CMake 지원 현황을 소개합니다.
2026년 1월 24일
C++ 안에는 훨씬 더 작고 더 깔끔한 언어가, 밖으로 나오기 위해 분투하고 있다.
—Bjarne Stroustrup
C++ 애호가들은 종종 최신의 훌륭한 메타프로그래밍 기능 대신 C 전처리기를 사용한다고 당신을 깎아내리곤 하지만, 최근까지도 C++를 의미 있게 사용하려면 최소 한 가지 전처리기 지시문(#include)을 반드시 써야 했고, 이제는 더 이상 그렇지 않습니다.
C++20 모듈은 Qt, cv, std1 같은 라이브러리(또는 네임스페이스)를 캡슐화할 수 있는 방법을 제공합니다.
모듈 사용은 다음처럼 아주 간단합니다.
cppimport std; auto main() -> int { std::println("Hello world!"); }
자신만의 모듈을 만드는 것도 크게 다르지 않지만, 먼저 용어를 조금 정리해두는 편이 좋겠습니다:
.cpp 파일이든 이것이라고 생각하면 됩니다..h 파일과 이에 대응하는 여러 .cpp 파일처럼).import할 수 있도록 선언(클래스, 함수 등)을 내보낼 수 있습니다. export는 명시적입니다.이제 첫 번째 모듈로, 자료구조와 알고리즘 모듈을 선언해봅시다:
cpp// dsa.cpp export module dsa; namespace dsa { export int pow(int a, int b) { ... } }
정말 쉽죠. 그렇다면 레드-블랙 트리를 서브모듈로 추가해볼까요?
cpp// rbtree.cpp export module dsa.rbtree; export namespace dsa { enum class AllowDuplicates : bool { No, Yes, }; template<typename T, AllowDuplicates AllowDuplicates, typename Compare = std::less<T>> class RedBlackTree { ... } }
똑같습니다. 사실 컴파일러 관점에서는 서브모듈이라는 개념이 없습니다. dsa.rbtree는 dsa에 대해 “openai”가 “open”에 대해 갖는 관계와 같습니다.
“서브모듈”이란 게 없으니, 모듈이 서로 상호작용할 수 있는 방법은 공개 인터페이스를 통해서뿐이며 이는 설계 의도이기도 합니다. 하지만 이는 또한, 서로 연관된 여러 부분과 구현 세부사항을 포함한 거대한 모듈 유닛 하나를 갖게 된다는 뜻이기도 합니다. 그런 코드를 탐색하는 일은 악몽이 될 것입니다.
여기서 모듈 파티션(module partitions) 이 구원투수로 등장합니다. 모듈 파티션은 오직 자신의 이름 있는(named) 모듈과, 그 모듈 아래의 다른 모듈 파티션들만이 import할 수 있는 모듈 유닛입니다.
예를 들어, DSA 라이브러리에 여러 종류의 연결 리스트를 추가했고, 이들이 모두 비공개 Node 구조체를 공유한다고 합시다. 그러면 같은 모듈에 속하되, 서로와 당신의 모듈에서만 보이는 여러 의존 모듈로 코드를 분리할 수 있습니다.
cpp// linked_list.cpp export module dsa.linked_list; export import :circular_list; export import :ordered_list; export import :unordered_list; // circular_list.cpp import :node; // (비공개) Node 구조체를 선언하며, // 3가지 리스트 변형이 모두 공유한다. export namespace dsa { template <typename T> class CircularSinglyLinkedList { } }
퍼즐의 마지막 한 조각이 남았습니다: 하위 호환성입니다. 네, 모듈을 지원하지 않는 라이브러리도 모듈 안에서 사용할 수 있고, “전역 모듈 프래그먼트(global module fragment)” (module;)를 통해 코드를 점진적으로 모듈로 업그레이드할 수도 있습니다. 비공개 모듈 프래그먼트도 있지만 여기서는 다루지 않겠습니다.
cppmodule; #define GLFW_INCLUDE_NONE #include <GLFW/glfw3.h> #include <glad/gl.h> export module dsa.sortvis;
모듈을 시작하는 데 필요한 내용은 대략 이 정도입니다!
“제 C++ 프로젝트는 전부 헤더 파일을 쓰고 있고 완벽히 잘 동작하는데, 왜 굳이 번거롭게?”라고 생각할 수도 있겠습니다. 맞습니다. 하지만 어느 시점이 되면 C 모델은 특히 큰 프로젝트의 컴파일 시간에서 노후함이 드러나기 시작합니다. 게다가 전처리기 해킹을 충분히 많이(혹은 결국 하게 될 때까지 시간이 지나면) 쓰게 되면 추상화가 새기 시작하고, 하이럼의 법칙(Hyrum’s Law)과 맞닥뜨리게 됩니다.
C++에 “컴파일 시간” 문제가 있다는 건 꽤 쉽게 체감할 수 있습니다. 꽤 오랫동안 저는 CSES Problem Set의 대부분을 풀어보는 일을 스스로의 과제로 삼아왔는데, 이런 경쟁 프로그래밍 스타일 문제에서는 보통 문제를 분석하고 그럴듯한 알고리즘을 떠올리는 데 시간이 대부분 쓰이지만, 주요 C++ 컴파일러들의 컴파일 시간이 실제 병목이 되는 경우를 많이 겪었습니다. 4초 이상2 기다려야 하는 것은 그냥 흐름을 끊어버립니다.
제가 푼 문제들에 대한 µ벤치마크는 C++20 모듈이 확실한 승자임을 보여줍니다. 기본 Clang 대비 8.6배, PCH 대비 1.2배의 속도 향상을 제공합니다. 모듈을 사용하는 경쟁 프로그래밍 템플릿과 스크립트는 각주3를 참고하세요.
지루하게 만들고 싶지 않기도 하고(솔직히 제가 잘 설명하지도 못할 것 같아서), Clang에서 C++20 모듈을 어떻게 다루는지 여기서 설명하진 않겠습니다. 대신 Clang의 훌륭한 개발자들이 포괄적인 문서를 써두었습니다: Standard C++ Modules — Clang documentation (Clang 주변의 도구를 만들고 있지 않다면 읽을 필요 없습니다. CMake 쪽 사람들은 이미 그 작업을 해놨습니다.)
C++ 벤더에서 도구(tooling)까지 모듈 지원이 늦다는 점은, 프로젝트에서 모듈을 아예 고려하지 않을 만한 타당한 이유입니다. 하지만 개인 프로젝트는 상용 프로젝트에서 C++가 종종 요구하는 수준의 보장을 필요로 하지 않습니다(그리고 저는 대부분의 상용 프로젝트조차도 그렇지 않다고 생각합니다). 그리고 상황은 ‘절반만’ 나쁩니다. 현재 기준으로 주요 컴파일러들은 명세를 완전하게 또는 부분적으로 구현하고 있고, CMake는 모듈에 대한 완전한 지원을 제공하며 import std;에 대해서는 실험적 지원을 제공합니다. 제 기준에서는 이 정도면 충분합니다.
시작을 위한 최소 CMakeLists.txt는 다음과 같습니다:
cmakecmake_minimum_required(VERSION 3.28) project(dsa) set(CMAKE_CXX_SCAN_FOR_MODULES ON) set(CMAKE_CXX_STANDARD 23) add_library(dsa) target_sources(dsa PUBLIC FILE_SET dsa_public_modules TYPE CXX_MODULES FILES src/dsa.cpp ) add_executable(hello src/bin/hello.cpp) target_link_libraries(hello PRIVATE dsa)
또는 정말로 import std;(실험적)을 쓰고 싶다면, 다음 줄들을 추가해야 합니다:
cmake# 이 UUID는 사용 중인 버전의 CMake/Help/dev/experimental.rst 파일에서 찾을 수 있다. set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") set(CMAKE_CXX_MODULE_STD ON)
모듈 std와 std.compact는 C++23에서 표준 라이브러리 모듈 기능 하에 표준화되었습니다. ↩
이는 비표준인 “std의 모든 것을” 포함하는 파일(#include <bits/stdc++.h>) 또는 그 현대적(C++23) 동등물인 import std;를 포함하기 때문입니다. 제 소박한 머신도 감안해 주세요. ↩
다음은 매우 빠른 ./run 스크립트입니다.
bash#!/bin/bash STDPCM="std.pcm" STDCPPM="/usr/share/libc++/v1/std.cppm" INFILE="./$1" BINDIR="./bin" OUTFILE="$BINDIR/${1%.*}" [ -d "$BINDIR" ] || mkdir -p "$BINDIR" && \ [ -f "$STDPCM" ] \ || clang++ -fsanitize=address \ -std=c++23 \ -Wno-reserved-module-identifier \ -stdlib=libc++ \ --precompile \ -o "$STDPCM" "$STDCPPM" && \ [ -f "$OUTFILE" -a "$INFILE" -ot "$OUTFILE" ] \ || clang++ -fsanitize=address \ @compile_flags.txt \ -o "$OUTFILE" "$INFILE" && \ exec "$OUTFILE"
이에 대응하는 compile_flags.txt(또한 clangd에도 유용함),
-std=c++23
-stdlib=libc++
-fmodule-file=std=std.pcm
-Wall
-Wextra
-Wno-unused-const-variable
-DNJUDGE
그리고 각 C++ 파일의 “헤더”는 다음과 같습니다.
cpp#ifdef NJUDGE import std; #else #include <bits/stdc++.h> #endif using namespace std;
Copyright © 2026 Fares A. Bakhit. All Rights Reserved.
제공된 모든 코드 스니펫은 MIT License에 따라 라이선스가 부여됩니다.