평범한 iOS 버전 업그레이드만으로도 로그인 토큰이 갑자기 “존재하지 않는 항목”이 될 수 있습니다. 새 버전이 충돌한 것도 아니고 API에도 문제가 없다면, 실제 원인은 Keychain의 service, account, 접근 그룹 또는 보호 속성이 달라진 데 있을 가능성이 큽니다. 클라우드 Mac에서 이런 문제를 안정적으로 찾아내려면 새로 설치한 상태만 테스트해서는 안 됩니다. 반드시 “이전 버전에서 쓰고 새 버전에서 읽는” 흐름부터 검증해야 합니다.
먼저 마이그레이션 계약 정의하기
Keychain 항목을 호환성을 유지해야 하는 데이터 구조로 취급해야 합니다. 최소한 항목 클래스, service, account, access group, kSecAttrAccessible의 다섯 가지를 기록합니다. 새 버전에서 이름을 변경해야 하더라도 조회 조건부터 바로 바꾸면 안 됩니다. 먼저 기존 조건으로 읽고, 새 구조로 쓴 뒤 성공 여부를 확인한 다음 기존 항목을 삭제해야 합니다.
각 마이그레이션에는 keychainSchemaVersion = 2와 같은 정수 버전을 할당하고, 완료 표식은 일반 환경설정에 저장하는 것이 좋습니다. 이 표식은 마이그레이션의 중복 실행을 막기 위한 용도일 뿐, Keychain 데이터가 실제로 존재한다는 증거가 될 수는 없습니다. 앱을 시작할 때는 대상 항목을 계속 확인하여 “표식은 있지만 데이터가 삭제된” 상황도 처리해야 합니다.
마이그레이션의 성공 기준은 함수가 성공을 반환하는지가 아닙니다. 새 버전에서 기존 데이터를 읽을 수 있고, 반복 실행해도 데이터가 손상되지 않으며, 실패 후에도 복구 가능한 원본 항목이 남아 있어야 합니다.
먼저 최소 인수 테스트 매트릭스를 작성합니다.
| 시나리오 | 초기 상태 | 예상 결과 |
|---|---|---|
| 새로 설치 | 기존 항목 없음 | 새 버전 항목 생성 |
| 덮어쓰기 업그레이드 | 기존 버전 항목이 온전함 | 마이그레이션 후 읽기 가능 |
| 반복 실행 | 마이그레이션 완료 | 중복 쓰기 없음 |
| 기존 항목 손상 | 데이터 형식 오류 | 현장 상태를 보존하고 명확한 오류 반환 |
| 접근 그룹 변경 | 새 버전에서 기존 그룹에 접근할 수 없음 | 배포를 차단하고 서명 설정 확인 |
이전 버전과 새 버전의 테스트 픽스처 구성하기
설치 가능한 시뮬레이터용 .app 두 개를 보관합니다. 이전 버전은 데이터를 시드하고, 새 버전은 마이그레이션과 검증을 수행합니다. 두 빌드 사이에 erase를 실행하거나 앱을 제거하면 테스트 전제 조건이 달라지므로 그렇게 해서는 안 됩니다. 테스트 빌드에는 -UITestSeedLegacyKeychain, -UITestVerifyKeychainV2처럼 자동화 환경에서만 활성화되는 실행 인수를 추가하고, 프로덕션 빌드에서는 이러한 인수에 반응하지 않도록 합니다.
set -euo pipefail
UDID="${SIMULATOR_UDID:?missing simulator id}"
OLD_APP="${OLD_APP_PATH:?missing old app path}"
NEW_APP="${NEW_APP_PATH:?missing new app path}"
BUNDLE_ID="com.example.product"
xcrun simctl bootstatus "$UDID" -b
xcrun simctl install "$UDID" "$OLD_APP"
xcrun simctl launch --terminate-running-process \
"$UDID" "$BUNDLE_ID" -UITestSeedLegacyKeychain
xcrun simctl install "$UDID" "$NEW_APP"
xcrun simctl launch --terminate-running-process \
"$UDID" "$BUNDLE_ID" -UITestVerifyKeychainV2
시드 작업 자체도 검증할 수 있어야 합니다. 이전 버전이 쓰기를 마치면 테스트 화면에서 민감한 값을 포함하지 않는 상태 파일을 출력할 수 있습니다. 여기에는 항목 버전, 쓰기 결과, 무작위 테스트 케이스 번호만 기록합니다. 새 버전도 검증을 완료한 뒤 결과 파일을 작성하며, CI에서는 상태만 비교하고 토큰 내용은 출력하지 않습니다.
동시 실행으로 인한 오염 방지하기
동일한 시뮬레이터에서 여러 Keychain 테스트를 병렬로 실행하지 마십시오. 작업마다 별도의 시뮬레이터를 생성하거나 테스트 스위트를 직렬로 실행해야 합니다. service와 account에는 무작위 테스트 케이스 접미사를 추가하여, 이전 실행이 비정상적으로 중단되면서 남긴 데이터 때문에 다음 테스트가 성공한 것으로 잘못 판단되지 않도록 합니다.
코드에 진단 가능한 결과 보존하기
모든 실패를 nil로 변환해서는 안 됩니다. OSStatus를 유지해야 항목 없음, 권한 불일치, 매개변수 오류를 구분할 수 있습니다. 조회 함수는 다음과 같이 명확한 결과 타입을 반환할 수 있습니다.
enum KeychainReadResult {
case found(Data)
case missing
case failed(OSStatus)
}
func readLegacyToken(service: String, account: String) -> KeychainReadResult {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
if status == errSecItemNotFound {
return .missing
}
guard status == errSecSuccess, let data = item as? Data else {
return .failed(status)
}
return .found(data)
}
마이그레이션할 때는 새 버전 항목을 먼저 추가하고, 다시 읽어 내용의 다이제스트를 비교한 다음 기존 항목을 삭제합니다. 먼저 삭제한 뒤 쓰면 안 됩니다. 권한이나 매개변수 문제로 쓰기에 실패하면 사용자 데이터를 복구할 수 없게 됩니다. 로그에는 상태 코드, 마이그레이션 버전, 테스트 케이스 번호만 기록하고 원본 자격 증명은 남기지 않습니다.
서명 및 접근 그룹 확인하기
코드는 바뀌지 않았는데 조회 결과가 갑자기 errSecItemNotFound로 반환된다면 이전 산출물과 새 산출물의 entitlements를 비교해야 합니다. 접근 그룹 접두사, 앱 식별자, Keychain access groups 중 하나라도 달라지면 기존 항목이 새 버전에서 보이지 않을 수 있습니다.
codesign -d --entitlements :- "$OLD_APP_PATH" > old-entitlements.plist
codesign -d --entitlements :- "$NEW_APP_PATH" > new-entitlements.plist
diff -u old-entitlements.plist new-entitlements.plist
차이 파일은 CI 첨부 파일로 보관하되, 서명 자료를 빌드 로그에 복사해서는 안 됩니다. 접근 그룹을 실제로 변경해야 한다면 새 버전이 임의의 기존 그룹을 직접 읽을 수 있다고 가정하지 말고, 기존 그룹과 새 그룹에 모두 접근할 수 있는 전환 버전이 데이터를 옮기도록 설계해야 합니다.
시뮬레이터는 마이그레이션 알고리즘과 오류 분기를 검증하는 데 적합하지만, 모든 서명 및 접근 제어 동작을 재현하지는 못합니다. 배포 전에는 통제된 실제 기기에서도 동일한 이전 버전 설치, 데이터 시드, 덮어쓰기 업그레이드 절차를 한 번 수행하고, 그 결과를 시뮬레이터 테스트와 별도로 기록해야 합니다.
정리, 실패 증거 수집 및 배포 게이트
성공한 테스트가 끝나면 앱 내부 테스트 인터페이스를 통해 정확한 service와 account를 지정하여 테스트 항목을 삭제합니다. 광범위한 삭제 쿼리를 사용하면 같은 테스트 기기의 다른 데이터까지 잘못 지울 수 있으므로 피해야 합니다. 실패한 테스트에서는 우선 시뮬레이터를 보존하고, 새 버전과 이전 버전의 entitlements, OSStatus, 마이그레이션 단계, 앱 로그를 수집한 뒤 폐기 여부를 결정합니다.
배포 게이트에서는 세 가지를 확인해야 합니다. 이전 버전의 데이터를 새 버전에서 읽을 수 있어야 하고, 마이그레이션을 연속 두 번 실행해도 결과가 동일해야 하며, 비정상 입력 때문에 기존 항목이 삭제되어서는 안 됩니다. 하나라도 실패하면 해당 산출물이 다음 배포 단계로 넘어가지 못하도록 차단해야 합니다.
마지막으로 마이그레이션 스크립트에 고정 입력값을 남깁니다. 이전 버전 빌드 식별자, 새 버전 빌드 식별자, 시뮬레이터 시스템 버전, 테스트 케이스 번호를 기록해야 합니다. 이렇게 하면 클라우드 Mac이 다시 초기화되더라도 보관된 산출물에서 동일한 업그레이드 경로를 복원할 수 있습니다. Keychain 문제에서 가장 어려운 점은 쓰기 자체가 아니라 이전 상태가 없다는 것입니다. 이전 버전 픽스처, 서명 차이, 오류 코드를 모두 재현할 수 있다면 마이그레이션은 운영 환경의 위험 요소가 아니라 일반적인 회귀 테스트로 관리할 수 있습니다.
자주 묻는 질문
새로 설치한 상태의 Keychain 테스트만으로 부족한 이유는 무엇인가요?
현재 빌드가 새 레코드를 쓸 수 있다는 사실만 확인하기 때문입니다. 이전 빌드가 만든 service, account, 접근 그룹과 보호 속성을 새 빌드가 읽는지는 별도로 검증해야 합니다.
Keychain 마이그레이션 실패 시 무엇을 먼저 확인해야 하나요?
SecItemCopyMatching이 반환한 OSStatus를 기록하고 두 빌드의 entitlements, 접근 그룹, service, account, kSecAttrAccessible을 비교합니다. 진단 전에 기존 레코드를 삭제하지 않습니다.
시뮬레이터 테스트가 기기 검증을 완전히 대체할 수 있나요?
아닙니다. 반복 가능한 조회와 마이그레이션 검증에는 적합하지만 서명, 접근 제어와 일부 보안 동작은 통제된 실제 기기에서 최종 확인해야 합니다.
개발 파이프라인에 고정 클라우드 Mac 구성하기
MangoVM은 M4와 M4 Pro 두 가지 Apple Silicon 물리 노드를 제공합니다. 대여 기간, 리전, 스토리지 추가 옵션을 선택하면 전체 주문 내역을 확인할 수 있습니다.