Vuncloud 블로그
← 개발 노트로 돌아가기

Xcode 캐시 공유 실전: 다중 노드 빌드 데이터 동기화 팁

DerivedData · SPM · CocoaPods · 멀티 Runner 동기화약 13분

데이터 분석 대시보드——다중 노드 Xcode 빌드 캐시 모니터링과 동기화 효율을 상징
TL;DR · 세 줄 요약
  • 한 대 Mac의 DerivedData 캐시는 두 번째 Runner에는 기본적으로 없다——다중 노드 iOS CI의 숨은 비용은 각 머신이 따로 콜드 스타트하는 것
  • 공유할 수 있는 것은 빌드 산출물 레이어(DerivedData, SPM 해석 결과, Pods/)이지, 「여러 대가 같은 디렉터리에 동시 쓰기」가 아니다. pull → build → push 또는 리전별 캐시 허브가 2026년 주류
  • Cloud Mac fleet에서는 lockfile 해시 + Xcode 버전을 캐시 키로, rsync/오브젝트 스토리지와 조합——warm 빌드로 추가 5–15분 절약이 흔하다(단일 머신 캐시 최적화 위에)

GitHub Actions actions/cache를 설정해 warm 빌드가 18분에서 9분으로 줄었다——그다음 셀프호스트 M4 Runner를 3대로 늘리니 P95가 다시 16분. 모든 머신이 「이 commit을 처음 본다」는 상태다.

이건 원점 회귀가 아니라 다중 노드 빌드의 2단계: Xcode 캐시 공유다. 이 글은 「왜 캐시인가」( CocoaPods / SPM / DerivedData 단일 머신 가이드 참고)가 아니라, 여러 Mac 사이에서 빌드 데이터를 동기화하는 방법을 다룬다——어떤 Runner든 팀원이 이미 컴파일한 모듈을 재사용하게 하는 것.

3
캐시 레이어: 의존성 · 빌드 산출물 · 인덱스
5–15분
다중 노드 warm 빌드에서 흔한 추가 절약
1
철칙: 동일 DerivedData에 동시 쓰기 금지

1. 다중 노드 CI가 단일 머신보다 느린 이유

로드 밸런서가 job을 Runner A/B/C에 무작위로 배정하면, 각 머신의 디스크 상태는 독립적이다. Runner A가 방금 컴파일한 MyAppKit.swiftmodule은 Runner B에게 무의미하다——산출물을 옮기지 않는 한.

GitHub 호스트 runner는 actions/cache로 tarball을 클라우드에 저장하고 job 시작 시 restore한다. 셀프호스트나 Cloud Mac fleet에는 이에 상응하는 관리형 레이어가 없는 경우가 많아, 팀은 다음 중 하나를 택한다:

  • 머신별 warming을 받아들인다(연산 낭비)
  • 캐시 디렉터리를 NFS에 마운트해 여러 대가 동시 쓰기(손상 위험)
  • 캐시 허브 구축: 빌드 전 pull, 성공 후 push

세 번째가 iOS 팀에서 가장 흔하고 다중 리전 노드 배포에도 맞다——미동부 M4 3대가 하나의 캐시 버킷을 공유하고, APAC 2대가 다른 버킷을 공유하며, 리전 간에는 SPM/Pods 레이어만 동기화한다.

2. Xcode 빌드 데이터 레이어

동기화 전에 「옮길 가치가 있는 것」과 「섞으면 안 되는 것」을 구분한다.

2.1 DerivedData

xcodebuild -derivedDataPath가 가리키는 디렉터리. 컴파일된 .o, .swiftmodule, 링크 중간 산출물, 부분 인덱스를 포함한다. 용량이 수 GB에 달하기도 하며, warm 빌드 가속의 최대 레버이자 Xcode 버전에 가장 민감한 레이다.

CI에서는 경로를 고정한다. 예: /Volumes/CI/DerivedData/<scheme>. 동기화 스크립트의 REMOTE 변수와 일치시킨다——단일 머신 캐시 글에서 강조했듯, 경로 불일치는 캐시 없음과 같다.

