26 5월 2025

C++의 모듈

C++ 생태계는 수십 년 만에 가장 심오한 변화를 겪고 있으며, 코드가 구성되고 컴파일되는 방식을 근본적으로 재구상하고 있습니다. 수년 동안 개발자들은 #include 시스템의 한계, 즉 느린 컴파일 시간, 광범위한 매크로 오염, 적절한 캡슐화 부족으로 어려움을 겪어왔습니다. 전처리기 기반 접근 방식의 이러한 내재된 결함은 대규모 개발에서 C++의 잠재력을 제한했으며, 엔지니어들이 복잡한 해결 방법과 빌드 시스템을 채택하도록 강요했습니다.

C++ 20 모듈은 이러한 오랜 과제에 대한 포괄적인 해결책으로 등장했으며, 언어의 시작 이래 C++ 코드 구성에서 첫 번째 주요 패러다임 전환을 나타냅니다. 모듈은 텍스트 포함을 구조화된 이진 인터페이스로 대체함으로써 컴파일 속도, 코드 격리 및 인터페이스 명확성에서 혁신적인 개선을 제공합니다. 이는 단지 점진적인 개선이 아니라 C++ 프로그램 구성의 바로 그 아키텍처를 다루는 근본적인 변화입니다.

C++ 모듈이 필수적인 이유

전통적인 헤더 파일 모델은 현대 C++ 개발에 네 가지 근본적인 제약을 부과합니다:

  1. 컴파일 병목 현상: 헤더 파일이 포함되면, 해당 헤더 파일을 포함하는 모든 번역 단위에 대해 모든 의존성을 포함한 전체 내용이 다시 파싱되고 다시 컴파일되었습니다. 특히 표준 라이브러리와 같이 널리 사용되는 헤더의 경우 이러한 중복 처리는 대규모 프로젝트에서 컴파일 시간을 극적으로 증가시켰습니다. 컴파일러는 동일한 선언과 정의를 여러 번 처리해야 했고, 이는 상당한 오버헤드를 초래했습니다.
  2. 매크로 오염: 헤더 파일 내에 정의된 매크로는 해당 헤더 파일을 포함하는 모든 번역 단위의 전역 이름 공간으로 "누출"되었습니다. 이는 종종 이름 충돌, 예상치 못한 동작, 그리고 진단하기 어려운 미묘한 버그로 이어졌습니다. 매크로는 언어 키워드를 재정의하거나 다른 유효한 식별자와 충돌하여 견고한 코딩에 불리한 환경을 조성할 수 있었습니다.
  3. 약한 캡슐화: 헤더 파일은 선언, 정의, 심지어 내부 도우미 함수나 데이터까지 그 모든 내용을 효과적으로 노출했습니다. 구성 요소의 "공개 인터페이스"와 내부 구현 세부 사항을 명시적으로 정의하는 직접적인 메커니즘이 없었습니다. 이러한 견고한 캡슐화의 부족은 코드 가독성, 유지 보수성 및 안전한 리팩토링을 방해했습니다.
  4. 단일 정의 규칙(ODR) 위반: ODR은 서로 다른 번역 단위에 걸쳐 동일한 엔티티의 다중 정의를 금지하지만, 헤더 기반 시스템은 특히 템플릿 및 인라인 함수에서 이 규칙을 준수하기 어렵게 만들었습니다. 우발적인 위반은 링커 오류 또는 정의되지 않은 동작으로 이어질 수 있었습니다.

C++ 모듈은 이러한 과제에 직접적으로 맞섭니다. 모듈은 한 번 이진 표현으로 컴파일되며, 컴파일러는 이를 텍스트 기반 헤더 파일보다 훨씬 빠르게 가져와 처리할 수 있습니다. 이러한 코드 변환은 컴파일 중의 중복 작업을 크게 줄여줍니다. 모듈은 또한 가시성을 엄격하게 제어하여 명시적으로 표시된 것만 내보내므로 매크로 오염을 방지하고 더 강력한 캡슐화를 강제합니다. 명시적으로 내보내지지 않은 이름과 매크로는 모듈 내부에 비공개로 유지되어 코드 격리를 개선합니다.

C++ 모듈 예제 살펴보기

기본 구문을 이해하는 것은 C++ 모듈을 사용한 효과적인 프로그래밍의 기초를 형성합니다. 모듈 정의는 일반적으로 모듈이 내보내는 것을 선언하는 인터페이스 단위와 정의를 제공하는 구현 단위를 포함합니다.

수학 모듈에 대한 간단한 C++ 모듈 예제를 고려해 봅시다:

