빌드 파이프라인을 교체한 뒤 기존 사용자의 UUID가 바뀌는 문제가 있었습니다. UUID를 생성하고 저장하는 코드는 그대로였는데, 이전에 저장한 값을 읽지 못해서 새 값을 발급하고 있었습니다.

빌드 설정에서 찾은 원인

확인해보니 키체인 접근 그룹이 기존 teamid.*에서 teamid.bundleid 형태로 바뀌어 있었습니다. 기존 값이 삭제된 것은 아니었고, 변경된 설정으로는 이전 그룹에 저장된 값을 읽지 못하는 상황이었습니다.

처음에는 접근 그룹을 변경한 기억이 없어서 Unity 빌드 후처리 코드를 확인했습니다. 그런데 프로젝트에는 keychain-access-groups를 설정하는 코드가 없었습니다.

이 설정은 빌드머신에서 추가하고 있었습니다. CI 단계에서 entitlements 파일을 생성해 넣었고, 키체인 접근 그룹에는 와일드카드가 사용되고 있었습니다. 프로젝트 소스만 봐서는 확인할 수 없었던 부분입니다.

이후 iOS Universal Link를 추가하면서 Associated Domains 설정과 entitlements 파일을 프로젝트에서 직접 구성하게 됐습니다. 이 과정에서 키체인 접근 그룹도 앱 전용으로 지정했습니다.

프로젝트에 없던 설정을 추가한다고 생각했는데, 실제로는 빌드머신에서 넣어주던 설정을 변경한 것이었습니다. 변경 전후를 비교하면 다음과 같습니다.

<!-- 기존: CI에서 생성해 넣었던 설정 -->
<key>keychain-access-groups</key>
<array>
  <string>$(AppIdentifierPrefix)*</string>
</array>

<!-- 변경 후: 프로젝트에서 직접 구성한 설정 -->
<key>com.apple.developer.associated-domains</key>
<array>
  <string>applinks:내-도메인</string>
</array>
<key>keychain-access-groups</key>
<array>
  <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
</array>

이 예시는 당시 빌드 설정입니다. 실제 적용된 접두사와 접근 그룹은 최종 앱의 서명 정보를 기준으로 확인해야 합니다. Universal Link의 빌드 문제는 Unity 딥링크 문제 해결에 따로 정리했습니다.

접근 그룹이 UUID 조회에 미치는 영향

키체인 항목은 하나의 접근 그룹에 속합니다. 앱이 해당 그룹에 접근할 권한을 잃으면 항목이 남아 있어도 읽을 수 없습니다. 다만 접근 그룹을 지정하지 않았을 때의 동작은 저장과 조회가 다릅니다.

동작kSecAttrAccessGroup을 생략했을 때
새 항목 저장앱의 기본 접근 그룹에 저장합니다.
기존 항목 조회앱이 접근할 수 있는 모든 그룹을 검색합니다.

기본 그룹은 접근 그룹 목록의 첫 번째 항목이며, 별도 키체인 그룹을 지정하지 않은 경우에는 앱 식별자가 사용됩니다. 그룹을 삭제하거나 순서를 변경하면 새 항목의 저장 그룹도 달라질 수 있습니다. Apple 키체인 공유 설정 문서

반면 SecItemCopyMatching에서 특정 그룹을 지정하면 그 그룹만 조회합니다. 기본 그룹이 바뀌어도 이전 그룹의 접근 권한이 남아 있고 조회 조건이 맞으면 기존 값을 읽을 수 있으므로, 서명 권한과 조회 코드를 함께 봐야 합니다. Apple kSecAttrAccessGroup 문서

같은 그룹을 사용하는 앱들은 키체인 항목을 공유할 수 있습니다. 다만 같은 팀에 속한다는 이유만으로 모든 앱이 같은 UUID를 사용하는 것은 아닙니다. 접근 그룹과 항목을 찾는 조건이 모두 맞아야 합니다. Apple 키체인 항목 공유 문서

그래서 대응 전에 기존 UUID의 저장 그룹과 다른 프로젝트의 공유 여부, 변경 전후의 저장·조회 조건을 확인해야 했습니다. 마이그레이션을 하려면 이전 그룹과 새 그룹을 함께 등록한 상태에서 양쪽에 접근할 수 있는지도 확인이 필요했습니다.

기존 UUID 유지와 변경 감지

파이프라인을 아직 변경하지 않은 프로젝트에는 기존 UUID를 유지하도록 마이그레이션을 추가했습니다.

  1. 새 그룹에서 UUID를 조회합니다.
  2. 값이 없으면 이전 그룹에서 UUID를 조회합니다.
  3. 이전 값이 있으면 새 그룹에 저장하고, 기존 UUID를 그대로 사용합니다.

이 방식은 앱의 서명 권한에서 두 그룹에 모두 접근할 수 있어야 합니다. 그룹 이름만 코드에 추가한다고 이전 값을 읽을 수 있는 것은 아닙니다.

이미 새 UUID를 사용하고 있는 프로젝트에는 이전 UUID의 변경 상황과 저장 실패를 C# 이벤트로 전달하도록 했습니다. 이후 문제가 얼마나 발생하는지 추적하기 위한 대응이었습니다. 이전 UUID를 복구하려면 기존 값과 접근 권한이 남아 있는지 확인해야 하고, 이미 새 UUID로 쌓인 데이터의 처리도 필요합니다.

이 과정에서 빌드 후처리와 네이티브 코드도 함께 수정했습니다.

  • CI와 프로젝트가 각각 만들던 entitlements 파일은 한 곳에서 관리하도록 정리했습니다. 기존 XcodeOption.cs를 제거하고 iOS 빌드 후처리 구조를 변경했습니다.
  • 접근 그룹 문자열이 잘못 구성되지 않도록 팀 식별자, AppIdentifierPrefix, 번들 식별자를 조합하는 로직을 보정했습니다.
  • UUID 조회·저장 인터페이스를 단순화하고, 호출한 쪽에서도 저장 실패를 확인할 수 있도록 실패 상태를 C#에 전달했습니다.
  • iOS 네이티브 문자열 전달을 Unity 마샬링 방식으로 정리하고, loadUuid가 두 번 호출되던 부분을 한 번으로 줄였습니다.

재설치 시 식별자 테스트

패키지에서는 SystemInfo.deviceUniqueIdentifier로 UUID를 만들고 있었습니다. Unity는 iOS에서 이 값을 얻을 때 UIDevice.identifierForVendor를 사용합니다. Unity SystemInfo.deviceUniqueIdentifier 문서

앱을 재설치하면 다른 값이 나올 것으로 생각하고 테스트했는데, Xcode 개발 빌드를 재설치해도 같은 값이 나왔습니다.

Apple 문서에 설명된 동작은 다음과 같습니다.

  • 같은 벤더의 앱이 기기에 설치되어 있는 동안에는 값이 유지됩니다.
  • 같은 벤더의 앱을 모두 삭제한 뒤 다시 설치하면 값이 바뀝니다.
  • Xcode 테스트 빌드나 Ad Hoc 배포 방식으로 설치할 때도 값이 바뀔 수 있습니다.

테스트 빌드를 설치할 때마다 반드시 바뀌는 것은 아닙니다. Apple identifierForVendor 문서

당시 테스트에서 앱을 삭제하지 않고 덮어 설치한 것인지, 같은 벤더의 다른 앱이 남아 있었는지는 명확하게 확인하지 못했습니다. 값이 유지된 이유를 확인하려면 이 조건들을 다시 맞춰 테스트해야 합니다.