본문으로 건너뛰기
Previous
Next
C++20 Modules | '#include 지옥' 탈출, import로 컴파일 속도 높이기

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

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

이 글의 핵심

큰 헤더 하나를 include하면 그 헤더가 또 수십 개를 include하고, 같은 내용이 수많은.cpp에서 반복 파싱됩니다. 모듈은 "한 번만 파싱하고 결과를 재사용"하는 단위라서, 컴파일 시간을 줄이는 데 도움이… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다.

들어가며: “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 모두 가능.


실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

1. 모듈이란

모듈(module)은 C++20에서 도입된 “한 번만 파싱한 뒤 결과를 재사용하는” 단위입니다. 비유하면 헤더(#include)는 매번 파일을 읽어서 복사하는 방식이라면, 모듈은 한 번만 읽고 캐시해 두었다가 재사용하는 방식이라 컴파일 시간을 줄이는 데 도움이 됩니다.

헤더 vs 모듈 의존 처리 흐름을 한눈에 보면 아래와 같습니다.

다음은 mermaid 예제 코드입니다.

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하고, 그 헤더들이 또 다른 헤더를 끌어옵니다.

다음은 mermaid 예제 코드입니다.

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 구현이 새어 나가는 문제를 줄일 수 있습니다.

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를 사용할 수 있습니다.

다음은 mermaid 예제 코드입니다.

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
)

파티션 사용 시:

다음은 cmake 예제 코드입니다.

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% 감소

실제 수치는 프로젝트 구조, 헤더 크기, 컴파일러에 따라 다릅니다.

왜 모듈이 빠른가?

다음은 mermaid 예제 코드입니다.

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: 빌드 스크립트에서 모듈 의존성 관리

다음은 cmake 예제 코드입니다.

# 모듈 의존성이 복잡할 때
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로 논블로킹 코드 작성하기


관련 글

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

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++20 Modules | ‘#include 지옥’ 탈출, import로 컴파일 속도 높이기」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++20 Modules | ‘#include 지옥’ 탈출, import로 컴파일 속도 높이기」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.