HexVM 엔지니어링 실무

클라우드 Mac에 iOS 개인정보 매니페스트 검사 게이트 구축하기

클라우드 Mac에 iOS 개인정보 매니페스트 검사 게이트 구축하기

팀이 iOS 프로젝트를 클라우드 Mac으로 이전했더라도 빌드가 성공했다는 사실만으로 개인정보 보호 선언이 완전하다고 볼 수는 없습니다. 의존성을 업데이트하면 새로운 PrivacyInfo.xcprivacy가 추가될 수 있고, 리소스 복사 설정 문제로 메인 프로젝트의 매니페스트가 최종 App에 포함되지 않을 수도 있습니다. 더 안전한 방법은 개인정보 매니페스트를 빌드 산출물의 일부로 취급하는 것입니다. 먼저 소스 코드를 검사하고, 이어서 생성된 산출물을 확인한 뒤, 마지막으로 버전 관리되는 기준 파일을 사용해 검토되지 않은 변경을 차단합니다.

먼저 검사 게이트의 범위 정의하기

실제로 사용할 수 있는 검사 게이트라면 최소한 네 가지 계층을 다뤄야 합니다. 매니페스트를 plist로 파싱할 수 있는지, 최상위 필드의 자료형이 올바른지, 모든 의존성 매니페스트가 검사 범위에 포함되는지, 최종 App에 예상한 매니페스트가 존재하는지를 확인해야 합니다. 저장소 루트에서 파일 하나만 찾는 방식으로는 충분하지 않습니다. 소스 의존성, 사전 컴파일된 프레임워크, 패키지 관리자가 체크아웃한 컴포넌트에도 각각 선언이 포함될 수 있습니다.

먼저 깨끗한 작업 공간에서 매니페스트 인덱스를 생성합니다.

find . \
  -path './.git' -prune -o \
  -path './DerivedData' -prune -o \
  -name PrivacyInfo.xcprivacy -print \
  | LC_ALL=C sort > privacy-manifests.current

프로젝트에서 명시적으로 관리하는 경로는 privacy-manifests.baseline에 기록하는 것이 좋습니다. 파이프라인에서 두 파일을 비교하고, 경로가 추가·삭제·변경되면 우선 실패 처리한 뒤 코드 소유자가 확인한 후 기준 파일을 갱신합니다. 이렇게 하면 의존성 업데이트로 생긴 선언 변경이 아무런 알림 없이 넘어가는 일을 막을 수 있습니다.

기준 파일은 규정 준수의 증거가 아닙니다. 현재 변경이 명시적으로 검토되었다는 사실만 보여 줄 뿐이며, 코드의 실제 동작에 대한 판단을 대신할 수 없습니다.

plist와 최상위 구조 검증하기

plutil -lint는 XML 또는 바이너리 plist의 손상을 찾아낼 수 있지만, 필드 자료형이 올바른지까지 판단해 주지는 않습니다. 별도 의존성을 설치하지 않고도 Python 표준 라이브러리로 가벼운 구조 검사를 한 단계 추가할 수 있습니다.

import pathlib
import plistlib
import sys

allowed = {
    "NSPrivacyTracking": bool,
    "NSPrivacyTrackingDomains": list,
    "NSPrivacyCollectedDataTypes": list,
    "NSPrivacyAccessedAPITypes": list,
}

failed = False
files = sorted(pathlib.Path(".").rglob("PrivacyInfo.xcprivacy"))

if not files:
    print("No privacy manifest found")
    sys.exit(1)

for path in files:
    try:
        with path.open("rb") as stream:
            data = plistlib.load(stream)
        if not isinstance(data, dict):
            raise TypeError("Root must be a dictionary")
        for key, value in data.items():
            expected = allowed.get(key)
            if expected is None:
                raise KeyError(f"Unknown top-level key: {key}")
            if not isinstance(value, expected):
                raise TypeError(f"{key} must be {expected.__name__}")
        print(path)
    except Exception as error:
        failed = True
        print(f"{path}: {error}")

sys.exit(1 if failed else 0)

스크립트를 Scripts/validate_privacy_manifests.py로 저장하고 빌드 전에 실행합니다. 프로젝트에서 조직 내부용 확장 필드를 사용한다면 모든 알 수 없는 키를 한꺼번에 허용해서는 안 됩니다. 각 키를 허용 목록에 개별적으로 추가하고 용도를 기록해야 합니다.

Required Reason API 검토표 만들기

구조가 올바르다고 해서 선언한 사유까지 올바른 것은 아닙니다. 파일 타임스탬프, 시스템 부팅 시간, 디스크 공간, 환경설정 등에 접근하는 호출도 모두 코드 검토 범위에 포함해야 합니다. 자동 스캔은 단서를 제공할 수 있지만 매크로, 래퍼 계층, 바이너리 의존성 때문에 누락이나 오탐이 발생할 수 있습니다. 따라서 한 번의 텍스트 검색만으로 사유 코드를 결정해서는 안 됩니다.

각 선언마다 감사 가능한 기록을 한 행씩 유지합니다.

검사 항목 기록할 내용 실패 조건
API 범주 매니페스트의 범주 식별자 범주에 대응하는 코드 경로가 없음
사용 위치 모듈, 파일 및 담당자 호출 출처를 찾을 수 없음
사용 목적 사용자에게 제공되는 실제 기능 사유와 실제 동작이 일치하지 않음
의존성 출처 자체 코드 또는 구체적인 컴포넌트 바이너리 출처가 불분명함
재검토 조건 코드 또는 의존성 버전 변경 변경 후 다시 확인하지 않음

