동일한 클라우드 Mac에서 Xcode 파이프라인 두 개를 동시에 실행할 때 간헐적으로 발생하는 database is locked, 인덱스 데이터베이스 손상, 또는 “로컬에서는 재현되지 않는” 링크 오류는 코드 변경 때문이 아닌 경우가 많습니다. 먼저 두 작업이 기본 경로인 ~/Library/Developer/Xcode/DerivedData를 공유하는지 확인해야 합니다. 이 디렉터리에는 단순한 캐시뿐 아니라 빌드 데이터베이스, 인덱스, 모듈 캐시, 작업 중 계속 갱신되는 중간 산출물도 들어 있습니다. 이를 동시 읽기·쓰기가 가능한 공용 캐시처럼 사용하면 실패는 시간문제입니다.
먼저 충돌이 발생하는 위치 확인하기
실패했다고 해서 즉시 모든 캐시를 삭제해서는 안 됩니다. 먼저 실패한 작업의 전체 xcodebuild 명령, 시작 시간, 작업 디렉터리, 프로세스 목록을 보관한 다음 로그에서 locked, database, unable to attach, malformed, 중복 산출물 관련 메시지를 검색합니다.
pgrep -alf 'xcodebuild|XCBuildService|swift-frontend'
find "$HOME/Library/Developer/Xcode/DerivedData" -name build.db -print
grep -Eini 'locked|database|malformed|multiple commands produce' build.log
두 작업의 -derivedDataPath가 같거나 둘 다 이 매개변수를 명시적으로 설정하지 않았다면 가장 우선적으로 확인해야 할 위험을 찾은 것입니다. 파이프라인이 시간 초과된 뒤 외부 스크립트만 종료되고 xcodebuild 또는 하위 컴파일 프로세스가 남아 디렉터리에 계속 쓰고 있지는 않은지도 확인해야 합니다.
재시도에 성공했다고 해서 문제가 해결된 것은 아닙니다. 경쟁이 발생하는 시간 구간이 짧아지면서 두 번째 빌드가 우연히 통과할 수 있지만, 공유 쓰기 관계는 그대로 남아 있습니다.
잠금 충돌과 일반적인 컴파일 실패 구분하기
소스 컴파일 오류는 일반적으로 같은 파일과 같은 줄에서 일관되게 발생합니다. 반면 잠금 충돌은 동시 실행 순서에 따라 달라지며, 의존성 분석, 모듈 생성, 링크 단계에서 불규칙하게 나타나는 경우가 많습니다. 병렬 작업을 중지하면 연속으로 성공하고 다시 병렬 실행했을 때 실패한다면, 비즈니스 코드를 수정하기보다 디렉터리 소유권부터 점검해야 합니다.
작업마다 독립된 작업 공간 할당하기
안정적인 방법은 저장소 체크아웃 디렉터리, DerivedData, 결과 번들, 임시 디렉터리에 모두 작업별 고유 식별자를 포함하는 것입니다. 식별자로 파이프라인 작업 번호를 사용할 수 있으며, 로컬 검증에서는 프로세스 번호를 대신 사용할 수 있습니다.
set -euo pipefail
JOB_KEY="${CI_JOB_ID:-local-$$}"
ROOT="${RUNNER_TEMP:-$HOME/ci-work}/$JOB_KEY"
DERIVED_DATA="$ROOT/DerivedData"
RESULT_BUNDLE="$ROOT/TestResults.xcresult"
mkdir -p "$DERIVED_DATA"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-derivedDataPath "$DERIVED_DATA" \
-resultBundlePath "$RESULT_BUNDLE" \
build-for-testing
test -d "$RESULT_BUNDLE"
경로는 해당 작업이 직접 생성해야 하며, 삭제도 그 작업만 수행해야 합니다. 프로젝트 이름만 고유 디렉터리 이름으로 사용해서는 안 됩니다. 같은 프로젝트의 서로 다른 두 브랜치도 충돌할 수 있기 때문입니다. 정리 스크립트에서도 이름에 App이 포함된 DerivedData를 모두 삭제하는 식의 모호한 일치를 사용하면 안 됩니다.
의존성 해석과 컴파일 분리하기
DerivedData를 격리한 뒤에도 여러 작업이 동일한 의존성 체크아웃 디렉터리를 동시에 수정하면 문제가 패키지 해석 단계로 이동합니다. 먼저 의존성 해석을 수행한 뒤, 실제 빌드에서는 의존성 버전이 자동으로 변경되지 않도록 해야 합니다. 저장소에는 검토가 완료된 잠금 파일을 커밋해야 하며, 파이프라인이 이를 조용히 업데이트해서는 안 됩니다.
PACKAGES="$ROOT/SourcePackages"
xcodebuild \
-resolvePackageDependencies \
-workspace App.xcworkspace \
-scheme App \
-clonedSourcePackagesDirPath "$PACKAGES"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-derivedDataPath "$DERIVED_DATA" \
-clonedSourcePackagesDirPath "$PACKAGES" \
-disableAutomaticPackageResolution \
build
캐시가 필요하다면 여러 작업이 하나의 활성 디렉터리에 쓰도록 하지 말고, 다운로드 결과나 검증된 불변 스냅샷을 캐시해야 합니다. 캐시 키에는 최소한 잠금 파일 해시, Xcode 버전, 대상 아키텍처가 포함되어야 합니다. 캐시가 적중한 뒤에도 잠금 파일이 변경되지 않았는지 확인해야 합니다.
판정 가능한 검증표 만들기
수정 여부를 한 번의 성공 결과만으로 판단해서는 안 됩니다. 동일한 커밋으로 여러 차례 작업을 동시에 시작하고 디렉터리, 프로세스, 산출물의 경계를 확인해야 합니다.
완전히 동일한 빌드 두 개를 최소한 동시에 실행한 다음, 서로 다른 브랜치의 빌드도 한 세트 실행하는 것이 좋습니다. 전자는 쓰기 격리를 검증하고, 후자는 작업 디렉터리, 모듈 캐시, 산출물 경로가 여전히 프로젝트 이름을 기준으로 공유되는 문제를 찾아내는 데 사용합니다.
증거를 보존하고 안전하게 회수하기
실패한 작업의 현장을 즉시 삭제해서는 안 됩니다. 먼저 빌드 로그, 결과 번들, Xcode 버전, 잠금 파일 해시, 디스크 여유 공간, 작업 경로를 보관해야 합니다. 소스 코드와 환경 변수, 로그에 포함된 토큰은 사전에 마스킹해야 합니다. 성공한 작업은 산출물이 독립된 배포 디렉터리로 복사되었는지 확인한 뒤 정리할 수 있습니다.
정리 작업은 작업 루트 디렉터리에 연결하고 경로 접두사 검증을 추가하는 것이 좋습니다. 작업이 강제로 종료된 경우 후속 회수 프로그램은 작업 식별자를 기준으로 남은 디렉터리를 처리해야 하며, 시스템 전체의 개발자 디렉터리를 비워서는 안 됩니다. NUMACS에서 여러 파이프라인을 실행할 때도 콘솔에서 현재 선택 가능한 구성을 확인한 뒤 동시 실행 수에 맞춰 작업 상한을 설정해야 합니다. 동시 실행 수를 높이는 것으로 쓰기 격리를 대신할 수는 없습니다.
최종 목표는 오류가 “덜 발생하게” 만드는 것이 아니라 명확한 불변 조건을 세우는 것입니다. 각 병렬 작업에는 쓰기 주체가 하나만 있어야 하고, 빌드 중에는 의존성 버전이 변하지 않아야 하며, 실패 현장을 추적할 수 있어야 하고, 정리 작업이 다른 작업에 영향을 주지 않아야 합니다. 이 네 가지 조건을 충족해야 DerivedData가 무작위 장애의 원인에서 관리 가능한 빌드 데이터로 돌아갈 수 있습니다.
자주 묻는 질문
병렬 작업마다 별도 DerivedData가 필요한가요?
필요합니다. 동시에 쓰는 모든 작업은 -derivedDataPath로 고유 경로를 받아야 하며, 같은 경로는 완전히 직렬화된 작업에서만 재사용해야 합니다.
경로를 격리하면 캐시 효과가 모두 사라지나요?
아닙니다. 다운로드한 의존성과 검증된 읽기 전용 산출물은 별도로 재사용하고, 변경되는 빌드 데이터만 작업별로 격리할 수 있습니다.
database is locked 오류는 재시도로 해결해도 되나요?
재시도만으로는 경쟁 조건이 사라지지 않습니다. 공유 쓰기 경로를 제거하고 남은 xcodebuild 프로세스가 없는지 먼저 확인해야 합니다.
NUMACS 클라우드 Mac
빌드 작업을 전용 물리 워크스테이션으로 옮기세요
두 가지 Apple Silicon 구성을 모두 전용 물리 머신으로 제공하며 가상 머신이 아닙니다. 일·주·월·분기 단위로 대여할 수 있고 싱가포르, 일본 도쿄, 한국 서울, 홍콩 노드를 지원합니다.