엔지니어링 실무

프레임워크의 호환성 파괴 변경을 병합 전에 차단하기

프레임워크의 호환성 파괴 변경을 병합 전에 차단하기

여러 App이 사용하는 Swift Framework에서는 공개 메서드 하나를 삭제하거나 매개변수 타입을 더 엄격하게 제한하거나 공개 프로토콜에 필수 요구 사항을 추가하는 것만으로도, 업데이트 후 하위 프로젝트가 컴파일되지 않을 수 있습니다. 단위 테스트는 대개 구현 동작을 검증하지만, 이번 변경으로 공개 인터페이스가 깨졌다는 사실을 명확히 알려 주지는 않습니다. OnceMac의 클라우드 Mac 빌드 노드에서는 Swift API Digester를 병합 검사에 포함할 수 있습니다. 먼저 출시된 버전의 API 스냅샷을 저장하고, 완전히 동일한 툴체인으로 후보 스냅샷을 생성한 다음 두 결과를 비교합니다.

먼저 비교 조건 고정하기

API 스냅샷은 소스 코드만으로 결정되지 않습니다. Xcode 버전, Swift 컴파일러, SDK, 대상 아키텍처, 빌드 구성, 조건부 컴파일 플래그가 모두 결과에 영향을 줍니다. 따라서 첫 단계는 비교 실행이 아니라 이러한 입력값을 로그에 기록하고 고정하는 것입니다.

set -euo pipefail

xcodebuild -version
xcrun swift --version
SDK_PATH="$(xcrun --sdk iphonesimulator --show-sdk-path)"
echo "SDK_PATH=$SDK_PATH"

CI에서는 Xcode를 명시적으로 선택하고, 기준 브랜치와 후보 브랜치에 동일한 이미지 또는 동일한 노드 템플릿을 사용해야 합니다. 대상 트리플도 일치시켜야 하며, 예를 들어 양쪽 모두 arm64-apple-ios17.0-simulator를 사용합니다. 한쪽에서는 실제 기기용 인터페이스를 생성하고 다른 쪽에서는 시뮬레이터용 인터페이스를 생성하면 안 됩니다. 플랫폼 조건의 차이가 코드 변경으로 잘못 판정될 수 있습니다.

API 기준 파일은 빌드 캐시가 아니라 릴리스 계약입니다. 버전 관리에 포함하고 리뷰를 거쳐야 하며, 일반 빌드 작업이 자동으로 덮어써서는 안 됩니다.

분석 가능한 Framework 빌드하기

다음 예시는 scheme 이름과 module 이름이 모두 MyKit이라고 가정합니다. 별도의 Derived Data를 먼저 정리한 뒤 Release 구성으로 빌드합니다. BUILD_LIBRARY_FOR_DISTRIBUTION=YES는 모듈 안정성에 필요한 인터페이스 파일을 생성하며, 검사 조건을 바이너리 배포 환경에 더 가깝게 맞춰 줍니다.

DERIVED_DATA="$PWD/.build/api-dd"
PRODUCTS="$DERIVED_DATA/Build/Products/Release-iphonesimulator"

rm -rf "$DERIVED_DATA"

xcodebuild build \
  -scheme MyKit \
  -configuration Release \
  -destination "generic/platform=iOS Simulator" \
  -derivedDataPath "$DERIVED_DATA" \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
  SKIP_INSTALL=NO \
  CODE_SIGNING_ALLOWED=NO

빌드가 끝나면 먼저 모듈이 실제로 존재하는지 확인합니다. 잘못된 검색 경로에서 Digester를 실행해 이해하기 어려운 “module not found” 오류가 발생하지 않도록 해야 합니다.

test -d "$PRODUCTS/MyKit.framework"
find "$PRODUCTS/MyKit.framework/Modules" -maxdepth 3 -type f -print

프로젝트가 workspace를 통해 의존성을 관리한다면 -workspace를 추가해야 합니다. scheme이 공유되지 않으면 CI에서 발견할 수 있으므로, 스크립트로 경로를 추측하지 말고 먼저 프로젝트 설정에서 scheme을 공유해야 합니다.

API 스냅샷 생성 및 비교하기

현재 릴리스 기준과 후보 코드를 각각 한 번씩 빌드한 뒤 JSON으로 출력합니다. 기준 파일은 저장소의 api-baselines/ 디렉터리에 두고 파일 이름에 플랫폼을 기록하는 것이 좋습니다. 머신 경로나 빌드 번호까지 넣을 필요는 없습니다.

mkdir -p api-baselines .build/api-report

xcrun swift-api-digester \
  -dump-sdk \
  -module MyKit \
  -sdk "$SDK_PATH" \
  -target arm64-apple-ios17.0-simulator \
  -F "$PRODUCTS" \
  -o ".build/api-report/current-ios-simulator.json"

xcrun swift-api-digester \
  -diagnose-sdk \
  -input-paths "api-baselines/MyKit-ios-simulator.json" \
  -input-paths ".build/api-report/current-ios-simulator.json" \
  > ".build/api-report/diagnostics.txt"

처음 도입할 때는 검증을 마친 current-ios-simulator.json을 기준 파일로 복사해 커밋합니다. 이후 CI에서는 이 파일을 읽기만 해야 합니다. 명령이 0이 아닌 종료 상태를 반환하면 “호환성 검사 실패”라는 문장만 표시하지 말고, 진단 텍스트를 빌드 첨부 파일로 보관해야 합니다.

