C++20 Modules | "#include 지옥" 탈출, import로 컴파일 속도 높이기

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 모두 가능.


목차

  1. 모듈이란
  2. 문제 시나리오: include 지옥
  3. 모듈 선언과 export
  4. 완전한 모듈 예제
  5. import 사용
  6. 모듈 파티션
  7. 빌드와 도구
  8. 자주 발생하는 오류
  9. 모범 사례
  10. 성능 비교
  11. 프로덕션 패턴

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, piimport 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 모듈이 exportadd, 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 순서

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

// ❌ 권장하지 않음
#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되는 모듈에 영향을 줄 수 있음.

// ❌ 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 알고리즘 쓰기