2.2 SPM과 CocoaPods

SPM: ~/Library/Caches/org.swift.swiftpm + 저장소 내 .build(해석 결과를 커밋한 경우). CocoaPods: Pods/~/Library/Caches/CocoaPods.

이 레이어는 DerivedData보다 변경 빈도가 낮다(lockfile을 따름). 머신 간, 심지어 리전 간 동기화에 적합하다. 많은 팀은 SPM/Pods만 S3에 push하고 DerivedData는 리전 내 rsync에 둔다.

2.3 ModuleCache와 Xcode 전역 캐시

~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex와 각종 SDKStatCaches. 보통 DerivedData와 함께 처리한다. 별도 동기화 시 동일 Xcode 마이너 버전을 강제한다.

MacBook의 코드 에디터——여러 Runner가 동일한 Xcode 컴파일 캐시를 공유하는 모습
공유하는 것은 「컴파일된 모듈」이지 「모든 머신에서 연 프로젝트」가 아니다——각 Runner는 로컬 DerivedData 복사본으로 빌드한다

3. 네 가지 다중 노드 동기화 아키텍처

패턴방식장점리스크
로컬 전용각 Runner가 독립 디스크, 공유 없음운영 부담 제로수평 확장해도 시간 절약 없음
NFS 공유 마운트DerivedData를 네트워크 볼륨에, 읽기 전용 또는 읽기/쓰기설정 간단동시 쓰기로 손상 쉬움; 네트워크 지연이 링크를 느리게 함
rsync 허브중앙 디렉터리 또는 leader 머신; 빌드 전 pull, 성공 후 push제어 가능, Xcode 호환성 양호스크립트와 잠금 필요
오브젝트 스토리지 tarball캐시 키로 S3/R2에 패킹 업로드; job 시작 시 다운로드·압축 해제크로스 리전, GHA cache 모델과 일치큰 DerivedData 업로드 느림; 압축 필요

2026년 실무에서는 동일 데이터센터 / 동일 리전 Cloud Mac fleet은 rsync 허브를 우선하고, 글로벌 팀은 오브젝트 스토리지로 SPM/Pods를 동기화하며 DerivedData는 리전 내에 둔다. NFS를 「다중 라이터 공유 DerivedData」의 만능책으로 보지 말 것.

철칙

두 Runner가 동시에 같은 DerivedData 트리에 xcodebuild를 실행해서는 안 된다. 볼륨 공유가 필요하면 파일 잠금 또는 단일 라이터·다중 리더(지정 warmer가 빌드 후 rsync로 배포)를 쓴다.

4. 캐시 키와 무효화

다중 노드 공유 캐시 키는 단일 머신보다 보수적으로——잘못된 히트는 콜드 스타트보다 나쁘다(미스터리 링크 오류, 서명 실패).

키에 포함할 필드:

  • xcodebuild -version 또는 XCODE_VERSION 환경 변수
  • hashFiles('**/Podfile.lock') 또는 Package.resolved
  • Scheme 이름 또는 타깃 집합(다중 scheme 저장소는 캐시 분리)
  • arm64(Apple Silicon 전용 접두사로 레거시 x86 오염 방지)

전체 github.sha를 유일한 키로 쓰지 말 것——그러면 항상 miss다. 계층적 restore-keys를 쓴다: dd-arm64-main-<lockhash>dd-arm64-main-dd-arm64-.

무효화 시점: Xcode 업그레이드, lockfile 변경, SWIFT_VERSION 또는 주요 Build Settings 변경, 수동 clean build 후——키 접미사를 bump하거나 원격 디렉터리를 삭제한다.

5. 실습: rsync pull/push 스크립트

아래는 M4 Cloud Mac fleet에서 검증된 최소 스크립트다. 캐시 허브는 cache-leader.internal:/cache/ios/(fleet 내 대용량 디스크 머신 또는 NAS).

#!/usr/bin/env bash
# cache-sync.sh — 在 xcodebuild 前后调用
set -euo pipefail

