[모던 CMake] 대규모 프로젝트에서의 베스트 프랙티스

모던 CMake를 활용하여 효율적인 C++ 프로젝트 빌드 시스템을 구축하는 방법을 계속해서 알아보겠습니다. 이번 글에서는 대규모 프로젝트에서 CMake를 효과적으로 사용하기 위한 베스트 프랙티스에 대해 다루겠습니다. 프로젝트 규모가 커질수록 빌드 시스템의 복잡도도 증가하기 때문에, 유지보수성과 확장성을 고려한 빌드 설정이 중요합니다.

etc-image-0

디렉토리 구조의 중요성

대규모 프로젝트에서는 명확하고 일관된 디렉토리 구조를 유지하는 것이 중요합니다. 이를 통해 코드의 가독성을 높이고, 협업 시 혼란을 최소화할 수 있습니다.

표준 디렉토리 구조 예시

my_large_project/
├── CMakeLists.txt
├── src/
│   ├── CMakeLists.txt
│   ├── module1/
│   │   ├── CMakeLists.txt
│   │   ├── module1.cpp
│   │   └── module1.h
│   ├── module2/
│   │   ├── CMakeLists.txt
│   │   ├── module2.cpp
│   │   └── module2.h
│   └── ...
├── include/
│   ├── module1/
│   │   └── module1.h
│   ├── module2/
│   │   └── module2.h
│   └── ...
├── libs/
│   ├── third_party_lib1/
│   └── third_party_lib2/
├── tests/
│   ├── CMakeLists.txt
│   ├── module1_tests/
│   └── module2_tests/
└── docs/
  • src/: 소스 코드가 위치합니다. 각 모듈이나 컴포넌트별로 하위 디렉토리를 구분합니다.
  • include/: 헤더 파일이 위치합니다. 소스 코드와 동일한 구조를 유지합니다.
  • libs/: 외부 라이브러리나 서브모듈이 위치합니다.
  • tests/: 테스트 코드가 위치합니다.
  • docs/: 문서화 파일이 위치합니다.

모듈화와 재사용성

라이브러리 분리

프로젝트를 여러 개의 라이브러리로 분리하여 모듈화합니다. 각 라이브러리는 독립적으로 빌드되고 테스트될 수 있어야 합니다.

예제: 모듈별 CMakeLists.txt

src/module1/CMakeLists.txt

add_library(Module1 module1.cpp)

target_include_directories(Module1 PUBLIC
    $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include/module1>
    $<INSTALL_INTERFACE:include/module1>
)

# 필요 시 의존성 추가
# target_link_libraries(Module1 PUBLIC Module2)

인터페이스 라이브러리 활용

인터페이스 라이브러리는 소스 파일 없이 빌드 설정과 의존성만을 전파하는 용도로 사용됩니다. 공통 설정이나 유틸리티 함수를 공유하는 데 유용합니다.

add_library(CommonConfig INTERFACE)

target_compile_definitions(CommonConfig INTERFACE USE_COMMON_FEATURE)
target_include_directories(CommonConfig INTERFACE ${CMAKE_SOURCE_DIR}/include/common)

타겟 프로퍼티와 범위 지정의 일관성

타겟 프로퍼티 설정 시 범위 지정 키워드인 PRIVATE, PUBLIC, INTERFACE를 일관성 있게 사용합니다.

  • PRIVATE: 타겟 내부에서만 사용되는 설정입니다.
  • PUBLIC: 타겟과 이를 의존하는 타겟 모두에 적용되는 설정입니다.
  • INTERFACE: 타겟을 의존하는 타겟에만 적용되는 설정입니다.

예제: 타겟 프로퍼티 설정

target_compile_definitions(Module1
    PUBLIC MODULE1_PUBLIC_DEFINE
    PRIVATE MODULE1_PRIVATE_DEFINE
)

target_include_directories(Module1
    PUBLIC ${CMAKE_SOURCE_DIR}/include/module1
)
  • Module1을 사용하는 타겟은 MODULE1_PUBLIC_DEFINE 매크로에 접근할 수 있지만, MODULE1_PRIVATE_DEFINE는 접근할 수 없습니다.

의존성 관리의 명확성

라이브러리 간 의존성을 명확하게 정의하여 빌드 순서와 설정 전파를 정확하게 관리합니다.

예제: 의존성 정의

add_library(Module2 module2.cpp)

target_link_libraries(Module2 PUBLIC Module1)
  • Module2는 Module1에 의존하며, PUBLIC 범위를 사용하여 Module2를 사용하는 타겟도 Module1의 설정을 상속받습니다.

전역 설정과 로컬 설정의 분리

프로젝트 전체에 적용되는 설정과 개별 타겟에 적용되는 설정을 분리하여 관리합니다.

전역 설정 적용

최상위 CMakeLists.txt에서 프로젝트 전역 설정을 지정합니다.

# C++ 표준 설정
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 전역 컴파일 옵션
add_compile_options(-Wall -Wextra -Wpedantic)

개별 타겟 설정

