엔지니어링 가이드

클라우드 Mac에서 git bisect로 Xcode 회귀 자동 추적

클라우드 Mac에서 git bisect로 Xcode 회귀 자동 추적

어떤 UI 테스트가 지난주부터 계속 실패하고 있지만, 그사이에 수십 개의 커밋이 병합되었습니다. 커밋을 하나씩 체크아웃하고 Xcode를 열어 테스트를 실행하는 방식은 느릴 뿐 아니라 환경 차이를 놓치기도 쉽습니다. 더 효과적인 방법은 “이 커밋이 조사 대상 오류를 유발했는가”를 반복 실행 가능한 명령으로 만든 뒤 git bisect에 이분 탐색을 맡기는 것입니다. 후보 커밋이 64개라면 이론적으로 약 6번만 판정하면 됩니다. 진짜 어려운 점은 이분 탐색 알고리즘이 아니라 매번 신뢰할 수 있는 결과를 얻는 데 있습니다.

먼저 회귀를 기계가 판정할 수 있는 결과로 정의하기

시작하기 전에 최소한 두 경계를 준비해야 합니다. 정상임이 확인된 커밋 하나와 비정상임이 확인된 커밋 하나입니다. 단순히 “페이지에 문제가 있다”고 정의하지 말고, 특정 XCTest 어설션 실패, 지정한 Scheme의 아카이브 실패, 고정 입력에 대한 잘못된 출력처럼 하나의 관찰 항목으로 회귀 범위를 좁혀야 합니다.

판정 조건에서는 다음 변수도 모두 고정해야 합니다.

  • Scheme, Configuration 및 테스트 대상
  • Xcode 선택 경로와 종속성 잠금 파일
  • 시뮬레이터 모델, 시스템 버전, 언어 및 시간대
  • 테스트 데이터, 네트워크 종속성 및 실행 계정
  • 정상과 비정상을 구분하는 단 하나의 근거

git bisect는 판정기의 오차를 그대로 증폭합니다. 우발적인 실패를 비정상 커밋으로 처리하면 이후 탐색이 순조롭게 끝나더라도 결론은 완전히 틀릴 수 있습니다.

먼저 정상 커밋과 비정상 커밋에서 동일한 명령을 각각 두 번 수동으로 실행합니다. 결과가 안정적으로 재현되지 않는다면 곧바로 이분 탐색을 시작하지 말고 테스트 격리부터 해결해야 합니다.

후보 커밋마다 실행 환경 격리하기

G-Mini의 클라우드 Mac에서 실행할 때는 전용 작업 복사본을 사용하고, 개발자가 편집 중인 디렉터리에서는 실행하지 않는 것이 좋습니다. 이분 탐색 과정에서는 커밋을 자주 전환하므로 추적되지 않는 파일, 자동 생성된 설정, 공유 캐시가 판정을 오염시킬 수 있습니다.

격리 항목 권장 방법 이유
Git 작업 공간 별도의 clone 또는 worktree 사용 일상적인 변경 사항이 덮어써지는 것을 방지
DerivedData 커밋 해시별로 디렉터리 분리 이전 산출물이 다른 커밋에서 재사용되는 것을 방지
시뮬레이터 UDID 고정 destination 자동 선택이 달라지는 것을 방지
결과 번들 각 실행 결과를 별도로 저장 최초 비정상 커밋을 다시 검토하기 쉬움
종속성 잠금 파일을 유지하고 암시적 업데이트 비활성화 버전 해석 결과가 달라지는 것을 방지

시작하기 전에 git status --porcelain을 실행하고 출력이 비어 있는지 확인해야 합니다. 시뮬레이터는 미리 생성하고 부팅한 다음 UDID를 환경 변수에 저장합니다. 판정 스크립트에서 이름만 보고 “사용 가능한 아무 기기”나 임시로 선택해서는 안 됩니다. 이름이 같은 기기나 시스템 업그레이드로 인해 실제 대상이 바뀔 수 있습니다.

3상태 Xcode 판정 스크립트 작성하기

git bisect run은 성공과 실패만 구분하지 않습니다. 종료 코드 0은 정상을, 1부터 127까지는 비정상을 뜻하며, 125는 현재 커밋을 판정할 수 없으므로 건너뛰어야 한다는 의미입니다. 이 세 번째 상태는 오래된 프로젝트에서 특히 중요합니다. 이전 커밋이 현재 종속성으로는 빌드되지 않더라도 조사 중인 회귀를 포함한다고 단정할 수는 없기 때문입니다.

다음 스크립트는 먼저 프로젝트를 빌드할 수 있는지 확인한 뒤 대상 테스트만 실행합니다. 기본 빌드가 실패하면 판정 불가로 분류하고, 테스트가 명확히 실패한 경우에만 비정상으로 처리합니다.

#!/bin/zsh
set -u

: "${DESTINATION_ID:?Set DESTINATION_ID first}"

sha="$(git rev-parse --short HEAD)"
root="${TMPDIR:-/tmp}/xcode-bisect"
derived="$root/derived-$sha"
result="$root/result-$sha.xcresult"
log="$root/test-$sha.log"

