본문으로 건너뛰기 C++20 Modules 완벽 마스터 | import·export·컴파일러 지원·빌드 시간 벤치마크

C++20 Modules 완벽 마스터 | import·export·컴파일러 지원·빌드 시간 벤치마크

C++20 Modules 완벽 마스터 | import·export·컴파일러 지원·빌드 시간 벤치마크

들어가며: 헤더의 컴파일 비용

헤더의 비용

큰 헤더 하나를 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 모두 가능.


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 등)를 재사용합니다.

추가 문제 시나리오

템플릿 헤더 의존 폭발

<algorithm>, <memory>, <optional> 등 템플릿이 많은 헤더를 100개 이상의 .cpp에서 include하면, 각 .cpp마다 템플릿 인스턴스화가 발생합니다. std::vector<int>만 해도 수십 개 .cpp에서 중복 인스턴스화됩니다. 모듈은 인스턴스화 결과를 모듈 단위로 공유하므로 중복이 크게 줄어듭니다.

헤더 수정 시 전체 재빌드

common_utils.h 한 줄만 바꿔도 이 헤더를 include하는 모든 .cpp가 재컴파일됩니다. 200개 파일이 의존하면 200개 전부 재빌드됩니다. 모듈은 해당 모듈을 import하는 파일만 재컴파일되므로 증분 빌드가 효율적입니다.

매크로·전처리기 오염

#define min(a,b) ((a)<(b)?(a):(b)) 같은 매크로가 헤더에 있으면, 그 헤더를 include한 모든 파일에서 min이 재정의됩니다. std::min과 충돌하거나 예상치 못한 치환이 발생할 수 있습니다. 모듈은 매크로가 모듈 내부로 새어 나가지 않아 캡슐화가 강화됩니다.

ABI 불안정

헤더에 private 멤버를 추가하면, 그 헤더를 쓰는 모든 바이너리가 재컴파일되어야 합니다. 모듈 + PIMPL 패턴을 쓰면 구현 디테일을 모듈 내부에 숨겨 ABI 변경 영향을 최소화할 수 있습니다.


3. 모듈 선언과 export

export module / export

export module math;로 이 파일이 math 모듈의 인터페이스임을 선언합니다. export가 붙은 add, piimport math; 하는 쪽에서 사용할 수 있으며, export 없이 정의한 internalHelper는 이 모듈 내부에서만 보입니다. 따라서 “이 모듈이 제공하는 API”만 명시적으로 노출할 수 있어, 헤더에서 private 구현이 새어 나가는 문제를 줄일 수 있습니다.

internalHelper 함수의 구현 예제입니다.

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를 각 줄에 붙이지 않고 블록으로 묶어 두면 관리하기 편합니다.

foo 함수의 구현 예제입니다.

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 — 인터페이스와 구현을 한 파일에:

square 함수의 구현 예제입니다.

// 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 — 메인 인터페이스:

C/C++ 예제 코드입니다.

// network.cppm
export module network;

export import network:tcp;
export import network:udp;
export import network:http;

network_tcp.cppm — TCP 파티션:

connect 함수의 구현 예제입니다.

// 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 파티션:

bind 함수의 구현 예제입니다.

// 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 모듈이 exportadd, pi 등을 사용할 수 있습니다. #include와 달리 import는 “이미 파싱된 모듈 결과”를 가져오므로, math가 크더라도 main.cpp 쪽 컴파일은 상대적으로 빠릅니다. 컴파일러가 모듈을 빌드할 때 .pcm(또는 도구별 중간 파일)을 만들어 두며, 그 결과를 재사용합니다.

main 함수의 구현 예제입니다.

// 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:

doSomething 함수의 구현 예제입니다.

// part1.cppm
module mylib:part1;

import mylib:part2;  // 다른 파티션 import 가능

export class Part1 {
public:
    void doSomething();
};

part2.cppm — 파티션 2:

C/C++ 예제 코드입니다.

// part2.cppm
module mylib:part2;

export class Part2 {
public:
    int value = 0;
};

part1_impl.cpp — Part1 구현:

C/C++ 예제 코드입니다.

// 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하지 않음 → 외부에 노출 안 됨

C/C++ 예제 코드입니다.

// public_api.cppm
module mylib:public_api;

import mylib:internal;  // 내부에서만 사용

export void publicFunction() {
    internalHelper();  // internal 파티션 함수
}

internalHelper 함수의 구현 예제입니다.

// 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를 쓸 수 없음.

C/C++ 예제 코드입니다.

// ❌ 잘못된 예
module;
export int foo() { return 0; }  // 오류!
#include <vector>
export module mylib;

C/C++ 예제 코드입니다.

// ✅ 올바른 예
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 순서

원인: 일부 컴파일러에서 #includeimport보다 먼저 오면 매크로가 모듈에 영향을 줄 수 있음.

C/C++ 예제 코드입니다.

// ❌ 권장하지 않음
#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: 매크로가 모듈에 유입

원인: #includeimport보다 먼저 쓰면, 해당 헤더의 매크로가 이후 import되는 모듈에 영향을 줄 수 있음.

C/C++ 예제 코드입니다.

// ❌ 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하지 않습니다.

C/C++ 예제 코드입니다.

// ✅ 좋은 예: 공개 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줄 이상의 모듈은 파티션으로 나누어 유지보수성을 높입니다.

C/C++ 예제 코드입니다.

// mylib.cppm - 메인은 re-export만
export module mylib;
export import mylib:core;
export import mylib:io;
export import mylib:algorithm;

전역 모듈 fragment 최소화

원칙: module; 블록에는 반드시 필요한 #include만 넣습니다. 가능하면 모듈 내부에서 import로 대체합니다.

C/C++ 예제 코드입니다.

// ✅ 필요한 경우만
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: 점진적 마이그레이션

기존 헤더를 한 번에 모듈로 바꾸지 말으며, 새 코드부터 모듈로 작성합니다.

process 함수의 구현 예제입니다.

// 기존: legacy_code.cpp
#include "old_header.h"
import new_module;  // 새 기능은 모듈로

void process() {
    oldFunction();   // 헤더 기반
    newFunction();   // 모듈 기반
}

패턴 2: 모듈 + PIMPL

ABI 안정성을 위해 모듈 내부에서 PIMPL을 사용합니다.

draw 함수의 구현 예제입니다.

// 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"; 형태로 지원합니다.

use 함수의 구현 예제입니다.

// MSVC: header.h를 헤더 유닛으로 빌드 후
import "legacy_utils.h";

void use() {
    legacyFunction();  // 헤더 기반이지만 import로 사용
}

패턴 7: 모듈 인터페이스와 구현 분리 (대형 라이브러리)

인터페이스(.cppm)와 구현(.cpp)을 분리하면, 구현 변경 시 인터페이스를 import하는 쪽 재컴파일을 최소화할 수 있습니다.

start 함수의 구현 예제입니다.

// 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로 테스트 픽스처를 노출합니다.

C/C++ 예제 코드입니다.

// 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++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로 논블로킹 코드 작성하기


관련 글