1. 모듈 인터페이스 단위 (math.ixx)

// math.ixx - Primary module interface unit for 'math'
export module math;

// Exported function declarations
export int add(int a, int b);
export int multiply(int a, int b);

// Internal helper function, not exported
int subtract_internal(int a, int b);

여기서 export module math;는 이 파일이 math라는 이름의 모듈에 대한 기본 인터페이스임을 선언합니다. addmultiply 앞의 export 키워드는 이 함수들이 모듈의 공개 인터페이스의 일부이며, math를 가져오는 다른 번역 단위에서 접근할 수 있음을 나타냅니다. export가 없는 subtract_internal 함수는 모듈에 비공개로 유지됩니다.

2. 모듈 구현 단위 (math.cpp)

// math.cpp - Module implementation unit for 'math'
module math; // Associate this file with the 'math' module

// Definitions for exported functions
int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

// Definition for internal helper function
int subtract_internal(int a, int b) {
    return a - b;
}

이 파일은 module math;로 시작하며, 이는 이 파일을 math 모듈과 연결합니다. math.ixx에 선언된 함수들의 정의를 포함합니다. 구현 파일은 모듈 선언에 export를 사용하지 않는다는 점에 유의하십시오.

3. 모듈 사용하기 (main.cpp)

// main.cpp - Consuming the 'math' module
import math; // Import the 'math' module
#include <iostream> // For console output

int main() {
    int sum = add(10, 5); // Call exported function
    int product = multiply(10, 5); // Call exported function

    std::cout << "Sum: " << sum << std::endl;
    std::cout << "Product: " << product << std::endl;

    // int difference = subtract_internal(10, 5); // ERROR: subtract_internal is not exported
    return 0;
}

main.cpp에서 import math;math 모듈에서 내보낸 선언을 사용할 수 있게 합니다. addmultiply는 직접 사용될 수 있지만, subtract_internal은 접근할 수 없어 모듈의 강력한 캡슐화를 보여줍니다.

이 C++ 모듈 예제는 기본적인 패턴을 보여줍니다. 모듈 인터페이스 단위는 export module을 사용하여 모듈의 이름과 공개 인터페이스를 선언하는 반면, 모듈 구현 단위는 module을 사용하여 모듈의 내부 구현에 기여합니다.

C20 및 C23 모듈: 고급 기능 및 미래

기본적인 exportimport 외에도, C++ 20 모듈은 복잡한 프로젝트 구조와 레거시 코드 변환을 처리하기 위한 여러 고급 기능을 도입합니다. C++23 모듈은 추가적인 개선과 향상된 컴파일러 지원으로 이러한 발전을 계속합니다.

모듈 단위 및 구조

명명된 모듈은 동일한 모듈 이름을 공유하는 모듈 단위(소스 파일)의 모음입니다.

  • 기본 모듈 인터페이스 단위: 각 명명된 모듈은 정확히 하나의 기본 인터페이스 단위(export module MyModule;)를 가져야 합니다. 이 단위에서 내보낸 내용은 import MyModule을 가져올 때 가시화됩니다.
  • 모듈 인터페이스 단위 (일반): export module로 선언된 모든 단위는 인터페이스 단위입니다. 여기에는 기본 인터페이스 단위와 파티션 인터페이스 단위가 포함됩니다.
  • 모듈 구현 단위: module MyModule; ( export 없음)로 선언된 단위는 구현 단위입니다. 이는 모듈의 내부 로직에 기여하지만, 가져오는 코드에 직접 인터페이스를 노출하지는 않습니다.

글로벌 모듈 프래그먼트

모듈로 코드를 포팅하는 동안 중요한 과제는 전처리기 매크로에 의존하는 기존 헤더와의 통합입니다. 글로벌 모듈 프래그먼트는 이를 해결합니다.

// mymodule_with_legacy.ixx
module; // Start of global module fragment
#define USE_ADVANCED_FEATURE 1 // Macro defined in global fragment
#include <legacy_header.h>    // Include legacy header (hypothetical)

export module mymodule_with_legacy; // End of global module fragment, start of module
import <iostream>; // Import standard library module

export void perform_action() {
#if USE_ADVANCED_FEATURE
    std::cout << "Advanced action performed!" << std::endl;
#else
    std::cout << "Basic action performed!" << std::endl;
#endif
    // Use declarations from legacy_header.h if needed
}