mkdir -p "$root"
rm -rf "$result"

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  -destination "platform=iOS Simulator,id=$DESTINATION_ID" \
  -derivedDataPath "$derived" \
  build >"$root/build-$sha.log" 2>&1

if [[ $? -ne 0 ]]; then
  exit 125
fi

xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -destination "platform=iOS Simulator,id=$DESTINATION_ID" \
  -derivedDataPath "$derived" \
  -resultBundlePath "$result" \
  -only-testing:AppTests/CheckoutReducerTests/testExpiredCart \
  test >"$log" 2>&1

status=$?

if [[ $status -eq 0 ]]; then
  exit 0
fi

if grep -q "TEST FAILED" "$log"; then
  exit 1
fi

exit 125

오류 유형에 따라 분류 조정하기

컴파일 회귀를 조사한다면 빌드 실패는 125가 아니라 1을 반환해야 합니다. 동작 회귀를 조사하는 경우에는 종속성 다운로드 실패, 시뮬레이터 서비스 오류, 디스크 쓰기 실패 같은 기본 환경 문제를 건너뛰어야 합니다. 모든 xcodebuild의 0이 아닌 종료 코드를 조사 대상 회귀로 일괄 해석해서는 안 됩니다.

이분 탐색을 시작하고 최초 비정상 커밋 재검증하기

스크립트가 준비되면 현재 브랜치와 작업 공간 상태를 기록한 뒤 다음을 실행합니다.

chmod +x ./scripts/bisect-xcode.sh
export DESTINATION_ID="固定的模拟器UDID"

git bisect start
git bisect bad BAD_COMMIT
git bisect good GOOD_COMMIT
git bisect run ./scripts/bisect-xcode.sh

탐색이 끝나면 Git이 최초 비정상 커밋을 알려 줍니다. 이때 문제를 바로 종료하지 말고 해당 커밋과 부모 커밋에서 스크립트를 각각 수동으로 실행한 다음, 저장된 빌드 로그와 .xcresult를 확인해야 합니다. 커밋 차이도 검토하여 관찰된 동작을 실제로 설명할 수 있는 변경인지, 우연히 다른 실패를 유발한 것인지 확인해야 합니다.

작업을 마친 뒤 git bisect reset을 실행해 원래 브랜치로 돌아갑니다. 이분 탐색 중 DerivedData가 많이 생성되었다면 재검증이 끝난 후 한꺼번에 정리할 수 있습니다. 조사 도중 결과 번들을 너무 일찍 삭제하면 판정 과정의 증거를 잃게 됩니다.

불안정한 테스트와 오래된 이력의 단절 처리하기

불안정한 테스트에는 다수결 적용하기

한 번의 실행 결과가 불안정하다면 판정기가 테스트를 연속으로 세 번 실행하도록 하고, 동일한 대상 오류가 최소 두 번 발생한 경우에만 1을 반환하게 할 수 있습니다. 세 번의 결과가 서로 모순되면 125를 반환합니다. 실행 시간은 늘어나지만 잘못된 커밋을 계속 조사하는 것보다는 비용이 적습니다. 반복 실행 전에도 테스트 데이터를 초기화하여 이전 실행의 상태가 다음 실행에 영향을 주지 않도록 해야 합니다.

건너뛰는 커밋이 너무 많으면 경계 좁히기

125가 지나치게 많으면 Git이 하나의 커밋을 특정하지 못할 수 있습니다. 일반적인 원인은 프로젝트 형식, 종속성 관리 방식 또는 테스트 이름이 이력 도중 변경된 경우입니다. 이때는 검색 범위를 마이그레이션 이후로 좁히거나 이전 디렉터리 구조를 지원하는 호환 분기를 추가해야 합니다. 다만 스크립트가 테스트 대상 소스 코드를 자동으로 수정하게 해서는 안 됩니다.

최종적으로 정상 커밋, 비정상 커밋, 최초 비정상 커밋, 판정 스크립트 버전, 시뮬레이터 UDID, Xcode 버전 및 결과 번들 경로를 보관해야 합니다. 그래야 다른 엔지니어가 같은 절차로 결과를 재현할 수 있고, 수정 커밋이 완료된 뒤에도 동일한 절차를 회귀 검증에 사용할 수 있습니다.

자주 묻는 질문

git bisect 판정 스크립트는 어떤 종료 코드를 반환해야 하나요?

정상 커밋은 0, 목표 회귀가 확인된 커밋은 1, 신뢰할 수 있게 판정할 수 없는 커밋은 125를 반환합니다. 시뮬레이터나 인프라 오류는 건너뛰어야 합니다.

간헐적으로 실패하는 Xcode 테스트도 git bisect로 찾을 수 있나요?

먼저 시뮬레이터, 언어, 시간대와 테스트 데이터를 고정해야 합니다. 각 커밋에서 테스트를 여러 번 실행하고 정해진 실패 임계값을 넘을 때만 문제 커밋으로 판정합니다.

전용 물리 노드

다음 빌드 큐를 위한 클라우드 Mac

두 가지 M4 구성과 5개 노드를 비교하고, 실제 작업 주기에 맞춰 일간·주간·월간·분기 임대를 선택하세요.

클라우드 Mac 요금제 선택