중점적으로 검토할 변경 사항

변경 기본 판단 처리 방법
공개 타입 또는 메서드 삭제 호환성 파괴 인터페이스를 복원하거나 명시적인 메이저 버전 변경에 포함
매개변수, 반환값 또는 제네릭 제약 변경 호환성 파괴 호환 가능한 오버로드를 제공하고 지원 중단 기간을 계획
공개 프로토콜에 필수 요구 사항 추가 고위험 기본 구현 제공 검토
공개 메서드 또는 타입 추가 일반적으로 호환 이름, 가시성, 플랫폼 표시 확인
내부 구현만 변경 나타나지 않아야 함 접근 수준이 의도치 않게 확대됐는지 확인

public이 항상 의도적인 호환성 약속을 뜻하는 것은 아닙니다. 외부에서 호출하면 안 되는 선언이라면 무시 규칙을 장기간 유지하기보다 접근 수준을 먼저 제한하는 편이 좋습니다.

진단 결과를 유지 관리 가능한 CI 게이트로 만들기

프로세스는 “빌드”, “스냅샷 생성”, “비교”, “보고서 업로드”의 네 단계로 나누는 것이 좋습니다. 스크립트에서는 set -euo pipefail을 활성화하고 Derived Data를 작업별 독립 디렉터리에 배치해, 병렬 작업이 서로의 산출물을 읽지 않도록 합니다. 여러 모듈이 있는 저장소라면 모듈 목록을 관리해 하나씩 실행하고, 이전 모듈의 PRODUCTS 경로를 재사용하지 않아야 합니다.

게이트가 실패하면 리뷰어에게 세 가지 정보가 제공되어야 합니다. 사용한 Xcode 및 Swift 버전, 기준 및 후보 스냅샷, 전체 진단 텍스트입니다. 변경 사항이 버전 정책에 부합한다고 확인된 경우에만 동일한 병합 요청에서 기준 파일을 업데이트해야 합니다. 실패한 작업이 기준 파일을 직접 다시 쓰도록 허용하면 검사가 항상 통과해 아무런 의미가 없어집니다.

오탐을 확인하는 순서

먼저 툴체인을 확인하고, 다음으로 SDK와 대상 트리플을 검사한 뒤, 빌드 매개변수와 조건부 컴파일 플래그를 비교합니다. 마지막 단계에서야 Digester 출력의 노이즈인지 판단해야 합니다. 다음 체크리스트를 사용하면 원인 파악 시간을 줄일 수 있습니다.

  1. xcodebuild -version이 완전히 동일한지 확인합니다.
  2. scheme, 구성, destination이 동일한지 확인합니다.
  3. 양쪽 모두 BUILD_LIBRARY_FOR_DISTRIBUTION이 활성화되어 있는지 확인합니다.
  4. 모듈 검색 경로가 현재 작업의 산출물을 가리키는지 확인합니다.
  5. 기준 파일이 임의의 과거 커밋이 아니라 마지막으로 출시된 인터페이스에서 생성됐는지 확인합니다.
  6. 생성된 파일에 작업 디렉터리처럼 변경되기 쉬운 절대 경로가 포함됐는지 확인합니다.

버전 정책으로 마무리하기

호환성 게이트는 변경 사항을 발견할 뿐이며, 팀을 대신해 버전 번호를 결정하지는 못합니다. 인터페이스 삭제, 공개 타입 변경, 프로토콜 요구 사항 추가는 일반적으로 호환되지 않는 버전 계획에 포함해야 합니다. 새 인터페이스도 이름과 가용성 검토를 거쳐야 합니다. 지원 중단된 인터페이스는 합의된 기간 동안 유지한 뒤 후속 버전에서 제거해야 합니다.

가장 안정적인 절차는 릴리스 브랜치에 기준 파일을 저장하고, 병합 요청에서는 후보 스냅샷만 생성하며, CI가 기계 판독 가능한 진단을 출력하고, 유지 관리자가 버전 정책에 따라 결정하는 것입니다. 이렇게 하면 모든 API 변경을 일률적으로 금지하지 않으면서도 의도하지 않은 public 변경이 모든 하위 프로젝트에 그대로 전달되는 일을 막을 수 있습니다.

자주 묻는 질문

Swift API Digester가 단위 테스트를 대체할 수 있나요?

아닙니다. 공개 Swift API의 구조 변화만 검사하며 런타임 동작, 비즈니스 로직, Objective-C 인터페이스는 검증하지 않습니다. 기존 테스트와 함께 실행해야 합니다.

API 기준 파일은 언제 갱신해야 하나요?

인터페이스 변경이 버전 정책에 맞는다고 검토자가 승인한 경우에만 갱신합니다. 일반 CI가 자동으로 덮어쓰게 하지 말고 코드 변경과 함께 리뷰해야 합니다.

같은 코드에서 비교 결과가 달라지는 이유는 무엇인가요?

Xcode, Swift 컴파일러, SDK, 대상 아키텍처 또는 빌드 설정 차이가 흔한 원인입니다. 기준 생성과 비교에 동일한 도구 체인을 사용해야 합니다.

전용 물리 Mac mini

다음 빌드를 독립 물리 노드에서 실행하세요.

칩, 메모리, 저장 공간, 노드와 대여 기간을 선택해 다른 고객과 리소스를 공유하지 않는 클라우드 Mac을 이용하세요.

구성 선택 후 대여하기