ccache로 Xcode 네이티브 의존성 빌드 가속하기

ccache로 Xcode 네이티브 의존성 빌드 가속하기

iOS 프로젝트에는 Swift 코드가 많지 않더라도 Objective-C, C 또는 C++ 컴파일 단위가 수십 개 포함될 수 있습니다. 클라우드 Mac 작업이 매번 새로운 작업 디렉터리에서 시작되면 이러한 네이티브 의존성을 처리하기 위해 Clang이 다시 호출되므로 증분 빌드의 이점이 사라집니다. ccache는 이 반복 작업을 줄이는 데 적합합니다. 전처리 결과, 컴파일 옵션, 컴파일러 식별 정보를 바탕으로 키를 생성하고, 적중하면 오브젝트 파일을 바로 재사용합니다.

다만 ccache는 Xcode 전반을 가속하는 범용 스위치가 아닙니다. 도입하기 전에 실제 병목이 Swift 프런트엔드, 링크, 리소스 컴파일 또는 테스트 단계가 아니라 Clang에 있는지 먼저 확인해야 합니다.

캐시 대상을 먼저 확인하기

전체 빌드 로그를 보관한 뒤 CompileC, CompileC normal과 .m, .mm, .c, .cc, .cpp 파일을 검색합니다. 이러한 단계가 빌드 시간에서 눈에 띄는 비중을 차지할 때만 ccache를 도입할 가치가 있습니다. 순수 Swift 프로젝트는 설정이 올바르더라도 성능 개선이 거의 없을 수 있습니다.

먼저 다음 세 가지 기준값을 기록합니다.

  • 정리 후 전체 빌드에 걸린 시간
  • 코드를 변경하지 않고 다시 빌드했을 때 걸린 시간
  • 네이티브 컴파일 단위 수와 가장 느린 대상 몇 개

처음부터 DerivedData 전체를 캐시하지 마세요. 이 디렉터리에는 인덱스, 모듈 캐시, 빌드 데이터베이스, 중간 산출물이 들어 있으며 Xcode 버전, SDK, 절대 경로, 프로젝트 그래프와 강하게 결합되어 있습니다. ccache 디렉터리를 별도의 관찰 가능한 캐시 단위로 관리하는 편이 더 적합합니다.

먼저 작업에서 Clang이 반복 호출된다는 사실을 입증한 다음 적중률을 논의해야 합니다. 그렇지 않으면 링크 지연, Swift 타입 검사 또는 스크립트 단계의 소요 시간을 캐시 실패로 잘못 판단하기 쉽습니다.

래퍼로 Clang 호출 가로채기

빌드 환경에 ccache를 설치한 뒤 시스템 컴파일러 파일을 교체하지 마세요. 대신 프로젝트 작업 공간에 가벼운 래퍼 두 개를 만들고 Xcode 빌드 설정을 통해 전달합니다. 이렇게 하면 해당 옵션만 제거해 손쉽게 되돌릴 수 있고, 같은 노드에서 실행되는 다른 작업에도 영향을 주지 않습니다.

set -eu

export CCACHE_BIN="$(command -v ccache)"
export CCACHE_DIR="$HOME/.cache/pingmini-ccache"
export CCACHE_BASEDIR="$PWD"

mkdir -p .ci/bin "$CCACHE_DIR"

cat > .ci/bin/clang <<'SH'
#!/bin/sh
exec "${CCACHE_BIN:?CCACHE_BIN is required}" /usr/bin/clang "$@"
SH

cat > .ci/bin/clang++ <<'SH'
#!/bin/sh
exec "${CCACHE_BIN:?CCACHE_BIN is required}" /usr/bin/clang++ "$@"
SH

chmod +x .ci/bin/clang .ci/bin/clang++

