한 뉴스 앱이 업데이트 후 다시 다운로드할 수 있는 수백 MB의 오프라인 리소스를 Application Support에 저장하기 시작했습니다. 기능 테스트는 모두 통과했고 사용자 데이터도 유실되지 않았지만, 백업 용량이 갑자기 증가했습니다. 문제는 다운로드 로직이 아니라, 팀에서 “반드시 복원해야 하는 파일”과 “다시 생성할 수 있는 파일”을 실행 가능한 규칙으로 정의하지 않았다는 데 있었습니다. 클라우드 Mac은 이러한 회귀 검사를 수행하기에 적합합니다. 환경을 장기간 일관되게 유지하고 시뮬레이터를 초기화할 수 있으며, 실패 시 컨테이너 목록을 보존해 원인을 조사할 수도 있습니다.
먼저 데이터 복원 의미 정의하기
처음부터 디렉터리를 기준으로 테스트를 작성하지 말고, 데이터 용도에 따라 분류해야 합니다. 같은 JSON 파일이라도 사용자가 만든 콘텐츠일 수도 있고 단순한 API 캐시일 수도 있습니다. 두 경우의 백업 요구 사항은 완전히 다릅니다.
| 데이터 유형 | 권장 위치 | 백업 기대 동작 | 예시 |
|---|---|---|---|
| 사용자가 생성했으며 재구성할 수 없음 | Documents |
보존해야 함 | 초안, 사용자가 가져온 파일 |
| 앱의 영구 상태 | Application Support |
비즈니스 요구 사항에 따라 결정 | 데이터베이스, 편집 진행 상태 |
| 다시 생성하거나 다운로드할 수 있음 | Library/Caches |
백업에 의존하지 않음 | 이미지 캐시, 오프라인 인덱스 |
| 일회성 작업의 중간 파일 | tmp |
보존하지 않음 | 압축 해제 디렉터리, 분할 파일 |
Application Support는 특히 잘못 사용되기 쉽습니다. 앱이 장기간 관리하는 데이터를 저장하기에 적합하지만, 그 안의 모든 콘텐츠가 백업 대상이어야 한다는 뜻은 아닙니다. 대형 모델, 지도 패키지 또는 미디어 프록시 파일을 다시 가져올 수 있다면 명시적으로 백업 제외 속성을 설정하거나 Caches로 옮겨야 합니다.
판단 기준은 “이 파일이 중요한가”가 아니라 “기기를 복원한 후 이 파일이 반드시 백업에서 돌아와야 하며, 신뢰할 수 있는 소스에서 재구성할 수 없는가”입니다.
논리적 이름, 상대 경로, 복원 요구 사항, 담당자 등을 기록한 체크리스트를 저장소 안에 작성하십시오. 테스트는 이 합의를 실행하는 역할만 해야 하며, 코드 곳곳에 우연히 존재하는 경로를 규칙으로 간주해서는 안 됩니다.
XCTest로 백업 제외 속성 고정하기
Foundation은 하위 수준의 확장 속성을 직접 읽는 것보다 안정적인 인터페이스를 제공합니다. 다음 도우미 메서드는 파일을 생성한 뒤 isExcludedFromBackup을 설정하고, 리소스 값을 다시 읽어 해당 설정이 실제로 저장되었는지 확인합니다.
import XCTest
final class BackupBoundaryTests: XCTestCase {
private func markExcluded(_ url: URL) throws {
var values = URLResourceValues()
values.isExcludedFromBackup = true
var mutableURL = url
try mutableURL.setResourceValues(values)
}
func testDownloadPackageIsExcludedFromBackup() throws {
let root = FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
)[0]
let package = root.appendingPathComponent(
"Downloads/catalog.bundle",
isDirectory: false
)
try FileManager.default.createDirectory(
at: package.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try Data("fixture".utf8).write(to: package)
try markExcluded(package)
let values = try package.resourceValues(
forKeys: [.isExcludedFromBackupKey]
)
XCTAssertEqual(values.isExcludedFromBackup, true)
}
}
테스트의 핵심은 Foundation이 작동한다는 사실을 증명하는 것이 아니라, 프로덕션 코드가 파일을 생성한 뒤에도 동일한 설정 경로를 거치도록 보장하는 것입니다. 더 안전한 방법은 디렉터리 생성, 원자적 쓰기, 백업 제외 표시를 스토리지 컴포넌트에 캡슐화하고 테스트에서 해당 컴포넌트를 직접 호출하는 것입니다. 그렇지 않으면 테스트 코드는 속성을 정상적으로 설정했지만 프로덕션 다운로더는 이를 누락할 수 있으므로, 게이트는 여전히 의미가 없습니다.
잘못된 위치도 함께 검사하기
반대 방향의 단언도 추가해야 합니다. 사용자 초안은 Caches에 들어가면 안 되고, 임시 내보내기 결과물은 Documents에 남아 있으면 안 됩니다. 경로 변경은 최초 설치 과정이 아니라 마이그레이션 분기에서 자주 발생하므로, 테스트 픽스처는 생성, 업그레이드 마이그레이션, 실패 후 재시도를 모두 포함해야 합니다.
디렉터리 단위로 백업을 제외하는 경우 새로 생성된 하위 파일도 표본 검사해야 합니다. 상위 디렉터리의 현재 속성이 쓰기 로직을 영구적으로 대체할 수 있다고 가정하지 마십시오. 컴포넌트가 디렉터리를 생성할 때마다 속성을 명시적으로 확인해야 기존 데이터 마이그레이션과 디렉터리 재생성에도 더 안정적으로 대응할 수 있습니다.
시뮬레이터에서 컨테이너 증거 보존하기
단위 테스트는 규칙을 검증하는 데 적합하고, 시뮬레이터 컨테이너 검사는 “실패했을 때 실제로 무엇이 기록되었는가”를 확인하는 데 적합합니다. 먼저 테스트에서 고정된 기기와 독립된 DerivedData를 사용해 여러 작업이 상태를 공유하지 않도록 합니다.
set -euo pipefail
DERIVED_DATA="$PWD/.ci/DerivedData-backup"
RESULT_BUNDLE="$PWD/.ci/BackupBoundary.xcresult"
rm -rf "$DERIVED_DATA" "$RESULT_BUNDLE"
xcodebuild test \
-workspace Example.xcworkspace \
-scheme Example \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-derivedDataPath "$DERIVED_DATA" \
-resultBundlePath "$RESULT_BUNDLE"
APP_DATA="$(
xcrun simctl get_app_container booted com.example.app data
)"
find "$APP_DATA" -type f -print | LC_ALL=C sort \
> "$PWD/.ci/app-container-files.txt"
get_app_container는 대상 App이 설치되어 있고 해당 시뮬레이터가 부팅된 상태일 때만 성공합니다. CI에서 임의의 부팅된 기기를 자동으로 선택해서는 안 됩니다. 작업 시작 시 기기를 생성하거나 지정하고, 부팅이 완료될 때까지 기다린 뒤, 작업이 끝나면 종료해야 합니다. Runner에서 여러 테스트 세트를 병렬로 실행한다면 작업마다 독립된 시뮬레이터 기기 세트를 사용해 컨테이너가 서로 섞이지 않도록 할 수 있습니다.
파일 목록에는 상대 경로만 기록하는 편이 더 안전하고 비교하기도 쉽습니다. 홈 디렉터리, 작업 공간의 절대 경로 또는 테스트 자격 증명을 산출물에 포함하지 마십시오. 확장 속성을 검사해야 한다면 xattr 출력을 진단용 첨부 파일로 저장할 수 있습니다. 다만 게이트의 최종 판정은 보장되지 않은 하위 수준 동작에 의존하지 않도록 Foundation 리소스 값과 비즈니스 체크리스트를 기준으로 해야 합니다.
용량 이상을 설명 가능한 실패로 만들기
불리언 속성만 확인해서는 충분하지 않습니다. 경로 리팩터링 한 번으로 모든 캐시가 Documents에 들어갈 수 있지만, 각 파일에는 백업 제외 속성 테스트가 없을 수 있습니다. 컨테이너의 최상위 디렉터리별로 용량 예산을 설정할 수 있지만, 고정된 바이트 수를 맞추는 대신 구조적 이상을 드러내는 기준이어야 합니다.
예를 들어 테스트 픽스처 실행 후 Documents에는 지정된 샘플만 있어야 하고, 작업 종료 시 tmp는 비어 있어야 하며, Caches는 증가할 수 있지만 사용자 초안 확장자가 나타나면 안 됩니다. 대형 테스트 리소스에는 명시적인 허용 목록을 사용하고, 실패 메시지에 상대 경로, 파일 크기, 적용 규칙, 생성 단계를 출력해야 합니다.
실패 현장을 바로 삭제하지 않기
실패 직후 시뮬레이터부터 초기화해서는 안 됩니다. 먼저 다음 내용을 보관하십시오.
- XCTest 결과 번들과 실패한 테스트 케이스 이름
- 앱 컨테이너의 상대 경로 목록
- 각 최상위 디렉터리의 총크기
- 테스트에 사용된 커밋 번호, Scheme, 시뮬레이터 런타임
- 파일 생성을 유발한 테스트 단계
이러한 증거만으로도 “속성이 설정되지 않음”, “파일이 잘못된 디렉터리에 저장됨”, “정리 절차가 실행되지 않음”을 구분할 수 있습니다. 첨부 파일이 모두 확보된 것을 확인한 뒤 기기를 삭제해야 간헐적 실패에서 단언 한 줄만 남는 상황을 피할 수 있습니다.
안정적인 CI 게이트로 설계하기
백업 경계 테스트는 스토리지 계층, 다운로더, 데이터베이스 마이그레이션 또는 내보내기 흐름을 수정할 때마다 실행해야 하며, 병합 전 경량 테스트 세트에 포함할 수도 있습니다. 외부 서비스가 필요한 엔드 투 엔드 흐름에 결합하지 마십시오. 로컬 픽스처와 고정 크기 데이터를 사용해야 결과를 반복해서 재현할 수 있습니다.
실패는 세 가지 범주로 나누는 것이 좋습니다. 반드시 차단해야 하는 사용자 데이터 위치 오류, 반드시 차단해야 하는 백업 제외 속성 누락, 경고만 표시하는 용량 추세 변화입니다. 용량 임계값은 저장소 내부 설정으로 관리하고, 변경할 때는 리뷰에서 그 이유를 설명해야 합니다. 스크립트가 한도 초과를 감지한 뒤 자동으로 임계값을 완화해서는 안 됩니다.
마지막으로 실제 기기에서 한 차례 인수 테스트를 수행해야 합니다. 시뮬레이터는 디렉터리, 속성, 마이그레이션 코드를 안정적으로 검증할 수 있지만 실제 복원 경로의 모든 동작을 재현하지는 못합니다. 릴리스 검사에서는 최소 데이터 세트를 선택해 재구성할 수 없는 데이터가 정상적으로 복원되고, 다시 생성할 수 있는 데이터가 복원의 전제 조건으로 취급되지 않는지 확인해야 합니다. 이렇게 하면 자동화 게이트는 빈번한 회귀 검사를 담당하고, 실제 기기 절차는 최종 경계를 검증하며, 어느 한쪽도 다른 쪽을 대체하지 않습니다.
자주 묻는 질문
일반적으로 iOS 백업에서 제외해야 하는 파일은 무엇인가요?
다시 다운로드하거나 생성할 수 있는 캐시, 오프라인 리소스 묶음, 압축 해제 중간 파일과 임시 내보내기 결과는 Caches나 tmp에 두거나 isExcludedFromBackup을 설정하는 편이 적절합니다.
파일의 디렉터리만 확인하면 왜 부족한가요?
마이그레이션 코드나 외부 라이브러리가 파일을 잘못된 위치에 저장할 수 있으므로 용도, 백업 제외 속성, 재설치 후 복구 요구까지 함께 검사해야 합니다.
시뮬레이터 테스트가 실제 기기 검증을 대체할 수 있나요?
대체할 수 없습니다. 시뮬레이터는 디렉터리와 속성 규칙의 지속 검사에 적합하지만, 배포 전에는 통제된 실제 기기 절차로 백업과 복원 동작을 확인해야 합니다.
클라우드 M4 Mac에서 다음 작업 실행
Runner M4 또는 Runner M4 Plus를 선택하고 프로젝트에 맞춰 노드, 대여 기간 및 스토리지 옵션을 결정하세요. 각 주문은 독립된 물리 머신에 배정되며 가상 머신이 아닙니다.