비공개 소스 바이너리 컴포넌트라면 최소한 컴포넌트 버전, 매니페스트 해시, 해당 컴포넌트를 도입한 비즈니스 기능을 기록해야 합니다. 출처를 설명할 수 없는 선언은 추측으로 채우지 말고 의존성 담당자에게 돌아가 확인해야 합니다.

최종 App에서 다시 검사하기

소스 코드에 있는 파일도 target membership 또는 리소스 단계 설정 오류 때문에 복사되지 않을 수 있습니다. 파이프라인에서 서명 없는 Release 빌드를 한 번 수행한 뒤 실제 산출물을 검사해야 합니다.

rm -rf .build/privacy

xcodebuild \
  -scheme "$SCHEME" \
  -configuration Release \
  -sdk iphoneos \
  -derivedDataPath .build/privacy \
  CODE_SIGNING_ALLOWED=NO \
  build

APP_PATH="$(find .build/privacy/Build/Products -type d -name '*.app' -print -quit)"
test -n "$APP_PATH"
find "$APP_PATH" -name PrivacyInfo.xcprivacy -print | LC_ALL=C sort

메인 App, 임베드된 프레임워크, 확장에는 프로젝트의 예상에 따라 각각 매니페스트가 나타나야 합니다. DerivedData의 임의 디렉터리를 하드코딩해서는 안 되며, 소스 파일 개수만 확인해서도 안 됩니다. 여러 소스 매니페스트가 빌드 과정에서 병합되거나 교체되거나 누락될 수 있기 때문입니다.

산출물 인벤토리 저장하기

산출물에 포함된 각 매니페스트의 상대 경로와 SHA-256을 파이프라인 첨부 파일로 저장합니다.

find "$APP_PATH" -name PrivacyInfo.xcprivacy -print0 |
  while IFS= read -r -d '' file; do
    relative="${file#"$APP_PATH"/}"
    digest="$(shasum -a 256 "$file" | awk '{print $1}')"
    printf '%s  %s
' "$digest" "$relative"
  done | LC_ALL=C sort > privacy-artifact.sha256

심사 문제가 발생했을 때 이 기록을 사용하면 “해당 빌드에 실제로 무엇이 포함되었는가”를 확인할 수 있습니다. 현재 브랜치만 살펴보는 것보다 신뢰할 수 있습니다.

일상적인 개발 흐름에 실패 조건 연결하기

검사를 빠른 검사와 전체 검사, 두 단계로 나누는 것이 좋습니다. 병합 요청에서는 먼저 매니페스트 검색, plist 파싱, 필드 자료형, 기준 파일과의 차이를 검사합니다. 메인 브랜치에서는 Release 빌드와 App 산출물 검토까지 수행합니다. 빠른 검사가 실패하면 대개 수십 초 안에 문제가 있는 경로를 확인할 수 있고, 전체 검사는 리소스 단계와 임베드된 프레임워크의 문제를 찾아냅니다.

출시 전에는 다음 검사 항목을 유지합니다.

  • 매니페스트 인덱스가 승인된 기준 파일과 일치합니다.
  • 모든 파일이 plutil -lint와 구조 검사 스크립트를 통과합니다.
  • 새로 추가된 API 범주가 코드 위치, 담당자, 실제 용도와 연결되어 있습니다.
  • 의존성 업데이트 후의 매니페스트 차이를 사람이 검토했습니다.
  • 최종 App, 확장, 임베드된 프레임워크의 매니페스트가 예상과 일치합니다.
  • 산출물 경로와 해시가 이번 빌드와 함께 보관되었습니다.

이 검사 게이트의 목적은 팀을 대신해 규정 준수 여부를 자동으로 판단하는 것이 아닙니다. 누락이 발생하면 빌드가 명확히 실패하도록 만들고, 선언의 모든 변경을 코드, 의존성, 검토 기록까지 추적할 수 있게 하는 데 목적이 있습니다.

자주 묻는 질문

저장소에 PrivacyInfo.xcprivacy가 있으면 충분한가요?

아닙니다. 파일이 정상적으로 해석되고 필드 형식이 올바르며 최종 앱이나 프레임워크 산출물에도 포함되어야 합니다.

자동화만으로 Required Reason API 사유의 적절성을 판단할 수 있나요?

완전히 판단할 수는 없습니다. 누락, 구조 오류와 미검토 변경은 찾을 수 있지만 실제 호출 목적과의 일치는 코드 담당자가 확인해야 합니다.

의존성 업데이트 후 기준 파일을 다시 만들어야 하는 이유는 무엇인가요?

의존성이 자체 매니페스트를 추가하거나 변경할 수 있기 때문입니다. 차이를 검토하고 선언이 실제 동작과 맞을 때만 기준을 갱신합니다.

독점 Apple Silicon

다음 빌드를 위한 독점 클라우드 Mac 선택

워크로드에 맞춰 Mac mini 모델, 대여 기간 및 리전을 선택하세요. 각 주문에는 독립된 물리 노드가 할당되며, 실제 이용 가능 여부는 콘솔의 실시간 응답을 기준으로 합니다.

클라우드 Mac 요금제 선택