C++ 기존 프로젝트를 Module로 전환 | 단계별 마이그레이션 [#24-2]

C++ 기존 프로젝트를 Module로 전환 | 단계별 마이그레이션 [#24-2]

이 글의 핵심

C++ 기존 프로젝트를 Module로 전환에 대한 실전 가이드입니다. 단계별 마이그레이션 [#24-2] 등을 예제와 함께 설명합니다.

들어가며: “헤더 기반 프로젝트를 모듈로 전환하고 싶어요”

실제 겪는 문제 시나리오

“한 줄만 수정했는데 전체가 다시 컴파일돼요”
헤더 기반 프로젝트에서 utils.h 하나를 수정하면, 그 헤더를 include하는 모든 .cpp 파일이 다시 컴파일됩니다. 프로젝트가 커질수록 빌드 시간이 기하급수적으로 늘어나고, CI/CD 파이프라인에서도 병목이 됩니다.

“include 순서가 바뀌면 링크 에러가 나요”
헤더 의존성이 얽혀 있으면 #include 순서를 바꾸는 것만으로도 ODR 위반이나 재정의 에러가 발생합니다. 순환 include를 피하려면 전방 선언을 난무하게 되고, 코드가 복잡해집니다.

“템플릿 헤더가 너무 커요”
<vector>, <string> 같은 표준 라이브러리를 include하면 수만 줄이 매번 파싱됩니다. 같은 내용이 수십 개의 .cpp에서 반복되므로, 컴파일러 입장에서는 엄청난 낭비입니다.

마이그레이션(기존 코드를 새 방식으로 옮기는 작업)을 점진적으로 진행하면, 한꺼번에 바꿀 필요 없이 한 레이어씩 모듈로 전환할 수 있습니다. 이 글에서는 그 전략과 실전 절차를 단계별로 정리합니다.

이전 글: C++20 모듈 기초 (#24-1)에서 export module, import 문법과 모듈 단위를 다뤘습니다.

목표:

  • 한 라이브러리/한 레이어부터 모듈화
  • export 할 것과 내부용 구분
  • includeimport 공존 시 순서와 주의사항
  • 빌드 시스템 변경 최소화

이 글을 읽으면:

  • 기존 헤더를 모듈 인터페이스로 옮기는 절차를 알 수 있습니다.
  • 하위 호환(헤더 남기기) 전략을 세울 수 있습니다.
  • 실무에서 단계별로 적용할 수 있습니다.

요구 환경: C++20 지원 컴파일러(GCC 11+, Clang 13+, MSVC 2019 16.10+). CMake 3.28+에서 모듈 빌드 지원이 안정적. Linux/macOS/Windows 모두 가능.


목차

  1. 문제 시나리오: 헤더 기반 프로젝트의 한계
  2. 추가 문제 시나리오: 실무에서 겪는 구체적 상황
  3. 단계별 마이그레이션 가이드 (헤더 → 모듈)
  4. 완전한 C++20 모듈 마이그레이션 예제
  5. CMake 전체 설정 예제
  6. Before/After 비교: 빌드 시간
  7. 모듈 인터페이스 유닛 vs 구현 유닛
  8. CMake 모듈 설정
  9. 마이그레이션 시 자주 나는 오류
  10. 모듈 마이그레이션 베스트 프랙티스
  11. 프로덕션 패턴
  12. 성능 비교: 빌드 시간, 바이너리 크기
  13. 프로덕션 마이그레이션 전략

1. 문제 시나리오: 헤더 기반 프로젝트의 한계

”헤더 기반 프로젝트를 모듈로 전환하고 싶어요”

많은 C++ 프로젝트가 여전히 헤더 + 소스 구조로 되어 있습니다. 이 구조의 한계를 정리하면 아래와 같습니다.

flowchart TB
  subgraph header["헤더 기반 빌드"]
    H1[utils.h 수정] --> H2[include 1]
    H1 --> H3[include 2]
    H1 --> H4[include N]
    H2 --> H5[전체 재파싱]
    H3 --> H5
    H4 --> H5
  end
  subgraph module["모듈 기반 빌드"]
    M1[utils 모듈 수정] --> M2[BMI만 재생성]
    M2 --> M3[import하는 쪽만 재컴파일]
  end

헤더의 비용

항목헤더 (#include)모듈 (import)
파싱매번 텍스트 복붙 후 파싱한 번 파싱 후 BMI 재사용
의존성include 순서에 민감import 순서로 명확
재컴파일헤더 수정 시 모든 include 쪽 재빌드모듈 수정 시 import하는 쪽만
캡슐화private 구현이 새어 나갈 수 있음export만 노출

전환 전략 개요

  • 바닥부터: 다른 코드에 의존하지 않는 유틸/기본 타입을 먼저 모듈로 만듦. 그 다음 그걸 쓰는 상위 레이어를 모듈로.
  • 위부터: 새로 추가하는 코드만 모듈로 쓰고, 기존 라이브러리는 include 유지.

한 번에 하나의 “모듈 단위”

  • 한 디렉터리/한 라이브러리를 하나의 모듈(또는 몇 개 파티션)로 묶기.
  • 그 모듈을 쓰는 쪽은 import 로 전환.

2. 추가 문제 시나리오: 실무에서 겪는 구체적 상황

시나리오 A: “common.h 하나가 50개 파일을 잡아당겨요”

common.h (플랫폼 매크로, 기본 타입 정의)
    ├── config.h
    ├── types.h
    └── platform.h
         └── (50개 .cpp가 직접 또는 간접 include)

문제: common.h에 주석 한 줄만 추가해도 50개 TU가 전부 재컴파일됩니다. CI에서 10분 걸리던 빌드가 15분으로 늘어납니다.

모듈 전환 후: common 모듈로 옮기면 BMI만 재생성되고, import하는 50개 TU는 BMI를 참조만 하므로 파싱 비용이 크게 줄어듭니다.

시나리오 B: “순환 include로 전방 선언 지옥”

a.hb.h 순환 의존 시 전방 선언으로 풀어야 하는데, 헤더가 늘어날수록 관리가 어려워집니다. 모듈 전환 후: import a; import b;로 의존 방향이 명확해지고, 모듈 단위로 컴파일 순서가 정해져 순환 문제가 사라집니다.

시나리오 C: “외부 라이브러리 헤더가 너무 무거워요”

Boost.Asio, nlohmann/json 같은 헤더 전용 라이브러리는 include할 때마다 수만 줄을 파싱합니다. 모듈 전환 후: 해당 라이브러리가 모듈을 제공하면 import 한 번으로 BMI를 재사용합니다. (Boost, nlohmann 등은 아직 모듈 지원이 제한적이므로, 자체 라이브러리부터 모듈화하는 것이 현실적입니다.)

시나리오 D: “매크로 오염으로 예상치 못한 동작”

#include는 매크로를 그대로 가져와 전역 네임스페이스를 오염시킵니다. 모듈 전환 후: 모듈은 매크로를 export하지 않습니다. 모듈 경계에서 매크로가 차단되므로 import하는 쪽에는 매크로가 전파되지 않습니다.

시나리오 E: “ODR 위반이 릴리스 빌드에서만 터져요”

헤더의 인라인/템플릿 정의가 여러 TU에 복사되면서, 수정 시 ODR 위반이 발생할 수 있습니다. 모듈 전환 후: 모듈은 단일 정의를 가집니다. BMI에 한 번만 저장되고, import하는 쪽은 그 정의를 참조하므로 ODR 위반 가능성이 줄어듭니다.


3. 단계별 마이그레이션 가이드 (헤더 → 모듈)

Step 0: 환경 확인

# GCC 11+ 또는 Clang 13+
g++ --version
clang++ --version

# CMake 3.28+
cmake --version

Step 1: 의존성이 적은 유틸부터 모듈로 작성

Before (헤더):

// mylib/utils.h
#ifndef MYLIB_UTILS_H
#define MYLIB_UTILS_H

int add(int a, int b);
double clamp(double v, double lo, double hi);

#endif
// mylib/utils.cpp
#include "utils.h"

int add(int a, int b) {
    return a + b;
}

double clamp(double v, double lo, double hi) {
    if (v < lo) return lo;
    if (v > hi) return hi;
    return v;
}

After (모듈):

// mylib/utils.cppm (모듈 인터페이스)
export module mylib.utils;

export int add(int a, int b);
export double clamp(double v, double lo, double hi);
// mylib/utils_impl.cpp (구현 유닛)
module mylib.utils;

int add(int a, int b) {
    return a + b;
}

double clamp(double v, double lo, double hi) {
    if (v < lo) return lo;
    if (v > hi) return hi;
    return v;
}

Step 2: 사용하는 쪽을 import로 전환

Before:

// app/main.cpp
#include "mylib/utils.h"

int main() {
    int x = add(3, 5);
    return 0;
}

After:

// app/main.cpp
import mylib.utils;

int main() {
    int x = add(3, 5);
    return 0;
}

Step 3: 여러 헤더를 하나의 모듈로

a.h, b.h를 각각 별도 모듈로 두지 않고, mylib 하나로 묶을 때는 파티션을 씁니다.

// mylib.cppm
export module mylib;
export import mylib:internal_a;
export import mylib:internal_b;
// part_a.cppm
module mylib:internal_a;
export class PartA {};
// part_b.cppm
module mylib:internal_b;
export class PartB {};

기존에 #include "mylib/a.h", #include "mylib/b.h" 하던 것을 import mylib; 한 번으로 바꿀 수 있어, 의존 관계가 단순해집니다.

Step 4: export 범위 정하기

  • 기존에 “public 헤더”로 제공하던 것만 export.
  • 내부용 함수/타입은 export 하지 않음 (모듈 내부 및 구현 파일에서만 사용).
// mylib.cppm
export module mylib;

export int public_api();

// export 없음: 내부 전용
int internal_helper() {
    return 42;
}

Step 5: include와 import 공존

  • 같은 파일에서 import 를 먼저 쓰고, 그 다음 #include 하는 것이 권장됨.
  • 모듈을 import한 뒤에는, 그 모듈이 제공하는 선언을 include 없이 쓸 수 있음.
// 예: 권장 순서
import mylib.utils;
#include <iostream>
#include "legacy_config.h"

int main() {
    std::cout << add(1, 2) << "\n";
    return 0;
}

4. 완전한 C++20 모듈 마이그레이션 예제

전체 프로젝트 구조 (헤더 → 모듈)

아래는 게임 엔진 유틸 라이브러리를 헤더에서 모듈로 완전 전환한 예제입니다.

Before (헤더 기반):

game_engine/
├── include/
│   ├── math/
│   │   ├── vector3.h
│   │   └── matrix4.h
│   ├── utils/
│   │   ├── logger.h
│   │   └── config.h
│   └── core/
│       └── types.h
├── src/
│   ├── math/
│   │   ├── vector3.cpp
│   │   └── matrix4.cpp
│   ├── utils/
│   │   ├── logger.cpp
│   │   └── config.cpp
│   └── main.cpp
└── CMakeLists.txt

After (모듈 기반):

game_engine/
├── modules/
│   ├── math/
│   │   ├── vector3.cppm      # export module engine.math.vector3;
│   │   ├── vector3_impl.cpp
│   │   ├── matrix4.cppm
│   │   └── matrix4_impl.cpp
│   ├── utils/
│   │   ├── logger.cppm
│   │   ├── logger_impl.cpp
│   │   ├── config.cppm
│   │   └── config_impl.cpp
│   └── core/
│       └── types.cppm        # 기본 타입만, 구현 유닛 없을 수 있음
├── src/
│   └── main.cpp
└── CMakeLists.txt

math 모듈 완전 예제

vector3.cppm (모듈 인터페이스):

// modules/math/vector3.cppm
export module engine.math.vector3;

export struct Vector3 {
    double x, y, z;
    Vector3(double x = 0, double y = 0, double z = 0);
    Vector3 operator+(const Vector3& other) const;
    double length() const;
};

vector3_impl.cpp (구현 유닛):

구현 유닛에서 std::sqrt를 쓰려면 #include <cmath>가 필요합니다. 구현 유닛에서는 #includemodule 선언 직후에 둡니다.

// modules/math/vector3_impl.cpp
module engine.math.vector3;

#include <cmath>

Vector3::Vector3(double x, double y, double z)
    : x(x), y(y), z(z) {}

Vector3 Vector3::operator+(const Vector3& other) const {
    return Vector3(x + other.x, y + other.y, z + other.z);
}

double Vector3::length() const {
    return std::sqrt(x*x + y*y + z*z);
}

utils 모듈 예제

logger.cppm:

// modules/utils/logger.cppm
export module engine.utils.logger;

export void log_info(const char* msg);
export void log_error(const char* msg);

logger_impl.cpp:

// modules/utils/logger_impl.cpp
module engine.utils.logger;

#include <iostream>

void log_info(const char* msg) {
    std::cout << "[INFO] " << msg << "\n";
}

void log_error(const char* msg) {
    std::cerr << "[ERROR] " << msg << "\n";
}

main.cpp (모듈 사용)

// src/main.cpp
import engine.math.vector3;
import engine.utils.logger;

#include <iostream>

int main() {
    Vector3 v(1, 2, 3);
    Vector3 w = v + Vector3(0, 1, 0);
    log_info("Vector length");
    std::cout << "length = " << w.length() << "\n";
    return 0;
}

5. CMake 전체 설정 예제

단일 모듈 + 앱 (최소 예제)

# CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(GameEngine LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)

# math 모듈
add_library(engine.math STATIC
    modules/math/vector3.cppm
    modules/math/vector3_impl.cpp
)
target_sources(engine.math PUBLIC
    FILE_SET CXX_MODULES FILES modules/math/vector3.cppm
)

# utils 모듈
add_library(engine.utils STATIC
    modules/utils/logger.cppm
    modules/utils/logger_impl.cpp
)
target_sources(engine.utils PUBLIC
    FILE_SET CXX_MODULES FILES modules/utils/logger.cppm
)

# 실행 파일
add_executable(game_engine src/main.cpp)
target_link_libraries(game_engine PRIVATE engine.math engine.utils)
  • engine.math, engine.utils가 먼저 빌드되어 BMI 생성
  • game_engine는 math, utils를 import하므로 마지막에 빌드

멀티 플랫폼 (MSVC .ixx 확장자)

MSVC는 .ixx를 모듈 인터페이스 확장자로 사용합니다. CMake 3.28+는 .cppm을 MSVC에 전달해도 처리합니다 (MSVC 2022 17.4+). 크로스 플랫폼을 위해 조건부로 처리할 수도 있습니다.

빌드 및 실행

# Linux/macOS
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/game_engine

# Windows (Visual Studio)
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
.\build\Release\game_engine.exe

6. Before/After 비교: 빌드 시간

테스트 프로젝트 구조

헤더 기반 (Before):

project/
├── include/
│   ├── utils.h
│   └── math.h
├── src/
│   ├── utils.cpp
│   ├── math.cpp
│   └── main.cpp
└── CMakeLists.txt

모듈 기반 (After):

project/
├── include/
│   ├── utils.cppm
│   └── math.cppm
├── src/
│   ├── utils_impl.cpp
│   ├── math_impl.cpp
│   └── main.cpp
└── CMakeLists.txt

빌드 시간 비교 (예시)

시나리오헤더 기반모듈 기반개선
클린 빌드 (50 TU)45초42초~7%
증분 빌드 (1개 파일 수정)12초8초~33%
헤더 수정 후35초 (재파싱)10초 (BMI만 재생성)~71%
# 증분 빌드 시나리오 (utils.h 수정)
헤더: utils.h를 include하는 30개 .cpp 재컴파일 → 12초
모듈: utils 모듈 BMI 재생성 + import하는 30개 .cpp 재컴파일 → 8초
      (BMI는 바이너리 형태라 파싱 비용이 훨씬 적음)

왜 증분 빌드에서 개선이 큰가?

  • 헤더: 수정 시 include하는 모든 TU가 전체 헤더를 다시 파싱.
  • 모듈: BMI(바이너리 모듈 인터페이스)만 재생성하고, import하는 쪽은 이미 파싱된 정보를 재사용.

7. 모듈 인터페이스 유닛 vs 구현 유닛

모듈 인터페이스 유닛 (Module Interface Unit)

  • export module 선언이 있는 파일.
  • 이 모듈을 import하는 쪽에 노출되는 선언을 정의.
  • 확장자: .cppm (Clang/GCC), .ixx (MSVC).
// math.cppm (인터페이스 유닛)
export module math;

export int add(int a, int b);
export double pi = 3.14159;

// export 없으면 이 모듈 내부에서만 사용
static int internal_helper() {
    return 0;
}

모듈 구현 유닛 (Module Implementation Unit)

  • module (export 없음) 선언만 있는 파일.
  • 인터페이스에서 export한 선언의 구현을 담당.
  • 인터페이스가 바뀌지 않으면, 구현만 수정해도 import하는 쪽은 재컴파일 불필요.
// math_impl.cpp (구현 유닛)
module math;

int add(int a, int b) {
    return a + b;
}

인터페이스 vs 구현 분리

flowchart LR
  subgraph interface["인터페이스 유닛"]
    I1[math.cppm] --> I2[export 선언]
    I2 --> I3[BMI 생성]
  end
  subgraph impl["구현 유닛"]
    J1[math_impl.cpp] --> J2[module math;]
    J2 --> J3[함수 정의]
  end
  I3 --> J1

파티션 vs 구현 유닛

구분파티션구현 유닛
선언module M:part;module M;
역할모듈을 여러 파일로 나눔export 구현만 담당
re-exportexport import M:part;불가

8. CMake 모듈 설정

기본 설정 (CMake 3.28+)

cmake_minimum_required(VERSION 3.28)
project(MyApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)

# 모듈 스캔 활성화 (C++20+ 사용 시)
# 모듈을 쓰지 않으면 비활성화: set(CMAKE_CXX_SCAN_FOR_MODULES 0)

모듈 제공 라이브러리

add_library(mylib STATIC
    mylib/utils.cppm
    mylib/utils_impl.cpp
)

target_sources(mylib PUBLIC
    FILE_SET CXX_MODULES FILES mylib/utils.cppm
)

모듈을 사용하는 실행 파일

add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mylib)

Ninja / Makefile 생성기

CMake 3.28+에서 C++ 모듈을 지원하는 생성기:

  • Ninja (ninja 1.11+)
  • Ninja Multi-Config
  • Visual Studio 17 2022 (MSVC 14.34+)
  • Visual Studio 18 2026
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build

컴파일러별 지원

컴파일러모듈 스캔import std
GCC 14+지원GCC 15+
Clang 16+지원Clang 18.1.2+
MSVC 14.34+지원MSVC 14.36+

모듈 스캔 비활성화

모듈을 아직 쓰지 않는 프로젝트에서 C++20을 쓸 때, 불필요한 스캔을 끄려면:

set(CMAKE_CXX_SCAN_FOR_MODULES 0)

9. 마이그레이션 시 자주 나는 오류

9.1 매크로 관련 문제

증상: 매크로가 모듈에서 안 보임.

원인: 모듈은 매크로를 export할 수 없음.

해결:

// ❌ 모듈에서 안 됨
export module config;
#define MAX_SIZE 1024

// ✅ 방법 1: constexpr로 대체
export module config;
export constexpr int MAX_SIZE = 1024;

// ✅ 방법 2: import 후 별도 include
import mylib;
#include "config_macros.h"  // 매크로만

9.2 private 헤더 문제

증상: 내부 구현용 헤더를 모듈로 옮기려다 실패.

원인: private 헤더는 보통 매크로, include 순서, 의존성에 의존.

해결:

  • private 헤더는 당분간 #include 유지.
  • 공개 API만 모듈로 옮기고, 내부는 include.
// mylib.cppm
export module mylib;
#include "mylib/internal_detail.h"  // 내부용, export 안 함
export int public_api();

9.3 import 후 선언을 찾을 수 없음

증상: import mylib;add 등이 정의되지 않았다는 에러.

원인: 모듈이 먼저 빌드되지 않음. CMake에서 모듈 인터페이스 타겟 의존성 미지정.

해결:

# 모듈 B를 import하는 A는 B보다 나중에 빌드되도록
add_library(B STATIC b.cppm)
target_sources(B PUBLIC FILE_SET CXX_MODULES FILES b.cppm)

add_executable(A main.cpp)
target_link_libraries(A PRIVATE B)

9.4 include와 import 순서 에러

증상: 전처리기와 모듈 혼합 시 순서 문제.

해결: 같은 파일에서는 import를 먼저, 그 다음 #include.

// ✅ 올바른 순서
import mylib;
#include <vector>
#include "legacy.h"

9.5 BMI/ifc 파일 경로 문제 (MSVC)

증상: 모듈 인터페이스 산출물 경로 미지정.

해결: /module:export 등 컴파일러 옵션과 출력 디렉터리 설정 확인. CMake 3.28+에서 C++ 모듈 지원 정리됨.

9.6 PRIVATE 모듈을 PUBLIC 모듈에서 import

증상: CMake에서 “PRIVATE 모듈을 PUBLIC 모듈에서 import할 수 없음” 에러.

원인: CMake는 모듈 가시성을 강제. PUBLIC 모듈 인터페이스에서 PRIVATE 모듈을 import하면, 그 모듈을 사용하는 쪽도 transitively import해야 하는데, PRIVATE 모듈은 설치되지 않아서 설치된 모듈 사용이 깨짐.

해결: PRIVATE 모듈은 구현 유닛에서만 import.

// mylib.cppm (PUBLIC)
export module mylib;
export int foo();  // internal_helper는 import 안 함

// mylib_impl.cpp (구현 유닛)
module mylib;
import mylib.internal;  // ✅ 구현 유닛에서만
int foo() { return internal_helper(); }

9.7 “redefinition of module” 에러

증상: 같은 모듈을 두 번 정의했다는 링크/컴파일 에러.

원인: export module foo;가 있는 파일이 여러 타겟에 포함되었거나, 같은 모듈 이름을 다른 .cppm에서 사용.

해결: 모듈당 인터페이스 유닛은 하나만. add_library에 .cppm 파일을 한 번만 넣고, FILE_SET CXX_MODULES에도 동일 파일만 지정.

9.8 구현 유닛에서 export 사용

증상: module math; 파일에서 export를 쓰면 에러.

원인: 구현 유닛은 export할 수 없음. export는 인터페이스 유닛 전용.

해결: export할 선언은 모두 .cppm(인터페이스)에 두고, .cpp(구현)에는 정의만.

9.9 표준 라이브러리 import (import std)

증상: import std; 또는 import std.compat; 사용 시 컴파일러가 지원하지 않음.

원인: import std는 C++23 표준이며, GCC 15+, Clang 18.1.2+, MSVC 14.36+에서만 지원.

해결: 구버전 컴파일러에서는 #include <vector> 등 기존 방식을 유지. 모듈 전환 시 표준 라이브러리는 include로 두고, 자체 라이브러리만 모듈화하는 것이 안전합니다.

요약 표

증상원인해결
import 후 선언을 찾을 수 없음모듈이 먼저 빌드되지 않음CMake에서 모듈 인터페이스 타겟 의존성 지정
매크로가 모듈에서 안 보임모듈은 매크로를 export할 수 없음constexpr/인라인 함수로 대체 또는 #include로 매크로만 가져오기
include와 import 순서 에러전처리기와 모듈 혼합 시 순서 문제import를 먼저, 그 다음 #include
BMI/ifc 파일 경로 문제 (MSVC)모듈 인터페이스 산출물 경로 미지정/module:export 등 컴파일러 옵션 확인
Private 헤더 의존성내부 구현이 매크로/헤더에 의존당분간 include 유지, 공개 API만 모듈화
redefinition of module같은 모듈을 중복 정의모듈당 인터페이스 유닛 하나만
구현 유닛에서 export구현 유닛은 export 불가export는 인터페이스 유닛에만

10. 모듈 마이그레이션 베스트 프랙티스

1. 모듈 이름은 네임스페이스처럼 계층적으로

// ✅ 좋은 예: 프로젝트.하위.구성요소
export module engine.math.vector3;
export module engine.utils.logger;

// ❌ 피할 것: 단순 이름 (충돌 가능)
export module vector3;
export module logger;

2. export는 최소한으로

  • 공개 API만 export. 내부 헬퍼, 디버그용 함수는 export하지 않음.
  • export { ... } 블록으로 한꺼번에 export할 수 있음.
// mylib.cppm
export module mylib;

export {
    int public_api();
    class PublicClass {};
}

// export 없음: 모듈 내부 전용
namespace detail {
    void internal_helper() {}
}

3. 구현 유닛에서 표준 라이브러리 include

구현 유닛에서는 module 선언 직후#include를 둡니다.

// impl.cpp
module mylib;

#include <algorithm>
#include <string>

void mylib_function() {
    std::string s = "hello";
    std::sort(s.begin(), s.end());
}

4. 파티션으로 큰 모듈 분할

한 모듈이 너무 커지면 파티션으로 나눕니다. 파티션은 해당 모듈 내부에서만 import 가능하고, 외부에는 메인 모듈을 통해서만 노출됩니다.

// mylib.cppm
export module mylib;
export import mylib:part_a;
export import mylib:part_b;

// part_a.cppm
module mylib:part_a;
export class PartA {};

// part_b.cppm
module mylib:part_b;
export class PartB {};

5. 헤더와 모듈 공존 시 래퍼 헤더

기존 사용자가 #include "mylib/foo.h"를 쓰고 있다면, 래퍼 헤더로 모듈을 import한 뒤 재export할 수 있습니다. (MSVC 등에서 지원, GCC/Clang은 제한적)

6. 의존성 순서: 바닥부터

의존성 그래프에서 리프(다른 모듈에 의존하지 않는 모듈)부터 순서대로 전환합니다. coremath, utilsapp 순서로 진행하면 빌드 순서가 자연스럽게 맞습니다.

7. CI에서 컴파일러 버전 고정

모듈 지원이 컴파일러마다 다르므로, CI에서는 GCC 14+, Clang 18+, MSVC 2022 17.4+ 이상을 명시적으로 지정하는 것이 안전합니다.


11. 프로덕션 패턴

패턴 1: 점진적 전환 (Strangler Fig)

기존 헤더를 한 번에 제거하지 않고, 새 코드만 모듈을 사용합니다. 기존 코드는 include 유지. 두 방식이 같은 라이브러리를 참조할 때는 헤더와 모듈 인터페이스를 동시에 유지하고, export하는 선언과 헤더의 선언이 동일해야 ODR이 유지됩니다.

// 새 코드
import engine.math.vector3;
import engine.utils.logger;

// 기존 코드 (당분간)
#include "engine/math/vector3.h"
#include "engine/utils/logger.h"

패턴 2: 모듈 + PCH 혼용

일부 컴파일러에서는 모듈과 PCH(Precompiled Header)를 함께 쓸 수 있습니다. 자주 쓰는 표준 라이브러리를 PCH로 두고, 자체 모듈은 import로 가져오는 방식입니다.

// pch.cpp
#include <vector>
#include <string>
#include <memory>

// main.cpp
#include "pch.h"
import mylib;

(실제 지원 여부는 컴파일러·버전에 따라 다르므로 문서 확인 필요)

패턴 2: 외부 라이브러리 래퍼 모듈

Boost, nlohmann/json 등 헤더 전용 라이브러리를 래퍼 모듈로 감싸서, 프로젝트 내에서는 import만 쓰게 할 수 있습니다. #include를 여러 TU에서 반복하지 않고, 래퍼 모듈 하나에서만 include합니다.

// wrappers/json.cppm
export module wrappers.json;
#include <nlohmann/json.hpp>
export namespace json = nlohmann;

이렇게 하면 #include <nlohmann/json.hpp>를 여러 TU에서 반복하지 않고, 래퍼 모듈 하나에서만 include합니다.

패턴 3: 테스트 전용 모듈

테스트에서만 필요한 테스트 픽스처 모듈을 두고, 프로덕션 빌드에는 포함하지 않습니다.

add_library(engine.test_helpers STATIC
    test/helpers.cppm
)
target_sources(engine.test_helpers PUBLIC
    FILE_SET CXX_MODULES FILES test/helpers.cppm
)
target_link_libraries(engine.test_helpers PUBLIC engine.core)

add_executable(engine_tests test/main.cpp)
target_link_libraries(engine_tests PRIVATE engine.math engine.test_helpers)

12. 성능 비교: 빌드 시간, 바이너리 크기

빌드 시간

증분 빌드 (Incremental Build):

  • 모듈이 헤더 대비 30~50% 빌드 시간 단축 가능.
  • 에러 발생 시 피드백: 모듈 0.85초 vs 헤더 3.11초 (예시).

클린 빌드 (Clean Build):

  • 개선이 상대적으로 작음 (병렬화·의존성 체인에 따라 다름).
  • BMI 생성 비용이 있어, 처음 빌드 시 오히려 느릴 수 있음.

템플릿이 많은 코드:

  • 일부 벤치마크에서 import#include보다 2~3배 느린 경우도 있음.
  • 컴파일러 구현·최적화에 따라 달라질 수 있음.

바이너리 크기

  • 모듈 전환 시 바이너리 크기는 거의 동일.
  • 인라인 함수·템플릿 인스턴스화는 동일하게 적용됨.
  • 모듈의 이점은 컴파일 시간·의존성 정리에 있음.
  • BMI 파일은 빌드 중간 산출물로, 최종 실행 파일에는 포함되지 않음.

벤치마크 결과 요약 (참고용)

항목헤더모듈비고
증분 빌드100%55~70%수정된 TU 수에 따라 다름
클린 빌드100%90~100%프로젝트 구조에 따라 다름
바이너리 크기100%~100%차이 거의 없음

13. 프로덕션 마이그레이션 전략

Phase 1: 준비 (1~2주)

  • C++20 활성화 및 컴파일러·CMake 버전 확인
  • 모듈 사용하지 않는 기존 코드는 CMAKE_CXX_SCAN_FOR_MODULES 0 설정 검토
  • 의존성 그래프 파악 (가장 아래 레이어부터 식별)

Phase 2: 파일럿 (2~4주)

  • 의존성 없는 유틸 1~2개를 모듈로 전환
  • CI/CD에서 빌드 시간·성공 여부 확인
  • 팀 내 공유 및 피드백 수집

Phase 3: 점진적 확장 (1~3개월)

  • 레이어별로 위에서 사용하는 모듈부터 순차 전환
  • include와 import 공존 시 순서·매크로 처리 정책 수립
  • 문서화: “이 모듈은 import, 이건 include” 가이드

Phase 4: 안정화

  • 새 코드는 기본적으로 모듈 사용
  • 레거시 헤더는 필요 시 점진적 제거 또는 래퍼 유지

의존성 순서와 BMI 캐시

  • 모듈 A가 모듈 B를 import하면, B가 A보다 먼저 컴파일되어야 함.
  • CMake 등에서는 모듈 인터페이스 타겟 간 의존성을 명시해 주면 됨.
  • 모듈 컴파일 결과(BMI 등)를 캐시해 두면, 의존하는 소스만 재컴파일할 수 있음. 도구/컴파일러별 옵션 확인 필요.

실무 팁

“C++20 module migration”, “헤더를 모듈로 전환” 등으로 검색할 때 위 표를 먼저 참고하면 원인 좁히기에 도움이 됩니다. MSVC에서는 .ixx 확장자를, Clang/GCC에서는 .cppm 확장자를 사용하는 것이 관례입니다.

체크리스트

- [ ] 환경: C++20, CMake 3.28+, Ninja/VS 2022
- [ ] 모듈 스캔: 필요 시에만 활성화
- [ ] export 범위: 공개 API만 export
- [ ] include/import 순서: import 먼저
- [ ] 매크로: constexpr/인라인 함수로 대체 또는 include 유지
- [ ] CI/CD: 빌드 시간 모니터링

기존 사용자 유지

  • 기존에는 #include "mylib/foo.h" 로 쓰던 코드를 그대로 두려면:
    • foo.h를 “모듈을 import한 뒤 재export”하는 래퍼로 만들 수 있음 (도구/컴파일러 지원에 따라 다름).
    • 또는 당분간 헤더와 모듈 인터페이스 둘 다 유지하고, 새 코드만 import 하도록 안내.

정리

항목내용
전략바닥부터 한 단위씩 모듈화
매핑헤더 1개 → 모듈 1개 또는 여러 헤더 → 1 모듈+파티션
export공개 API만 export
공존import 먼저, include 나중; 기존 헤더 유지 가능
빌드모듈 의존 순서와 BMI 캐시 고려
CMake3.28+, FILE_SET CXX_MODULES, target_link_libraries
성능증분 빌드에서 30~50% 개선 가능
프로덕션파일럿 → 점진적 확장 → 안정화

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 헤더 기반 C++ 프로젝트를 C++20 모듈로 옮기는 전략, export/import 매핑, 그리고 헤더와의 공존 방법을 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

Q. 모듈 전환 시 CMake는 어떻게 바꾸나요?

A. CMake 3.28 이상에서는 CXX_MODULE_STD 등을 설정하고, 모듈 인터페이스 소스(.cppm)를 target_sources(이름 FILE_SET ... TYPE CXX_MODULES)로 등록합니다. 기존 프로젝트는 CMake 공식 C++ 모듈 문서모듈 기초(#24-1)를 참고해 단계적으로 적용하면 됩니다.

Q. 헤더와 모듈을 같이 쓰는 코드에서 주의할 점은?

A. 같은 번역 단위에서는 import를 먼저 쓰고, 그 다음 #include를 두는 것이 안전합니다. 매크로나 조건부 컴파일에 의존하는 헤더는 당분간 include로 유지하고, 새로 작성하는 코드만 모듈을 사용하는 식으로 점진적으로 옮기면 됩니다.

Q. 모듈 전환 후 바이너리 크기는 어떻게 되나요?

A. 바이너리 크기는 헤더와 거의 동일합니다. 모듈의 이점은 컴파일 시간 단축과 의존성 정리에 있습니다.


참고 자료


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++20 Modules | “#include 지옥” 탈출, import로 컴파일 속도 높이기
  • C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)
  • C++ 컴파일 과정 | “undefined reference” 에러가 나는 이유 (전처리·링킹 4단계)

이 글에서 다루는 키워드 (관련 검색어)

C++20 모듈 마이그레이션, 헤더를 모듈로 전환, export import, 모듈과 헤더 공존, CMake C++ 모듈, 모듈 빌드 오류, 점진적 모듈 전환, 빌드 시간 단축, 모듈 인터페이스 구현 유닛 등으로 검색하시면 이 글이 도움이 됩니다.

한 줄 요약: 한 레이어씩 모듈로 옮기고 include와 import를 함께 쓰면 점진적 전환이 가능합니다. 다음으로 Ranges 기초(#25-1)를 읽어보면 좋습니다.

다음 글: [C++ 실전 가이드 #25-1] C++20 Ranges 기초: 범위와 반복자 개선

이전 글: [C++ 실전 가이드 #24-1] C++20 Modules 기초: “#include 지옥” 탈출


관련 글

  • C++20 Modules |
  • C++20 Coroutine | co_await·co_yield로
  • C++ Generator 완벽 가이드 | co_yield로 lazy 시퀀스·무한 수열·파이프라인 만들기
  • C++ 비동기 작업과 Coroutine | co_await로 콜백 지옥 탈출하기 [#23-3]
  • C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기