ccache --set-config="cache_dir=$CCACHE_DIR"
ccache --set-config="base_dir=$PWD"
ccache --set-config="compiler_check=content"
ccache --set-config="compression=true"
ccache --set-config="max_size=20G"
ccache --zero-stats

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration Release \
  -derivedDataPath "$DERIVED_DATA" \
  build \
  CC="$PWD/.ci/bin/clang" \
  CPLUSPLUS="$PWD/.ci/bin/clang++"

ccache --show-stats

CC는 C와 Objective-C를 담당하고, CPLUSPLUS는 C++와 Objective-C++를 담당합니다. 링커를 래핑하거나 래퍼를 Swift 컴파일러처럼 위장해서는 안 됩니다. 실행 후 상세 로그에서 실제로 .ci/bin/clang이 호출되었는지 확인해야 합니다. ccache가 설치되어 있다는 사실만으로는 Xcode가 이를 사용했다는 증거가 되지 않습니다.

작업 간 캐시 키를 안정적으로 유지하기

CI는 동일한 커밋을 작업 번호가 포함된 디렉터리에 체크아웃하는 경우가 많습니다. 소스 코드는 바뀌지 않았더라도 절대 경로가 달라지면 디버그 정보, 헤더 파일 경로, 컴파일 옵션이 달라질 수 있습니다. CCACHE_BASEDIR와 base_dir는 작업 공간 내부의 경로를 상대 경로로 바꿔 줍니다. 이는 여러 작업에서 캐시를 재사용하기 위한 첫 단계입니다.

다음 입력도 고정해야 합니다.

툴체인과 빌드 옵션 고정하기

캐시 키에는 Xcode 메이저 버전, SDK, 대상 아키텍처, 구성 이름, 잠금 파일의 요약값이 포함되어야 합니다. 툴체인을 업그레이드할 때는 새 오브젝트와 이전 오브젝트를 같은 디렉터리에 섞기보다 캐시 네임스페이스를 명시적으로 전환하는 편이 감사하기 쉽습니다.

하나의 캐시를 Debug와 Release에서 동시에 사용해서는 안 되며, 서로 다른 아키텍처가 인위적으로 단순화된 키를 공유해서도 안 됩니다. ccache 자체는 옵션 차이를 식별하지만, 외부 영속화 키의 범위가 지나치게 넓으면 불필요한 다운로드와 제거가 대량으로 발생합니다.

생성 파일 제어하기

실행할 때마다 현재 시간, 무작위 식별자 또는 임시 절대 경로를 기록하는 생성 헤더 파일은 이를 참조하는 모든 컴파일 단위를 무효화합니다. 생성 스크립트는 내용이 바뀌었을 때만 파일을 덮어써야 하며, 임시 파일과 비교한 뒤 원자적으로 교체해야 합니다.

generate_config > "$TMPDIR/build-config.new"
if ! cmp -s "$TMPDIR/build-config.new" Generated/build-config.h; then
  mv "$TMPDIR/build-config.new" Generated/build-config.h
else
  rm "$TMPDIR/build-config.new"
fi

컴파일할 때마다 값이 달라지는 시간 매크로를 소스 코드에서 사용하지 마세요. 빌드 시간이 실제로 필요하다면 별도 파일에 넣어 영향을 받는 컴파일 범위를 줄입니다.

두 차례 빌드로 캐시 적중 검증하기

처음 도입할 때는 두 차례의 격리된 검증을 수행해야 합니다. 첫 번째 빌드에서는 빈 캐시를 유지한 상태로 통계만 초기화한 뒤 전체 빌드를 완료합니다. 두 번째 빌드에서는 DerivedData를 삭제하되 ccache는 유지하고, 완전히 동일한 툴체인과 옵션으로 다시 빌드합니다. 두 번째 빌드에서 직접 적중이 발생해야만 재사용이 이전 빌드 디렉터리에 의존하지 않는다는 사실을 입증할 수 있습니다.