module; 선언은 글로벌 모듈 프래그먼트를 시작합니다. 이 프래그먼트 내의 모든 #include 지시문이나 매크로 정의는 모듈 자체가 정의되기 전에 처리됩니다. 이를 통해 모듈은 모듈의 인터페이스 내에서 매크로 오염 없이 전처리기 종속 레거시 코드와 상호 작용할 수 있습니다. 여기서 정의된 매크로는 이 프래그먼트에 포함된 헤더의 처리에 영향을 미치지만, 나중에 모듈에서 헤더 단위로 가져온 헤더에는 영향을 미치지 않습니다.

프라이빗 모듈 프래그먼트

개발자들이 구현 세부 사항을 기본 인터페이스 단위 내에 유지하면서 가져오는 코드로부터 완전히 숨기고 싶어 하는 대규모 모듈의 경우, C++20 모듈은 프라이빗 모듈 프래그먼트를 제공합니다.

// main_module.ixx
export module main_module;

export int public_function();

module :private; // Start of private module fragment

// This function is only visible within main_module.ixx itself,
// not to any code that imports 'main_module'.
int internal_helper_function() {
    return 36;
}

int public_function() {
    return internal_helper_function();
}

module :private; 선언은 모듈의 공개 인터페이스와 비공개 구현 세부 사항 사이의 경계를 표시합니다. 기본 인터페이스 단위 내에서 이 선언 뒤의 코드는 해당 단위와 동일한 명명된 모듈에 속하는 다른 단위에서만 접근할 수 있으며, 외부에서 가져오는 코드에서는 접근할 수 없습니다. 이는 단일 .ixx 파일이 모듈을 나타내면서 공개 및 비공개 섹션을 명확하게 분리하여 캡슐화를 향상시킵니다. 프라이빗 모듈 프래그먼트를 포함하는 모듈 단위는 일반적으로 해당 모듈의 유일한 단위가 될 것입니다.

모듈 파티션

매우 큰 모듈의 경우, 더 작고 관리하기 쉬운 단위로 분해하는 것이 유익합니다. 모듈 파티션(C++20 모듈)은 이러한 내부 모듈성을 가능하게 합니다. 파티션은 본질적으로 더 큰 명명된 모듈의 서브 모듈입니다.

// large_module_part1.ixx - Partition interface unit
export module large_module:part1; // Declares a partition named 'part1' of 'large_module'

export void do_something_in_part1();
// large_module_part1.cpp - Partition implementation unit
module large_module:part1; // Links this file to the 'part1' partition

void do_something_in_part1() {
    // Implementation
}
// large_module_main.ixx - Primary module interface unit for 'large_module'
export module large_module;

// Export-import the partition interface. This makes `do_something_in_part1`
// visible when `large_module` is imported.
export import :part1;

// Can import an implementation partition (if one existed), but cannot export it.
// import :internal_part;

export void do_main_thing() {
    do_something_in_part1(); // Can call partition functions directly
}

파티션은 명명된 모듈 자체 내에서만 가시적입니다. 외부 번역 단위는 large_module:part1;을 직접 가져올 수 없습니다. 대신, 기본 모듈 인터페이스 단위(large_module_main.ixx)는 export import :part1;을 사용하여 part1에서 내보낸 엔티티가 large_module을 가져오는 코드에 가시화되도록 해야 합니다. 이러한 계층적 구조는 상당한 소프트웨어 개발 프로젝트 내에서 복잡성을 관리하는 견고한 방법을 제공합니다.

헤더 단위

C++20 모듈은 또한 헤더 단위를 도입하여 import <header_name>; 또는 import "header_name";를 사용하여 전통적인 헤더 파일을 가져올 수 있게 합니다. 헤더가 헤더 단위로 가져와질 때, 이는 한 번 파싱되고 그 선언은 모듈과 유사하게 효율적으로 사용 가능해집니다. 결정적으로, 가져오는 번역 단위의 import이전에 정의된 전처리기 매크로는 #include와 달리 헤더 단위 자체의 처리에 영향을 미치지 않습니다. 이는 더 일관된 동작을 제공하며 대규모 헤더의 컴파일 속도를 크게 향상시킬 수 있습니다.

// my_app.cpp
#define CUSTOM_SETTING 10 // This macro does NOT affect <vector> or "my_utility.h"

import <vector>;         // Imports the standard vector header as a header unit
import "my_utility.h"; // Imports a custom header as a header unit (hypothetical)

int main() {
    std::vector<int> numbers = {1, 2, 3};
    // ...
    return 0;
}

