C++20 Modules | "#include 지옥" 탈출, import로 컴파일 속도 높이기
이 글의 핵심
C++20 Modules에 대해 정리한 개발 블로그 글입니다. 큰 헤더 하나를 include하면 그 헤더가 또 수십 개를 include하고, 같은 내용이 수많은 .cpp에서 반복 파싱됩니다. 모듈은 "한 번만 파싱하고 결과를 재사용"하는 단위라서, 컴파일 시간을 줄이는 데 도움이… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다. 관련 키워드: C++, C…
들어가며: “include만 해도 컴파일이 너무 오래 걸려요”
헤더의 비용
큰 헤더 하나를 include하면 그 헤더가 또 수십 개를 include하고, 같은 내용이 수많은 .cpp에서 반복 파싱됩니다. 모듈은 “한 번만 파싱하고 결과를 재사용”하는 단위라서, 컴파일 시간을 줄이는 데 도움이 됩니다.
목표:
- module 선언과 export 로 공개 인터페이스 정의
- import 로 다른 모듈 가져오기
- 모듈 사용 시 빌드 설정 (컴파일러 옵션)
export로 “이 모듈이 노출하는 것”만 정해 두면 include처럼 구현 디테일이 새어 나가지 않고, 의존 관계도 import만 보면 파악할 수 있습니다. MSVC·Clang·GCC가 C++20 모듈을 지원하므로, 새로 추가하는 라이브러리부터 모듈로 작성해 보는 것이 실무에서 점진적으로 도입하는 한 방법입니다.
이 글을 읽으면:
- 모듈 파일(.cppm / .ixx)을 작성할 수 있습니다.
- import로 의존성을 명시하고 컴파일 비용을 줄일 수 있습니다.
- 기존 헤더와의 혼용 방식을 알 수 있습니다.
요구 환경: C++20 지원 컴파일러(GCC 11+, Clang 13+, MSVC 2019 16.10+). CMake 3.28+에서 모듈 빌드 지원이 안정적. Linux/macOS/Windows 모두 가능.
목차
- 모듈이란
- 문제 시나리오: include 지옥
- 모듈 선언과 export
- 완전한 모듈 예제
- import 사용
- 모듈 파티션
- 빌드와 도구
- 자주 발생하는 오류
- 모범 사례
- 성능 비교
- 프로덕션 패턴
1. 모듈이란
모듈(module)은 C++20에서 도입된 “한 번만 파싱한 뒤 결과를 재사용하는” 단위입니다. 비유하면 헤더(#include)는 매번 파일을 읽어서 복사하는 방식이라면, 모듈은 한 번만 읽고 캐시해 두었다가 재사용하는 방식이라 컴파일 시간을 줄이는 데 도움이 됩니다.
헤더 vs 모듈 의존 처리 흐름을 한눈에 보면 아래와 같습니다.
flowchart LR
subgraph header["#include 헤더"]
H1[.cpp 1] --> H2[매번 파싱]
H3[.cpp 2] --> H2
H2 --> H4[중복 파싱]
end
subgraph module["import 모듈"]
M1[.cpp 1] --> M2[한 번 파싱]
M3[.cpp 2] --> M2
M2 --> M4[캐시 재사용]
end
헤더 vs 모듈
| 항목 | 헤더 (#include) | 모듈 (import) |
|---|---|---|
| 단위 | 텍스트 복붙 | 파싱 결과 재사용 |
| 중복 | 매번 파싱 | 한 번만 파싱 후 재사용 |
| 순환 | 전방 선언 등으로 제어 | 모듈 단위로 의존성 명확 |
기본 형태
module; 다음의 전역 모듈 fragment에서는 기존 헤더(#include <vector> 등)만 사용할 수 있고, export module mylib;로 이 파일이 mylib 모듈임을 선언합니다. export가 붙은 add, Widget만 이 모듈을 import하는 쪽에서 쓸 수 있고, export 없이 정의한 것(예: internalHelper)은 모듈 내부에서만 사용됩니다. 따라서 #include처럼 “헤더 전체를 복붙”하는 것이 아니라 “공개한 선언만” 노출되어, 컴파일러는 모듈을 한 번 파싱한 결과를 재사용하므로 대규모 프로젝트에서 컴파일 시간이 줄어드는 경험이 있습니다. 확장자는 컴파일러마다 .cppm(Clang/GCC) 또는 .ixx(MSVC)를 쓰는 경우가 많습니다.
// mylib.cppm (또는 .ixx)
module;
// 전역 모듈 fragment: 여기서만 #include
#include <vector>
export module mylib;
export int add(int a, int b) {
return a + b;
}
export class Widget {
public:
void draw();
};
2. 문제 시나리오: include 지옥
실제로 겪는 상황
시나리오: 50개의 .cpp 파일이 있는 프로젝트에서, 각 파일이 #include <vector>, #include <string>, #include "common_utils.h"를 사용한다고 가정합니다. common_utils.h는 다시 10개의 다른 헤더를 include하고, 그 헤더들이 또 다른 헤더를 끌어옵니다.
flowchart TD
subgraph cpp["50개 .cpp 파일"]
C1[main.cpp]
C2[parser.cpp]
C3[renderer.cpp]
C50[...]
end
subgraph headers["공통 헤더"]
H1[common_utils.h]
H2[vector]
H3[string]
H4[algorithm]
end
C1 --> H1
C2 --> H1
C3 --> H1
C50 --> H1
H1 --> H2
H1 --> H3
H1 --> H4
결과: 컴파일러는 동일한 헤더 내용을 50번 이상 파싱합니다. <vector>만 해도 수천 줄인데, 이를 50번 반복하면 컴파일 시간이 기하급수적으로 늘어납니다.
include 지옥의 구체적 비용
| 항목 | 헤더 방식 | 영향 |
|---|---|---|
<vector> 파싱 | 50개 .cpp × 1회 = 50회 | 매번 전체 파싱 |
common_utils.h 파싱 | 50회 | 의존 헤더 10개도 50회씩 |
| 순환 의존 | 전방 선언·가드 필요 | 실수 시 빌드 실패 |
| 구현 노출 | private 멤버도 헤더에 | ABI 취약, 재컴파일 유발 |
모듈으로 해결
모듈을 사용하면 import std.vector;(C++23) 또는 import mylib; 한 번으로 이미 파싱된 결과를 가져옵니다. 50개 .cpp가 동일 모듈을 import해도 모듈은 한 번만 파싱되고, 나머지는 캐시(.pcm 등)를 재사용합니다.
추가 문제 시나리오
시나리오 2: 템플릿 헤더 의존 폭발
<algorithm>, <memory>, <optional> 등 템플릿이 많은 헤더를 100개 이상의 .cpp에서 include하면, 각 .cpp마다 템플릿 인스턴스화가 발생합니다. std::vector<int>만 해도 수십 개 .cpp에서 중복 인스턴스화됩니다. 모듈은 인스턴스화 결과를 모듈 단위로 공유하므로 중복이 크게 줄어듭니다.
시나리오 3: 헤더 수정 시 전체 재빌드
common_utils.h 한 줄만 바꿔도 이 헤더를 include하는 모든 .cpp가 재컴파일됩니다. 200개 파일이 의존하면 200개 전부 재빌드됩니다. 모듈은 해당 모듈을 import하는 파일만 재컴파일되므로 증분 빌드가 효율적입니다.
시나리오 4: 매크로·전처리기 오염
#define min(a,b) ((a)<(b)?(a):(b)) 같은 매크로가 헤더에 있으면, 그 헤더를 include한 모든 파일에서 min이 재정의됩니다. std::min과 충돌하거나 예상치 못한 치환이 발생할 수 있습니다. 모듈은 매크로가 모듈 내부로 새어 나가지 않아 캡슐화가 강화됩니다.
시나리오 5: ABI 불안정
헤더에 private 멤버를 추가하면, 그 헤더를 쓰는 모든 바이너리가 재컴파일되어야 합니다. 모듈 + PIMPL 패턴을 쓰면 구현 디테일을 모듈 내부에 숨겨 ABI 변경 영향을 최소화할 수 있습니다.
3. 모듈 선언과 export
export module / export
export module math;로 이 파일이 math 모듈의 인터페이스임을 선언합니다. export가 붙은 add, pi만 import math; 하는 쪽에서 사용할 수 있고, export 없이 정의한 internalHelper는 이 모듈 내부에서만 보입니다. 따라서 “이 모듈이 제공하는 API”만 명시적으로 노출할 수 있어, 헤더에서 private 구현이 새어 나가는 문제를 줄일 수 있습니다.
export module math;
export int add(int a, int b) {
return a + b;
}
export double pi = 3.14159;
// export 없으면 이 모듈 내부에서만 사용
static int internalHelper() {
return 0;
}
주의사항: 모듈 경계에서는 헤더 때보다 export 목록 관리가 API 계약이 됩니다. 외부에 노출할 최소 집합만 export하는 습관을 들이세요.
export block
export { … } 블록 안에 여러 선언을 넣으면 한꺼번에 export할 수 있습니다. foo, Bar 모두 import utils; 하는 코드에서 사용 가능합니다. 선언이 많을 때 export를 각 줄에 붙이지 않고 블록으로 묶어 두면 관리하기 편합니다.
export module utils;
export {
void foo();
class Bar {};
}
템플릿 export
모듈에서 템플릿도 export할 수 있습니다. 헤더와 달리 템플릿 정의가 모듈 내부에만 있어도, import하는 쪽에서 인스턴스화할 수 있습니다.
// container.cppm
module;
#include <utility>
export module container;
export template<typename T>
class Box {
T value;
public:
explicit Box(T v) : value(std::move(v)) {}
const T& get() const { return value; }
};
주의사항: 템플릿 정의는 여전히 “한 번에 보이는” 문맥이 필요합니다. .cpp 구현 분리 시에는 인터페이스/구현 유닛 규칙을 컴파일러 문서에 맞추세요.
4. 완전한 모듈 예제
예제 1: 수학 유틸리티 모듈 (단일 파일)
math.cppm — 인터페이스와 구현을 한 파일에:
// math.cppm
export module math;
export int add(int a, int b) {
return a + b;
}
export int multiply(int a, int b) {
return a * b;
}
export constexpr double PI = 3.141592653589793;
// 내부 전용: export 없음
namespace detail {
int square(int x) { return x * x; }
}
main.cpp — 모듈 사용:
// main.cpp
import math;
#include <iostream>
int main() {
std::cout << add(3, 5) << "\n"; // 8
std::cout << multiply(4, 7) << "\n"; // 28
std::cout << PI << "\n"; // 3.14159...
return 0;
}
예제 2: 인터페이스와 구현 분리
geometry.cppm — 선언만 export:
// geometry.cppm
export module geometry;
export struct Point {
double x, y;
};
export double distance(const Point& a, const Point& b);
export Point midpoint(const Point& a, const Point& b);
geometry_impl.cpp — 구현:
// geometry_impl.cpp
module geometry;
#include <cmath>
double distance(const Point& a, const Point& b) {
double dx = a.x - b.x;
double dy = a.y - b.y;
return std::sqrt(dx * dx + dy * dy);
}
Point midpoint(const Point& a, const Point& b) {
return Point{(a.x + b.x) / 2, (a.y + b.y) / 2};
}
main.cpp:
// main.cpp
import geometry;
int main() {
Point p1{0, 0}, p2{3, 4};
double d = distance(p1, p2); // 5.0
Point m = midpoint(p1, p2); // {1.5, 2}
return 0;
}
예제 3: 전역 모듈 fragment로 기존 헤더 사용
// string_utils.cppm
module;
#include <string>
#include <algorithm>
#include <cctype>
export module string_utils;
export std::string to_upper(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
{ return std::toupper(c); });
return s;
}
export std::string trim(const std::string& s) {
auto start = s.find_first_not_of(" \t\n\r");
if (start == std::string::npos) return "";
auto end = s.find_last_not_of(" \t\n\r");
return s.substr(start, end - start + 1);
}
예제 4: 파티션을 활용한 대형 모듈 (완전한 예제)
network 모듈을 tcp, udp, http 파티션으로 나누어 관리하는 예제입니다.
network.cppm — 메인 인터페이스:
// network.cppm
export module network;
export import network:tcp;
export import network:udp;
export import network:http;
network_tcp.cppm — TCP 파티션:
// network_tcp.cppm
module network:tcp;
export class TcpSocket {
public:
void connect(const char* host, int port);
void send(const void* data, size_t len);
size_t receive(void* buf, size_t len);
};
network_udp.cppm — UDP 파티션:
// network_udp.cppm
module network:udp;
export class UdpSocket {
public:
void bind(int port);
void sendTo(const void* data, size_t len, const char* addr, int port);
};
network_http.cppm — HTTP 파티션 (tcp 파티션 사용):
// network_http.cppm
module;
#include <string>
module network:http;
import network:tcp;
export class HttpClient {
public:
std::string get(const char* url);
};
main.cpp — 사용 예:
// main.cpp
import network;
int main() {
TcpSocket tcp;
tcp.connect("localhost", 8080);
UdpSocket udp;
udp.bind(9000);
HttpClient http;
auto response = http.get("https://example.com");
return 0;
}
5. import 사용
다른 모듈 가져오기
import math; 한 번으로 math 모듈이 export한 add, pi 등을 사용할 수 있습니다. #include와 달리 import는 “이미 파싱된 모듈 결과”를 가져오므로, math가 크더라도 main.cpp 쪽 컴파일은 상대적으로 빠릅니다. 컴파일러가 모듈을 빌드할 때 .pcm(또는 도구별 중간 파일)을 만들어 두고, 그 결과를 재사용합니다.
// main.cpp
import math;
int main() {
int x = add(3, 5);
return 0;
}
여러 모듈
// main.cpp
import math;
import utils;
import geometry;
// C++23 표준 라이브러리 모듈 (지원 시)
// import std;
import와 include 혼용
- 같은 파일에서 import 한 모듈과 #include 한 헤더를 함께 쓸 수 있음.
- 가능하면 모듈을 먼저 import하고, 그 다음 include하는 것이 권장됨.
// main.cpp
import math;
import geometry;
#include <iostream>
#include <vector>
int main() {
std::cout << add(1, 2) << "\n";
return 0;
}
6. 모듈 파티션
파티션이란?
한 모듈을 여러 파일로 나누되 “하나의 모듈”로 노출하고 싶을 때 파티션을 씁니다. 사용자는 import mylib; 한 번으로 모든 파티션의 export를 사용할 수 있습니다.
flowchart TB
subgraph mylib["mylib 모듈"]
M1[mylib.cppm - 메인]
P1[part1.cppm - 파티션 1]
P2[part2.cppm - 파티션 2]
P3[part3.cppm - 파티션 3]
end
M1 --> P1
M1 --> P2
M1 --> P3
User[main.cpp] -->|import mylib| M1
기본 파티션 구조
mylib.cppm — 메인 인터페이스, 파티션 re-export:
// mylib.cppm
export module mylib;
export import mylib:part1;
export import mylib:part2;
part1.cppm — 파티션 1:
// part1.cppm
module mylib:part1;
import mylib:part2; // 다른 파티션 import 가능
export class Part1 {
public:
void doSomething();
};
part2.cppm — 파티션 2:
// part2.cppm
module mylib:part2;
export class Part2 {
public:
int value = 0;
};
part1_impl.cpp — Part1 구현:
// part1_impl.cpp
module mylib:part1;
void Part1::doSomething() {
// 구현
}
파티션 규칙
| 규칙 | 설명 |
|---|---|
| 파티션 이름 | module 모듈이름:파티션이름; |
| 내부 전용 파티션 | export 없이 import mylib:internal; |
| 순환 | 파티션 간 순환 import 가능 (모듈 간은 제한적) |
| 사용자 노출 | export import mylib:part1;로 re-export |
내부 전용 파티션 (구현 디테일 숨기기)
// mylib.cppm
export module mylib;
export import mylib:public_api;
// internal 파티션은 export하지 않음 → 외부에 노출 안 됨
// public_api.cppm
module mylib:public_api;
import mylib:internal; // 내부에서만 사용
export void publicFunction() {
internalHelper(); // internal 파티션 함수
}
// internal.cppm
module mylib:internal;
void internalHelper() {
// 구현 디테일
}
7. 빌드와 도구
GCC 11+
# 단일 모듈
g++ -std=c++20 -fmodules-ts -c math.cppm -o math.o
g++ -std=c++20 -fmodules-ts main.cpp math.o -o app
# 파티션 있는 모듈 (의존 순서 중요)
g++ -std=c++20 -fmodules-ts -c mylib:part2 -o part2.o
g++ -std=c++20 -fmodules-ts -c mylib:part1 -o part1.o
g++ -std=c++20 -fmodules-ts -c mylib -o mylib.o
g++ -std=c++20 -fmodules-ts main.cpp part2.o part1.o mylib.o -o app
Clang 13+
# Clang은 -fmodules-ts 대신 -std=c++20만으로 모듈 지원
clang++ -std=c++20 -c math.cppm -o math.o
clang++ -std=c++20 main.cpp math.o -o app
MSVC
.ixx파일을 모듈 인터페이스로 컴파일.- Visual Studio 프로젝트에서 “C++ 모듈”로 설정 후 빌드.
- 명령줄:
cl /std:c++20 /interface math.ixx
CMake 3.28+
cmake_minimum_required(VERSION 3.28)
project(MyApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
add_executable(app main.cpp)
target_sources(app PRIVATE FILE_SET all MODULES
math.cppm
geometry.cppm
geometry_impl.cpp
)
파티션 사용 시:
add_executable(app main.cpp)
target_sources(app PRIVATE FILE_SET all MODULES
mylib/part1.cppm
mylib/part2.cppm
mylib/mylib.cppm
mylib/part1_impl.cpp
)
8. 자주 발생하는 오류
오류 1: “module not found” / “undefined reference”
원인: 모듈 인터페이스(.cppm)가 컴파일되지 않았거나, 링크 순서가 잘못됨.
해결:
# ❌ 잘못된 순서: main을 먼저 컴파일
g++ -std=c++20 main.cpp math.cppm -o app # 실패 가능
# ✅ 올바른 순서: 모듈 먼저, 그 다음 main
g++ -std=c++20 -fmodules-ts -c math.cppm -o math.o
g++ -std=c++20 -fmodules-ts main.cpp math.o -o app
오류 2: 전역 모듈 fragment에서 export 사용
원인: module; 블록 안에서는 export를 쓸 수 없음.
// ❌ 잘못된 예
module;
export int foo() { return 0; } // 오류!
#include <vector>
export module mylib;
// ✅ 올바른 예
module;
#include <vector>
export module mylib;
export int foo() { return 0; }
오류 3: export되지 않은 심볼 사용
원인: export 없이 정의한 함수/클래스를 외부에서 사용 시도.
// math.cppm
export module math;
int internalAdd(int a, int b) { return a + b; } // export 없음
// main.cpp
import math;
int x = internalAdd(1, 2); // ❌ 오류: internalAdd는 export되지 않음
해결: 외부에 노출할 항목에만 export를 붙입니다.
오류 4: 순환 import
원인: 모듈 A가 B를 import하고, B가 A를 import할 때.
// mod_a.cppm
export module mod_a;
import mod_b; // mod_b가 mod_a를 import하면 순환
해결: 파티션으로 분리하거나, 공통 인터페이스를 별도 모듈로 추출합니다.
오류 5: include와 import 순서
원인: 일부 컴파일러에서 #include가 import보다 먼저 오면 매크로가 모듈에 영향을 줄 수 있음.
// ❌ 권장하지 않음
#include <iostream>
import math;
// ✅ 권장: import 먼저
import math;
#include <iostream>
오류 6: GCC/Clang 확장자 인식
원인: .cpp로 모듈 인터페이스를 저장하면 컴파일러가 일반 소스로 처리할 수 있음.
해결: .cppm(Clang/GCC) 또는 .ixx(MSVC) 사용.
오류 7: 파티션 컴파일 순서
원인: 파티션 B가 A를 import할 때, A보다 B를 먼저 컴파일하면 실패.
# ❌ 잘못된 순서
g++ -std=c++20 -fmodules-ts -c mylib.cppm -o mylib.o # part1, part2 의존
g++ -std=c++20 -fmodules-ts -c part1.cppm -o part1.o # part2 필요
# ✅ 올바른 순서: 의존 없는 파티션부터
g++ -std=c++20 -fmodules-ts -c part2.cppm -o part2.o
g++ -std=c++20 -fmodules-ts -c part1.cppm -o part1.o
g++ -std=c++20 -fmodules-ts -c mylib.cppm -o mylib.o
오류 8: private 구현 unit에서 export 사용
원인: module mylib;(구현 unit)에서 export를 사용하면 오류.
// ❌ geometry_impl.cpp - 구현 unit
module geometry;
export double distance(...) { ... } // 오류: 구현 unit에서는 export 불가
// ✅ geometry.cppm - 인터페이스 unit에서만 export
export module geometry;
export double distance(const Point& a, const Point& b);
오류 9: 매크로가 모듈에 유입
원인: #include를 import보다 먼저 쓰면, 해당 헤더의 매크로가 이후 import되는 모듈에 영향을 줄 수 있음.
// ❌ Windows.h의 min/max 매크로가 math 모듈에 영향
#include <windows.h>
import math;
int x = min(1, 2); // std::min이 아닌 매크로 min이 적용될 수 있음
// ✅ import 먼저
import math;
#include <windows.h>
#define NOMINMAX // 또는 windows.h 전에 매크로 비활성화
9. 모범 사례
export 범위 최소화
원칙: 외부에 필요한 API만 export하고, 내부 헬퍼·구현 디테일은 export하지 않습니다.
// ✅ 좋은 예: 공개 API만 export
export module config;
export struct Config {
int timeout;
std::string host;
};
export Config loadConfig(const std::string& path);
// 내부 전용 - export 없음
namespace detail {
std::string parseEnv(const std::string& key);
}
모듈당 단일 책임
원칙: 하나의 모듈은 하나의 관심사만 담당합니다. math, geometry, string_utils처럼 역할이 분명한 이름을 사용합니다.
// ✅ 좋은 예: 역할이 명확
export module math; // 수학 연산
export module geometry; // 기하학 타입·함수
export module logging; // 로깅
// ❌ 나쁜 예: 모듈이 너무 비대
export module utils; // 수학, 문자열, 날짜, JSON... 전부 포함
import 순서 통일
원칙: 표준 라이브러리 → 외부 라이브러리 → 프로젝트 모듈 순으로 정렬하면 의존 관계를 파악하기 쉽습니다.
// ✅ 권장 순서
// import std; // C++23 표준 모듈 (지원 시)
import third_party; // 외부 라이브러리
import myproject.math;
import myproject.geometry;
#include <iostream> // 모듈 없는 헤더는 마지막
파티션으로 대형 모듈 분리
원칙: 500줄 이상의 모듈은 파티션으로 나누어 유지보수성을 높입니다.
// mylib.cppm - 메인은 re-export만
export module mylib;
export import mylib:core;
export import mylib:io;
export import mylib:algorithm;
전역 모듈 fragment 최소화
원칙: module; 블록에는 반드시 필요한 #include만 넣습니다. 가능하면 모듈 내부에서 import로 대체합니다.
// ✅ 필요한 경우만
module;
#include <windows.h> // 매크로·플랫폼 API
#include <legacy_header.h> // 모듈화되지 않은 레거시
export module mylib;
import std; // C++23 std 모듈이 있으면 include 대신 사용
10. 성능 비교
컴파일 시간 벤치마크 (개념)
| 시나리오 | 헤더 방식 | 모듈 방식 | 개선 |
|---|---|---|---|
| 10개 .cpp, 공통 유틸 1개 | ~5초 | ~2초 | 약 60% 감소 |
| 50개 .cpp, 대형 헤더 5개 | ~45초 | ~12초 | 약 73% 감소 |
| 200개 .cpp, 템플릿 헤더 | ~5분 | ~1분 20초 | 약 73% 감소 |
실제 수치는 프로젝트 구조, 헤더 크기, 컴파일러에 따라 다릅니다.
왜 모듈이 빠른가?
flowchart LR
subgraph header["헤더"]
A1[파일1] --> P[파싱]
A2[파일2] --> P
A3[파일3] --> P
P --> R1[50회 반복]
end
subgraph mod["모듈"]
B1[모듈] --> Q[1회 파싱]
Q --> C[캐시]
C --> D1[재사용]
C --> D2[재사용]
C --> D3[재사용]
end
| 단계 | 헤더 | 모듈 |
|---|---|---|
| 파싱 | 매 .cpp마다 전체 파싱 | 1회 파싱 후 .pcm 저장 |
| 의존성 | 매번 재계산 | 캐시에서 로드 |
| 템플릿 인스턴스화 | 사용하는 .cpp마다 | 모듈 단위로 1회 |
증분 빌드
- 헤더 수정: 해당 헤더를 include하는 모든 .cpp 재컴파일
- 모듈 수정: 해당 모듈을 import하는 .cpp만 재컴파일, .pcm 재생성
11. 프로덕션 패턴
패턴 1: 점진적 마이그레이션
기존 헤더를 한 번에 모듈로 바꾸지 말고, 새 코드부터 모듈로 작성합니다.
// 기존: legacy_code.cpp
#include "old_header.h"
import new_module; // 새 기능은 모듈로
void process() {
oldFunction(); // 헤더 기반
newFunction(); // 모듈 기반
}
패턴 2: 모듈 + PIMPL
ABI 안정성을 위해 모듈 내부에서 PIMPL을 사용합니다.
// widget.cppm
export module widget;
export class Widget {
public:
Widget();
~Widget();
void draw();
private:
struct Impl;
Impl* pimpl;
};
// widget_impl.cpp
module widget;
#include <memory>
struct Widget::Impl {
int state = 0;
};
Widget::Widget() : pimpl(new Impl) {}
Widget::~Widget() { delete pimpl; }
void Widget::draw() { /* ... */ }
패턴 3: 표준 라이브러리 헤더 래핑
C++23 import std;가 아직 지원되지 않는 환경에서는, 자주 쓰는 헤더를 모듈로 래핑합니다.
// std_vector.cppm (프로젝트 내부)
module;
#include <vector>
export module std_vector;
export template<typename T>
using vector = std::vector<T>;
// 필요한 std::vector 멤버만 export
패턴 4: 빌드 스크립트에서 모듈 의존성 관리
# 모듈 의존성이 복잡할 때
add_library(mylib MODULE
mylib/part1.cppm
mylib/part2.cppm
mylib/mylib.cppm
)
target_compile_features(mylib PUBLIC cxx_std_20)
패턴 5: CI에서 .pcm 캐시
# GitHub Actions 예시
- name: Cache module artifacts
uses: actions/cache@v4
with:
path: build/modules
key: modules-${{ hashFiles('**/*.cppm') }}
패턴 6: 헤더 유닛 (Header Units)으로 점진적 전환
기존 헤더를 한 번에 모듈로 바꾸기 어려울 때, 헤더 유닛으로 컴파일해 import할 수 있습니다. MSVC에서 import "header.h"; 형태로 지원합니다.
// MSVC: header.h를 헤더 유닛으로 빌드 후
import "legacy_utils.h";
void use() {
legacyFunction(); // 헤더 기반이지만 import로 사용
}
패턴 7: 모듈 인터페이스와 구현 분리 (대형 라이브러리)
인터페이스(.cppm)와 구현(.cpp)을 분리하면, 구현 변경 시 인터페이스를 import하는 쪽 재컴파일을 최소화할 수 있습니다.
// api.cppm - 선언만, 자주 변경되지 않음
export module api;
export class Service {
public:
void start();
void stop();
};
// api_impl.cpp - 구현, 변경이 잦아도 api import 쪽 영향 적음
module api;
void Service::start() { /* 구현 */ }
void Service::stop() { /* 구현 */ }
패턴 8: 테스트용 모듈 구조
테스트에서 내부 구현을 검증해야 할 때, 테스트 전용 파티션을 두거나 export로 테스트 픽스처를 노출합니다.
// mylib.cppm
export module mylib;
export import mylib:public_api;
#ifdef ENABLE_TEST_API
export import mylib:test_support; // 테스트 빌드에서만
#endif
프로덕션 체크리스트
- C++20 이상, GCC 11+ / Clang 13+ / MSVC 16.10+ 사용
- CMake 3.28+ 또는 해당 빌드 시스템에서 모듈 지원 확인
- 새 라이브러리는 모듈로 작성, 기존 코드는 점진적 전환
- export 범위 최소화 (필요한 API만)
- 파티션으로 대형 모듈 분리
- CI에서 모듈 아티팩트(.pcm) 캐시 검토
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 기존 프로젝트를 Module로 전환 | 단계별 마이그레이션 [#24-2]
- C++20 Modules 완벽 가이드 | 헤더 파일을 넘어서
- Visual Studio C++ 빌드 느림 | “10분 걸리던 빌드” PCH·/MP로 2분 만들기
이 글에서 다루는 키워드 (관련 검색어)
C++20 모듈, module import export, 헤더 대안, 컴파일 속도, 모듈 기초, 모듈 파티션, include 지옥 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 내용 |
|---|---|
| 선언 | export module name; |
| 공개 | export 로 함수/클래스/변수 노출 |
| 사용 | import name; |
| 파티션 | module name:partition; 로 분할 |
| 효과 | 컴파일 시간 감소, 의존성 명확화 |
| 도구 | GCC/Clang/MSVC 각각 모듈 옵션 필요 |
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. C++20 모듈의 기본 문법, module/import, export, 그리고 헤더 대신 모듈을 쓸 때의 장점과 빌드 설정을 다룹니다. 새 라이브러리 개발, 대규모 프로젝트 컴파일 시간 단축, 헤더 의존성 정리가 필요할 때 활용합니다. 실무에서는 위 본문의 예제와 프로덕션 패턴을 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. P1103R3 모듈 표준 문서도 도움이 됩니다.
한 줄 요약: module·export·import로 컴파일 속도를 높이고 의존을 명확히 할 수 있습니다. 다음으로 모듈 마이그레이션(#24-2)를 읽어보면 좋습니다.
다음 글: [C++ 실전 가이드 #24-2] 기존 프로젝트를 Module로 전환: 단계별 마이그레이션
이전 글: [C++ 실전 가이드 #23-3] 비동기 작업과 Coroutine: co_await로 논블로킹 코드 작성하기
관련 글
- C++ 기존 프로젝트를 Module로 전환 | 단계별 마이그레이션 [#24-2]
- C++20 Coroutine | co_await·co_yield로
- C++ Generator 완벽 가이드 | co_yield로 lazy 시퀀스·무한 수열·파이프라인 만들기
- C++ 비동기 작업과 Coroutine | co_await로 콜백 지옥 탈출하기 [#23-3]
- C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기