콘텐츠로 이동

C++20 Modules 기초와 실전 활용

C++ 모듈은 40년 이상 사용된 #include 기반 헤더 시스템을 대체하는 새로운 코드 조직화 메커니즘입니다. 빌드 속도 향상, 매크로 격리, 명시적 인터페이스 정의 등 여러 장점을 제공합니다.


// 헤더 파일의 전통적 문제들
#include <vector> // 전체 vector 구현 코드 복붙
#include <string> // 다시 복붙
// 컴파일 단위마다 반복, 빌드 시간 폭증
// 매크로 오염
#define MAX_SIZE 100 // 전역으로 퍼짐

모듈은 이를 해결합니다. 모듈 인터페이스는 한 번만 컴파일되고 바이너리 형태로 재사용됩니다.


2.1 모듈 인터페이스 단위 (.ixx 또는 .cppm)

섹션 제목: “2.1 모듈 인터페이스 단위 (.ixx 또는 .cppm)”
math.ixx
export module math; // 모듈 선언
import <string>; // 표준 라이브러리 임포트 (모듈화된 경우)
// export: 외부에서 사용 가능
export int add(int a, int b) {
return a + b;
}
export double pi = 3.14159265358979;
// export 없음: 모듈 내부에서만 사용
static int helper(int x) {
return x * 2;
}
main.cpp
import math; // 모듈 임포트
#include <iostream> // 헤더와 혼용 가능
int main() {
std::cout << add(3, 4) << "\n"; // 7
std::cout << pi << "\n"; // 3.14159...
// helper(5); // 오류: export 되지 않음
return 0;
}

export module geometry;
export {
struct Point { double x, y; };
struct Rect { Point tl, br; };
double area(const Rect& r);
}
// 구현 (export 불필요)
double area(const Rect& r) {
return (r.br.x - r.tl.x) * (r.br.y - r.tl.y);
}
export module mylib;
export import math; // math 모듈의 export를 재내보냄
export import geometry; // geometry도 재내보냄

대형 모듈을 논리적 파티션으로 분할할 수 있습니다.

// engine-render.ixx (파티션)
export module engine:render; // engine 모듈의 render 파티션
export void renderFrame();
export void setResolution(int w, int h);
void renderFrame() { /* 구현 */ }
void setResolution(int w, int h) { /* 구현 */ }
engine-physics.ixx
export module engine:physics;
export void updatePhysics(float dt);
void updatePhysics(float dt) { /* 구현 */ }
// engine.ixx (기본 모듈 인터페이스)
export module engine;
export import :render; // 파티션 재내보내기
export import :physics;
main.cpp
import engine; // render, physics 모두 접근 가능
int main() {
renderFrame();
updatePhysics(0.016f);
return 0;
}

5. 구현 단위 (Module Implementation Unit)

섹션 제목: “5. 구현 단위 (Module Implementation Unit)”

인터페이스와 구현을 분리할 수 있습니다.

// logger.ixx (인터페이스)
export module logger;
export class Logger {
public:
void log(const char* msg);
void warn(const char* msg);
void error(const char* msg);
};
// logger.cpp (구현 단위)
module logger; // export 없음 = 구현 단위
#include <cstdio>
void Logger::log(const char* msg) {
std::printf("[LOG] %s\n", msg);
}
void Logger::warn(const char* msg) {
std::printf("[WARN] %s\n", msg);
}
void Logger::error(const char* msg) {
std::printf("[ERROR] %s\n", msg);
}

기존 헤더 파일을 모듈처럼 임포트할 수 있습니다.

// 헤더 단위로 임포트 (컴파일러 지원 필요)
import <vector>;
import <string>;
import "my_legacy_header.h";

이는 완전한 모듈은 아니지만 빌드 속도를 개선하는 중간 단계로 유용합니다.


cmake_minimum_required(VERSION 3.28)
project(MyProject)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
add_executable(app
main.cpp
math.ixx
geometry.ixx
)
cl /std:c++20 /experimental:module /interface math.ixx
cl /std:c++20 /experimental:module main.cpp math.ixx.obj

기존 코드베이스를 한 번에 모듈로 전환하는 것은 현실적이지 않습니다.

legacy_wrapper.ixx
// 전략 1: 헤더 파일 래핑
export module legacy;
// 기존 헤더를 global module fragment에 포함
module; // global module fragment 시작
#include "old_library.h" // 매크로, 전처리기 내용 포함 가능
export module legacy; // named module 시작
// old_library.h의 타입을 재내보내거나 래핑
export using OldType = ::OldLibraryType;