이러한 구분은 코드 포팅 노력에 중요합니다. 헤더가 가져오는 번역 단위의 전처리기 상태에 반드시 영향을 받아야 하는 구성을 위해 전처리기 지시문에 의존하는 경우, 글로벌 모듈 프래그먼트가 적절한 해결책입니다. 그렇지 않은 경우, 헤더 단위로 가져오는 것은 #include에 대한 더 빠르고 깔끔한 대안입니다.

기존 코드베이스에 C++ 모듈 통합하기

대규모 기존 C++ 프로젝트를 완전한 모듈 시스템으로 코드 변환하고 포팅하는 것은 점진적인 과정일 수 있습니다. 개발자들은 전체 코드베이스를 한 번에 변환할 필요가 없습니다. C++ 모듈은 전통적인 헤더 파일과 원활하게 공존하도록 설계되었습니다.

  • 병렬 사용: C++ 소스 파일은 모듈을 import하고 헤더 파일을 #include할 수 있습니다. 이러한 유연성은 점진적인 채택을 가능하게 하여 한 번에 하나의 구성 요소나 라이브러리를 변환할 수 있습니다.
  • 헤더를 모듈로 변환: 심각한 컴파일 병목 현상을 유발하는 핵심 라이브러리나 자주 포함되는 헤더를 식별하는 것부터 시작하십시오. 이들을 명명된 모듈로 변환하십시오. 이는 종종 가장 즉각적인 컴파일 속도 이점을 제공합니다.
  • 전략적 모듈 파티션: 매우 큰 내부 라이브러리의 경우, 모듈 파티션을 사용하여 구성 요소를 구성하는 것을 고려하십시오. 이는 내부 모듈성을 제공하면서 모듈 인터페이스를 깔끔하게 유지합니다.
  • 매크로 의존성 처리: 광범위한 전처리기 매크로를 사용하는 레거시 헤더의 경우, 헤더 단위로 가져올 수 있는지 또는 매크로 구성이 글로벌 모듈 프래그먼트 사용을 요구하는지 평가하십시오. 이러한 정보에 입각한 의사 결정은 성공적인 코드 변환에 필수적입니다.

핵심은 실험하고 측정하는 것입니다. 개발자들은 컴파일 시간의 의미 있는 단축과 코드 구성의 개선을 달성하는지 여부에 따라 채택 결정을 내려야 합니다.

컴파일러 지원 및 C++ 모듈의 미래

C++20 모듈에 대한 지원은 주요 컴파일러 전반에 걸쳐 지속적으로 발전하고 있습니다.

  • Microsoft Visual C++ (MSVC): MSVC는 C++ 20 모듈에 대한 강력한 지원을 제공하며, 최근 버전에서 상당한 발전을 이루었습니다. Visual Studio 2022 버전 17.5부터 C++ 표준 라이브러리를 모듈로 가져오는 것이 표준화되고 완전히 구현되어 상당한 성능 향상을 제공합니다.
  • Clang: Clang 또한 C++20 모듈에 대한 상당한 지원을 제공하며, 구현 및 성능 특성을 개선하기 위한 지속적인 개발이 이루어지고 있습니다.
  • GCC: GCC의 C++20 모듈 지원은 꾸준히 진행되고 있으며, 더 넓은 범위의 개발자들이 이를 사용할 수 있게 하고 있습니다.

C++ 23 모듈이 더욱 보편화됨에 따라, 추가적인 개선과 향상된 컴파일러 완성도는 전반적인 개발자 경험을 향상시킬 것입니다. 완전하고 최적화된 모듈 지원을 향한 여정은 계속되고 있지만, C++ 20에서 다져진 기반은 C++ 소프트웨어 개발 방식에 심오한 변화를 나타냅니다. 이러한 지속적인 발전은 언어에 더욱 견고하고 효율적인 미래를 약속합니다.

결론

C++ 모듈은 C++ 프로그래밍 관행을 근본적으로 재편하는 혁신적인 기능입니다. 이들은 느린 컴파일, 매크로 간섭, 약한 캡슐화와 같은 헤더 기반 시스템과 관련된 오랜 문제들을 직접적으로 해결합니다. 명시적인 인터페이스 정의와 효율적인 컴파일을 위한 견고한 시스템을 제공함으로써, C++ 모듈은 코드 구성을 향상시키고, 빌드 시간을 단축하며, 관심사의 명확한 분리를 촉진합니다. C++ 20 모듈을 채택하고 C++23 모듈의 향상된 기능을 준비하는 것은 개발자들이 더욱 확장 가능하고, 유지 보수하기 쉬우며, 효율적인 소프트웨어 개발 프로젝트를 구축할 수 있도록 하여 언어에 새로운 시대를 열어줍니다.