타겟별로 필요한 설정은 각자의 CMakeLists.txt에서 지정합니다.

target_compile_definitions(Module1 PRIVATE MODULE1_INTERNAL)

사용자 정의 함수와 매크로 활용

반복되는 설정이나 패턴은 사용자 정의 함수나 매크로로 추출하여 재사용성을 높입니다.

예제: 라이브러리 생성 함수

function(create_module NAME)
    add_library(${NAME} ${ARGN})
    target_include_directories(${NAME} PUBLIC
        $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include/${NAME}>
        $<INSTALL_INTERFACE:include/${NAME}>
    )
endfunction()
  • 사용법:
create_module(Module1 module1.cpp)
create_module(Module2 module2.cpp)

CMake 모듈과 패키지 관리

공통 설정이나 기능을 CMake 모듈로 분리하여 관리하고, 필요에 따라 패키지화하여 재사용합니다.

CMake 모듈 작성

cmake/ 디렉토리에 공통 모듈을 작성합니다.

cmake/CompilerWarnings.cmake

function(enable_strict_warnings TARGET)
    if(MSVC)
        target_compile_options(${TARGET} PRIVATE /W4 /WX)
    else()
        target_compile_options(${TARGET} PRIVATE -Wall -Wextra -Werror)
    endif()
endfunction()
  • 사용법:
include(cmake/CompilerWarnings.cmake)

enable_strict_warnings(MyApp)

패키지 설정 파일 생성

라이브러리를 패키지화하여 다른 프로젝트에서 find_package()로 쉽게 사용할 수 있도록 설정합니다.

install(TARGETS Module1 EXPORT Module1Config
        ARCHIVE DESTINATION lib
        LIBRARY DESTINATION lib
        RUNTIME DESTINATION bin
        PUBLIC_HEADER DESTINATION include/module1)

install(EXPORT Module1Config
        DESTINATION lib/cmake/Module1)

빌드 속도 최적화

대규모 프로젝트에서는 빌드 시간이 길어질 수 있으므로, 빌드 속도를 최적화하는 것이 중요합니다.

Unity Build 활용

Unity Build는 여러 소스 파일을 하나로 합쳐 컴파일하는 방식으로, 컴파일러의 오버헤드를 줄여 빌드 시간을 단축합니다.

set_target_properties(MyApp PROPERTIES UNITY_BUILD ON)
  • 주의: Unity Build는 코드 구조에 따라 컴파일 오류를 유발할 수 있으므로, 충분한 테스트가 필요합니다.

ccache 사용

ccache는 컴파일 캐시를 활용하여 동일한 컴파일 작업을 반복하지 않도록 합니다.

  • ccache 설치 후, 컴파일러 경로를 ccache로 설정합니다.
export CC="ccache gcc"
export CXX="ccache g++"
  • 또는 CMake 설정에서 지정합니다.
set(CMAKE_C_COMPILER_LAUNCHER ccache)
set(CMAKE_CXX_COMPILER_LAUNCHER ccache)

지속적 통합과 테스트

CI/CD 파이프라인을 구축하여 지속적으로 코드의 품질을 관리합니다.

다양한 빌드 구성 테스트

  • 여러 가지 빌드 타입(Debug, Release)과 옵션 조합으로 빌드를 테스트합니다.
  • 다양한 플랫폼과 컴파일러에서 빌드를 실행하여 호환성을 확인합니다.

코드 커버리지 분석

코드 커버리지를 측정하여 테스트의 효과성을 평가합니다.

if(CMAKE_BUILD_TYPE STREQUAL "Coverage")
    target_compile_options(MyApp PRIVATE --coverage)
    target_link_options(MyApp PRIVATE --coverage)
endif()
  • 빌드 후 lcov와 genhtml을 사용하여 커버리지 리포트를 생성합니다.

문서화와 코드 스타일 유지

프로젝트의 문서화를 자동화하고, 일관된 코드 스타일을 유지합니다.

Doxygen을 사용한 API 문서화

find_package(Doxygen)

if(DOXYGEN_FOUND)
    set(DOXYFILE_IN ${CMAKE_CURRENT_SOURCE_DIR}/docs/Doxyfile.in)
    set(DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
    
    configure_file(${DOXYFILE_IN} ${DOXYFILE_OUT} @ONLY)
    
    add_custom_target(doc
        COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYFILE_OUT}
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
        COMMENT "Generating API documentation with Doxygen"
        VERBATIM)
endif()

코드 스타일 검사

  • Clang-FormatClang-Tidy를 활용하여 코드 스타일을 유지하고 잠재적 버그를 찾아냅니다.
  • 이전 글에서 소개한 방법을 참고하여 빌드 시스템에 통합합니다.

결론

이번 글에서는 대규모 프로젝트에서 모던 CMake를 효과적으로 사용하기 위한 베스트 프랙티스에 대해 알아보았습니다. 프로젝트 구조화, 모듈화, 의존성 관리, 빌드 최적화 등 다양한 측면에서 고려해야 할 사항들을 살펴보았습니다.

반응형