항목헤더 파일모듈
빌드 속도파일마다 재파싱한 번만 컴파일
매크로 격리전역으로 누출모듈 내부 격리
순환 의존성문제 발생명시적 오류
include guard필요불필요
도입 비용없음빌드 시스템 지원 필요

10. Global Module Fragment — 레거시 헤더 통합

섹션 제목: “10. Global Module Fragment — 레거시 헤더 통합”

모듈 내부에서 매크로를 포함한 기존 헤더를 사용해야 할 때 global module fragment를 활용합니다.

mylib.ixx
module; // global module fragment 시작 — 이름 없는 영역
// 레거시 헤더는 여기에만 포함 (매크로, 전처리기 지시문 포함 가능)
#include <cstdio>
#include <windows.h> // 플랫폼 헤더
#define LEGACY_MACRO 1 // 이 매크로는 모듈 외부로 누출되지 않음
export module mylib; // named module 시작
// global fragment에서 포함한 헤더의 심볼 사용 가능
export void log(const char* msg)
{
std::printf("[LOG] %s\n", msg);
}

Terminal window
# 모듈 인터페이스 컴파일
g++ -std=c++20 -fmodules-ts -x c++-module math.ixx -o math.gcm
# 모듈을 사용하는 파일 컴파일
g++ -std=c++20 -fmodules-ts main.cpp -o main
Terminal window
# 모듈 인터페이스 유닛 컴파일
clang++ -std=c++20 --precompile math.ixx -o math.pcm
# 모듈을 사용하는 파일 컴파일
clang++ -std=c++20 -fmodule-file=math.pcm main.cpp -o main
Terminal window
:: 모듈 인터페이스 컴파일
cl /std:c++20 /EHsc /interface /TP math.ixx
:: 모듈 사용 파일 컴파일
cl /std:c++20 /EHsc main.cpp math.obj
cmake_minimum_required(VERSION 3.28)
project(ModuleDemo CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_SCAN_FOR_MODULES ON) # 자동 모듈 의존성 스캔
add_executable(app
main.cpp
src/math.ixx
src/string_utils.ixx
)
# 컴파일러가 모듈 의존성을 자동으로 처리
target_compile_features(app PRIVATE cxx_std_20)

project/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── core/
│ │ ├── core.ixx # export module core;
│ │ ├── core-types.ixx # export module core:types;
│ │ └── core-utils.ixx # export module core:utils;
│ └── io/
│ ├── io.ixx # export module io;
│ └── io.cpp # module io; (구현 단위)
core/core-types.ixx
export module core:types;
export struct Vector3 {
float x, y, z;
Vector3 operator+(const Vector3& o) const {
return {x + o.x, y + o.y, z + o.z};
}
};
export using Point3D = Vector3;
// core/core.ixx — 파티션 집계
export module core;
export import :types; // core:types 재내보내기
export import :utils; // core:utils 재내보내기
main.cpp
import core; // types, utils 모두 접근 가능
int main()
{
Vector3 a{1, 2, 3};
Vector3 b{4, 5, 6};
auto c = a + b; // {5, 7, 9}
}

// 함정 1: export와 #define은 함께 사용 불가
export module mylib;
// #define MY_MACRO 1 // 이 매크로는 import하는 쪽에 전달되지 않음
// export #define ... // 문법 오류 — 매크로는 export 불가
// 함정 2: 모듈에서 전처리기 지시문은 global fragment에만 허용
module;
#include <cmath> // OK — global fragment
export module mylib;
// #include <algorithm> // 경고 또는 오류 — named module 영역에서는 지양
// 함정 3: 같은 TU에서 헤더와 모듈 import 순서
import std.core; // 모듈 import는 먼저
#include <cstdio> // 헤더 include는 나중 (또는 global fragment에서)
// 함정 4: inline 함수는 모듈에서 정의가 필요
export module utils;
export inline int square(int x) { return x * x; } // OK — 인라인은 정의 포함
// export int cube(int x); // 선언만 export하면 링크 오류 가능 (구현 단위 필요)

C++20 모듈은 C++ 빌드 시스템의 근본적인 개선을 가져옵니다. 컴파일 시간 단축, 명확한 인터페이스 정의, 매크로 오염 방지라는 세 가지 핵심 이점을 제공합니다. 2025년 기준으로 MSVC, GCC 14+, Clang 16+ 모두 기본적인 모듈 지원을 제공하므로 새 프로젝트부터 점진적으로 도입해볼 것을 권장합니다.

도입 로드맵:

  1. 새 유틸리티 모듈부터 .ixx / .cppm으로 작성
  2. CMake 3.28+로 자동 의존성 스캔 활성화
  3. 기존 헤더는 global fragment 래핑으로 통합
  4. 점진적으로 모듈 파티션 구조로 리팩터링