전체 적중률만 보지 말고 cacheable calls, hits, misses, uncacheable calls를 각각 확인하세요. 첫 번째 빌드는 원래 misses가 대부분인 것이 정상입니다. 두 번째 빌드에서도 전부 misses라면 다음 순서로 점검합니다.

  1. 빌드 로그를 확인해 래퍼가 호출되었는지 검증합니다.
  2. 두 빌드의 작업 공간, SDK, 아키텍처, 컴파일 옵션을 비교합니다.
  3. 생성 헤더 파일이 실행할 때마다 바뀌는지 확인합니다.
  4. 시간 매크로, 무작위 값 또는 절대 경로가 있는지 확인합니다.
  5. 작업이 끝난 뒤 캐시 디렉터리가 실제로 저장되었는지 확인합니다.

uncacheable calls가 많다면 ccache 디버그 로그를 사용해 컴파일 단위 하나만 재현하세요. 전체 빌드에 대해 방대한 진단 파일을 생성하지 않도록 하기 위해서입니다. 원인을 찾은 뒤에는 디버그를 꺼서 로그 자체가 디스크를 가득 채우지 않게 해야 합니다.

정리 및 롤백 경계 설정하기

캐시 상한은 무제한으로 늘리는 대신 네이티브 대상의 규모에 맞춰 정해야 합니다. ccache --cleanup을 정기적으로 실행하고 디렉터리 크기, 제거 수, 쓰기 실패를 모니터링합니다. 여러 동시 작업이 같은 디렉터리를 공유할 수는 있지만, 기반 디렉터리는 신뢰할 수 있는 로컬 파일 시스템에 있어야 합니다. 불안정한 원격 마운트를 사용하면 메타데이터 접근에 시간이 소모되어 절약한 컴파일 시간이 상쇄될 수 있습니다.

잘못된 재사용이 의심되면 먼저 로그와 캐시 통계를 보존한 뒤 CCACHE_DISABLE=1을 설정해 같은 명령을 다시 실행합니다. 문제가 사라진다면 툴체인, 대상, 구성에 따라 캐시 범위를 좁히세요. 검증을 비활성화하거나 타임스탬프 판정을 완화하거나 헤더 파일 변경을 무시하는 방식을 장기적인 해결책으로 사용해서는 안 됩니다.

최종 검증 기준은 보기 좋은 특정 적중률이 아니라 다음 세 조건이 동시에 충족되는지 여부입니다. 산출물이 캐시를 비활성화했을 때와 동일하고, 두 번째 빌드에서 네이티브 컴파일이 크게 줄어들며, 툴체인이나 입력 변경으로 캐시 무효화를 설명할 수 있어야 합니다. 이 세 조건을 모두 만족해야 ccache를 유지 관리 가능한 빌드 인프라 계층으로 사용할 수 있습니다.

자주 묻는 질문

ccache가 Swift 컴파일도 직접 가속하나요?

아닙니다. 이 구성에서 ccache는 Clang을 감싸므로 Objective-C, Objective-C++, C, C++ 컴파일 단위에 적용됩니다. Swift는 안정적인 DerivedData와 적절한 동시성 설정이 필요합니다.

두 번째 빌드에서도 캐시 적중이 없는 이유는 무엇인가요?

먼저 Xcode가 실제로 래퍼를 호출하는지 확인합니다. 이후 작업 경로, 컴파일 옵션, 생성 헤더, 빌드마다 달라지는 매크로를 비교하면 캐시 키가 변한 원인을 찾을 수 있습니다.

CI에서는 DerivedData와 ccache 중 무엇을 보존해야 하나요?

우선 ccache 디렉터리를 독립적으로 보존하는 편이 안전합니다. DerivedData는 Xcode 버전, 절대 경로, SDK와 프로젝트 그래프 상태에 더 강하게 결합됩니다.

다음 빌드를 위한 전용 물리 Mac

개발, 지속적 통합 또는 로컬 모델 테스트에 적합한 세 가지 고정 구성 중 하나를 선택하고, 작업 기간에 맞춰 클라우드 Mac을 구성하세요.

클라우드 Mac 구성하기