ACTION="${1:?pull|push}"
CACHE_ROOT="${CACHE_ROOT:-cache-leader.internal:/cache/ios}"
SCHEME="${SCHEME:-MyApp}"
KEY="${CACHE_KEY:-arm64-$(md5 -q Podfile.lock 2>/dev/null || echo nolock)-$(xcodebuild -version | head -1 | tr ' ' '-')}"
LOCAL_DD="${DERIVED_DATA_PATH:-/Volumes/CI/DerivedData/${SCHEME}}"
REMOTE="${CACHE_ROOT}/${KEY}/${SCHEME}/DerivedData"
LOCAL_SPM="${HOME}/Library/Caches/org.swift.swiftpm"
REMOTE_SPM="${CACHE_ROOT}/${KEY}/swiftpm"

rsync_opts=(-az --delete-delay --contimeout=10)

case "$ACTION" in
  pull)
    rsync "${rsync_opts[@]}" "${REMOTE}/" "${LOCAL_DD}/" || true
    rsync "${rsync_opts[@]}" "${REMOTE_SPM}/" "${LOCAL_SPM}/" || true
    ;;
  push)
    # 仅成功构建后调用
    rsync "${rsync_opts[@]}" "${LOCAL_DD}/" "${REMOTE}/"
    rsync "${rsync_opts[@]}" "${LOCAL_SPM}/" "${REMOTE_SPM}/"
    ;;
  *) echo "usage: $0 pull|push" >&2; exit 1 ;;
esac

CI 흐름:

  1. job 시작 → cache-sync.sh pull
  2. xcodebuild -scheme "$SCHEME" -derivedDataPath "$LOCAL_DD" build
  3. 빌드 성공 시에만 → cache-sync.sh push

여러 머신이 병렬일 때 push는 경합할 수 있다——flock을 쓰거나 last-write-wins를 받아들인다. 동일 lockfile + 동일 Xcode 하의 DerivedData는 호환되어야 한다. push가 매우 잦으면 비동기 업로드(빌드 후 백그라운드 rsync, 파이프라인 블로킹 없음)로 전환한다.

6. 크로스 리전 Cloud Mac 캐시 허브

Runner가 미동부, 미서부, APAC에 분산되면, 전체 DerivedData의 대양 횡단 rsync는 종종 비용 대비 효과가 없다(지연 + egress). 권장 접근:

  • 리전당 하나의 캐시 버킷(S3 / R2 / 제공자 오브젝트 스토리지)
  • 동기화 내용: Podfile.lock 해시 키의 Pods/ tarball + Package.resolved 키의 SPM 캐시
  • DerivedData는 리전 내에서만 rsync; 첫 job은 콜드 스타트, 이후 warm
  • 이미지 버전을 골든 이미지 태그와 맞춰 Xcode 드리프트로 전체 라이브러리가 무효화되는 것을 방지

업로드 전 SPM 캐시를 tar czf로 압축——40%–60% 작아지는 경우가 많다. DerivedData는 이미 수많은 작은 파일이므로, 크로스 리전 맨 rsync보다 tar + 병렬 업로드가 더 안정적이다.

7. GitHub Actions / 셀프호스트 Runner 연결

셀프호스트 runner에서는 workflow를 이렇게 감싼다:

- name: Restore shared Xcode cache
  run: ./scripts/cache-sync.sh pull
  env:
    CACHE_ROOT: ${{ secrets.IOS_CACHE_SSH }}
    SCHEME: MyApp
    DERIVED_DATA_PATH: /Volumes/CI/DerivedData/MyApp

- name: Build
  run: xcodebuild -scheme MyApp -derivedDataPath /Volumes/CI/DerivedData/MyApp build

- name: Save shared Xcode cache
  if: success()
  run: ./scripts/cache-sync.sh push

GitHub 호스트 actions/cache를 쓰는 경우, 셀프호스트 fleet은 이중 트랙 가능: GHA cache를 폴백, rsync 허브를 동일 리전 저지연 레이어로. 두 키 네임스페이스는 분리해 상호 덮어쓰기를 피한다.

