겉보기에는 평범한 Core Data 필드 변경도 새로 설치한 시뮬레이터에서는 아무 문제 없이 동작할 수 있습니다. 하지만 이전 버전에서 곧바로 업그레이드한 사용자는 시작 화면에서 멈출 수 있습니다. CI에 실제로 포함해야 할 검증은 “빈 데이터베이스를 생성할 수 있는가”가 아닙니다. 기존 데이터베이스가 지원되는 경로를 따라 마이그레이션되는지, 핵심 레코드의 일관성이 유지되는지, 실패했을 때 원인을 분석할 증거가 충분히 남는지를 확인해야 합니다. 상시 실행되는 클라우드 Mac은 시간이 다소 걸리더라도 결과가 결정적인 이런 검사를 독립적인 게이트로 운영하기에 적합합니다.
먼저 마이그레이션 지원 범위 정의하기
현재 남아 있을 가능성이 있는 데이터베이스 버전부터 모두 정리해야 합니다. 사용자가 모든 중간 버전을 순서대로 설치한다고 가정해서는 안 됩니다. 현재 모델이 V5이고 V3, V4 클라이언트가 아직 운영 환경에 남아 있다면 최소한 V3→V5와 V4→V5를 테스트해야 합니다. 빈 V5 데이터베이스 테스트로 확인할 수 있는 것은 현재 모델이 정상적으로 로드된다는 사실뿐입니다. 이전 필드의 이름 변경, 관계 제약 조건의 변경, 고유 인덱스 충돌은 검증할 수 없습니다.
다음과 같은 간단한 매트릭스를 관리하는 것이 좋습니다.
| 샘플 | 업그레이드 대상 | 필수 검증 항목 |
|---|---|---|
| V3 기본 데이터베이스 | V5 | 계정, 프로젝트, 과거 작업 수 |
| V4 경계 조건 데이터베이스 | V5 | 빈 관계, 중복 이름, 삭제 표시 |
| V4 대규모 샘플 | V5 | 마이그레이션 완료 시간과 최대 파일 크기 |
| V5 빈 데이터베이스 | V5 | 현재 모델 초기화 |
마이그레이션 게이트의 통과 조건은 단순히 영구 저장소 컨테이너에서 오류가 발생하지 않는 것이 아니라, 데이터의 의미가 그대로 유지되는 것이어야 합니다.
각 샘플에는 이를 생성한 앱 버전, 모델 식별자, 예상 레코드 수, 검증 요약도 기록해야 합니다. 개발자가 일상적으로 사용하는 데이터베이스에서 임시로 샘플을 가져오지 마십시오. 작업에 따라 샘플이 계속 바뀌므로 실패를 재현하기 어려워집니다.
재현 가능한 이전 데이터베이스 샘플 고정하기
과거 버전마다 전용 빌드를 준비하고 정해진 데이터를 기록하도록 구성합니다. 정상 레코드뿐 아니라 null 값, 긴 텍스트, 보관 처리된 객체, 제약 조건의 경계에 가까운 객체도 포함해야 합니다. 데이터 기록이 끝나면 영구 저장소 컨테이너를 정상적으로 종료한 뒤 데이터베이스를 복사합니다.
SQLite에서 WAL을 사용하면 데이터가 세 파일에 나뉘어 있을 수 있습니다. 가장 안전한 방법은 앱에서 저장을 완료하고 checkpoint를 실행하는 것입니다. 이를 보장할 수 없다면 관련 파일을 하나의 파일 그룹으로 함께 보관해야 합니다.
set -euo pipefail
STORE="$HOME/Library/Developer/CoreSimulator/Devices/$SIM_UDID/data/Containers/Data/Application/$APP_ID/Library/Application Support/App.sqlite"
OUT="MigrationFixtures/V4"
mkdir -p "$OUT"
cp "$STORE" "$OUT/App.sqlite"
for suffix in -wal -shm; do
if [ -f "${STORE}${suffix}" ]; then
cp "${STORE}${suffix}" "$OUT/App.sqlite${suffix}"
fi
done
find "$OUT" -type f -print0 | sort -z | xargs -0 shasum -a 256 > "$OUT/SHA256SUMS"
테스트 중에는 저장소에 있는 원본 샘플을 직접 열지 마십시오. 각 테스트는 먼저 샘플을 임시 디렉터리로 복사하고, 마이그레이션이 끝나면 삭제해야 합니다. 그래야 병렬 작업이 서로의 파일을 덮어쓰지 않습니다. 샘플에 실제 사용자 콘텐츠, 토큰 또는 연결 정보가 들어 있다면 사후 비식별화에 의존하지 말고 익명 데이터로 다시 만들어야 합니다.
마이그레이션을 테스트 가능한 진입점으로 캡슐화하기
앱 시작 과정에서는 데이터베이스 로드, UI 초기화, 네트워크 요청이 하나로 묶이는 경우가 많습니다. 이런 구조에서는 마이그레이션 실패 원인을 파악하기 어렵습니다. 영구 저장소 컨테이너 생성을 URL 주입이 가능한 컴포넌트로 캡슐화하여 XCTest가 임시 복사본을 직접 로드할 수 있게 해야 합니다.
func makeContainer(storeURL: URL) throws -> NSPersistentContainer {
let container = NSPersistentContainer(name: "AppModel")
let description = NSPersistentStoreDescription(url: storeURL)
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
container.persistentStoreDescriptions = [description]
var loadError: Error?
container.loadPersistentStores { _, error in
loadError = error
}
if let loadError {
throw loadError
}
return container
}
경량 마이그레이션은 선택적 필드 추가, 기본값이 있는 속성 추가, 명시적인 이름 변경에 적합합니다. 엔터티 분할, 데이터 병합, 값 변환이 필요하다면 명시적 매핑이나 단계별 마이그레이션을 제공해야 합니다. 테스트를 통과시키기 위해 이전 모델을 삭제해서는 안 됩니다. 과거 모델은 기존 데이터베이스의 메타데이터를 읽고 마이그레이션 경로를 추론하는 데 필요한 입력입니다.
비즈니스 불변 조건 검증하기
마이그레이션 후에는 최소한 네 가지 계층을 확인해야 합니다. 영구 저장소가 정상적으로 로드되는지, 모델 버전이 업데이트되었는지, 레코드 수가 예상과 일치하는지, 핵심 관계와 필드의 의미가 올바른지 검증합니다. 예를 들어 전체 작업 수는 바뀌면 안 되고, 보관 처리된 프로젝트는 계속 보관 상태여야 하며, 고립된 하위 객체 수는 0이어야 합니다.
비교할 때 자동 생성된 객체 ID에만 의존하지 마십시오. 테스트 데이터에 안정적인 비즈니스 키를 기록하고, 해당 키를 기준으로 필드를 검증하는 것이 좋습니다. 정렬 관계나 컬렉션 관계는 먼저 정규화한 뒤 비교해야 의미 없는 순서 차이로 인한 간헐적 실패를 방지할 수 있습니다.
클라우드 Mac에서 독립 테스트 작업 실행하기
마이그레이션 테스트는 샘플 복사와 영구 저장소의 반복 생성이 필요하므로 일반 단위 테스트와 분리해서 실행해야 합니다. 시뮬레이터 런타임과 대상 기기 이름을 고정하고, 먼저 기기를 부팅한 다음 지정한 테스트 플랜을 실행합니다.
set -euo pipefail
xcrun simctl bootstatus "$SIM_UDID" -b
xcodebuild test \
-workspace App.xcworkspace \
-scheme App \
-testPlan DatabaseMigration \
-destination "platform=iOS Simulator,id=$SIM_UDID" \
-resultBundlePath Artifacts/MigrationTests.xcresult \
CODE_SIGNING_ALLOWED=NO
테스트 작업을 시작하기 전에 임시 디렉터리를 정리합니다. 작업이 성공하든 실패하든 종료 후에는 xcresult, 마이그레이션 로그, 샘플 요약, 대상 모델 식별자를 보관해야 합니다. 마이그레이션이 끝난 전체 데이터베이스를 기본 아티팩트로 업로드하지 마십시오. 테스트용 비밀 정보가 들어 있을 수 있고 저장 공간도 빠르게 증가합니다. 일반적으로 실패한 경우에만 익명화된 샘플 복사본을 보관하고 명확한 삭제 주기를 설정합니다.
ZoomMini에서 실행할 때는 작업을 고정된 Xcode 툴체인에 연결하고 콘솔에서 현재 선택 가능한 구성을 확인합니다. Xcode나 시뮬레이터 런타임을 변경한 뒤에는 먼저 마이그레이션 플랜만 단독으로 실행하고, 그다음 전체 파이프라인을 복원합니다. 툴체인 변경과 모델 변경이 동시에 반영되면 실패 원인을 구분하기 어려워집니다.
자주 발생하는 실패와 릴리스 점검 항목
“로컬에서는 통과하지만 CI에서는 실패하는” 문제는 대개 파일 그룹을 완전히 복사하지 않았거나, 이전 테스트가 샘플을 수정했거나, 모델 리소스가 테스트 번들에 포함되지 않았거나, 여러 테스트가 동시에 같은 경로를 사용해서 발생합니다. 먼저 SHA-256을 확인하고, 임시 디렉터리가 작업마다 고유한지 점검한 뒤, 빌드 산출물에 지원되는 모든 모델 버전이 포함되어 있는지 확인합니다.
릴리스 전에 다음 항목을 하나씩 확인합니다.
- 지원되는 각 시작 버전에 고정 샘플과 예상 요약이 있다.
- SQLite 주 파일과 WAL 상태가 일치한다.
- 테스트는 임시 복사본만 마이그레이션하며 원본 fixture를 수정하지 않는다.
- 경량 마이그레이션과 명시적 매핑의 적용 범위가 문서화되어 있다.
- 레코드 수, 안정적인 비즈니스 키, 관계, 기본값에 대한 assertion이 모두 있다.
- 실패한 작업은
xcresult와 비식별화된 마이그레이션 로그를 보관한다. - 모델 버전 삭제, 필드 이름 변경, 제약 조건 변경 시 마이그레이션 플랜이 반드시 실행된다.
- 전체 테스트는 지원되는 가장 오래된 버전에서 현재 버전으로 직접 업그레이드한다.
데이터베이스 마이그레이션은 일회성 릴리스 스크립트가 아니라 모델이 발전할수록 함께 확장되는 호환성 계약입니다. 과거 샘플, 비즈니스 불변 조건, 실패 증거를 고정해 두면 모델을 변경할 때마다 병합 전에 같은 질문에 답할 수 있습니다. 기존 데이터가 새 버전에 안전하게 도달할 수 있는가?
자주 묻는 질문
마이그레이션 테스트에는 몇 개의 이전 버전이 필요합니까?
최소한 현재 배포 버전과 아직 직접 업그레이드될 수 있는 직전 버전을 포함해야 합니다. 장기 미접속 사용자를 지원한다면 허용된 모든 시작 버전을 추가해야 합니다.
SQLite 파일 하나만 복사하면 왜 문제가 됩니까?
WAL 모드에서는 커밋된 변경이 -wal 파일에 남을 수 있습니다. 안전하게 checkpoint하지 않았다면 sqlite, sqlite-wal, sqlite-shm 파일을 한 세트로 보존해야 합니다.
빌드, 테스트 및 실험에 사용할 독점 물리 노드 선택
두 가지 M4 구성이 싱가포르, 도쿄, 서울 및 홍콩 노드를 지원합니다. 실제 이용 가능 여부는 주문 시 반환되는 결과를 기준으로 합니다.