모니터링: pull/push 소요 시간, 전송 바이트, 빌드가 증분인지(xcodebuild 로그의 CompileSwift 줄 수)를 기록한다. P95가 느려지면 먼저 캐시 miss, 다음 머신 부하를 확인——빌드 시간 불안정 트러블슈팅과 같은 플레이북이다.

8. 함정 체크리스트

  • DerivedData 동시 쓰기: 무작위 stat cache file corrupted → pull/push로 전환
  • Xcode 마이너 버전 불일치: 모듈 ABI 불일치 → 키에 버전 포함, 이미지는 통일 태그
  • 경로 불일치: 캐시는 경로 A, 컴파일은 경로 B → 전체 재빌드
  • clean 후 push: 빈 디렉터리가 좋은 캐시를 덮음 → 성공 시에만 push, clean 후에는 push 안 함
  • 서명된 산출물을 캐시에 포함: Provisioning 변경 후 오래된 서명 → Build/Products를 캐시에서 제외하거나 설정별 키 분리
  • 디스크 가득 참: DerivedData 팽창 → 원격 ${KEY} 디렉터리를 주기적으로 LRU 정리, 최근 N개 lockhash 유지

9. FAQ

여러 Mac이 같은 DerivedData 디렉터리를 직접 공유할 수 있나?

동시 쓰기는 안 된다. 읽기 전용 마운트나 단일 라이터/다중 리더는 가능하지만, 링크 단계에서 잠금에 걸릴 수 있다. 프로덕션에서는 로컬 DerivedData + rsync를 우선한다.

Xcode 캐시 공유에 동일 Xcode 버전이 필요한가?

그렇다. Xcode 업그레이드 후에는 새 캐시 키를 쓰고, 이전 디렉터리는 비동기로 삭제할 수 있다.

Bazel / Tuist 원격 캐시와 비교하면?

Bazel/Tuist 원격 캐시는 더 세밀한 단위(action 수준)이며 거대 monorepo에 적합하다. 표준 Xcode 프로젝트는 DerivedData 동기화의 이전 비용이 낮고 기존 xcodebuild 흐름과 호환된다.

여러 Cloud Mac에 디스크가 얼마나 필요한가?

각 머신에 DerivedData + SPM용으로 로컬 최소 50–100GB 확보. 허브 또는 오브젝트 스토리지는 「병행 scheme 수 × scheme당 캐시 크기 × 보존 버전 수」로 산정한다. M4 + 2TB 확장은 중형 팀에 보통 충분하다.

결론

Xcode 캐시 공유가 푸는 것은 다중 Runner 시대의 「두 번째 콜드 스타트」——단일 머신 캐시 최적화 이후, 수평 확장해도 전액 컴파일 세금을 다시 내지 않아야 한다. 세 레이어 분리(Pods / SPM / DerivedData), 하나의 동기화 규율(pull-build-push), 하나의 철칙(DerivedData 동시 쓰기 금지)을 기억하라.

캐시는 분산 빌드에서 가장 저렴한 연산 배율기——M4 두 대를 더 사는 것보다 ROI가 빠르다.

cache leader 한 대와 cache-sync.sh부터 시작한다. 원격 디렉터리는 lockfile 해시로 이름 짓고, 빌드가 green일 때만 push한다. 다음 주 P95 곡선이 가치를 알려줄 것이다.

다중 노드 CI용 디스크 여유 Cloud Mac

Vuncloud Mac mini M4: 미동부·미서부·APAC SSH 즉시 이용——대용량 DerivedData, rsync 캐시 허브, 셀프호스트 Runner fleet에 적합.

Cloud Mac 요금제 보기 · M4 CI Runner 비용 모델

Apple 툴체인 동작은 공식 릴리스를 따른다. SSH와 오브젝트 스토리지 자격 증명은 팀 보안 정책에 맞게 감사할 것. 최종 업데이트: 2026년 7월 27일.

개발 노트 · 빌드 가속

DerivedData 동기화 · 멀티 Runner · Cloud Mac

rsync 허브 · cache key · 리전 캐시 버킷

Cloud Mac 플랜 보기
한정 혜택 플랜 보기