엔지니어링 글

클라우드 Mac CI에서 Swift 생성 코드 드리프트 방지하기

클라우드 Mac CI에서 Swift 생성 코드 드리프트 방지하기

동일한 커밋인데도 개발자 컴퓨터에서는 변경 사항이 없고, 클라우드 Mac CI에서 실행하면 Generated/에 수십 줄의 차이가 생길 수 있다. 더 까다로운 점은 프로젝트가 여전히 컴파일되기 때문에 오래된 상수, 누락된 리소스 키, 기한이 지난 mock이 그대로 결과물에 포함될 수 있다는 것이다. 이런 문제는 생성 명령을 한 번 더 실행하는 것만으로 해결할 수 없다. 생성기 버전, 입력 집합, 출력 디렉터리, 검증 규칙을 모두 빌드 계약에 포함해야 한다.

먼저 드리프트의 원인 파악하기

SwiftGen이나 Sourcery 같은 도구의 출력은 실행 파일 버전, 설정과 템플릿, 입력 파일, 실행 환경이라는 네 가지 요소에 의해 결정된다. 설정 파일만 고정하는 것으로는 충분하지 않다. 개발 환경의 PATH에 있는 도구가 CI 버전보다 최신일 수 있고, 디렉터리 상태에 따라 파일 순회 순서가 달라질 수 있으며, 로케일 설정이 정렬 순서나 날짜 형식에 영향을 줄 수도 있다.

먼저 로컬과 CI에서 각각 최소한의 실행 환경 정보를 기록한다.

set -euo pipefail

sw_vers
xcodebuild -version
swiftgen --version
sourcery --version
printf 'LANG=%s
' "${LANG:-unset}"
printf 'PWD=%s
' "$PWD"
git status --short

목표는 가능한 한 많은 정보를 수집하는 것이 아니라 생성 과정이 실제로 무엇을 읽었는지 확인하는 것이다. 템플릿에 현재 시각, 절대 경로, 무작위 식별자가 기록된다면 이런 비결정적 입력부터 제거해야 한다. 그렇지 않으면 실행할 때마다 타당해 보이지만 의미 없는 차이가 발생한다.

“다시 생성할 수 있다”는 것이 “동일한 결과를 생성할 수 있다”는 뜻은 아니다. 게이트에서는 입력이 같을 때 결과물이 바이트 단위까지 일치하는지를 판단한다.

재현 가능한 생성 기준선 구축하기

생성 디렉터리는 생성 도구가 전적으로 관리해야 하며, 사람이 직접 유지보수하는 Swift 파일과 섞어 두면 안 된다. 실행할 때마다 기존 디렉터리를 삭제하면 입력에서 제거되었지만 증분 생성 과정에서 남아 있던 타입을 발견할 수 있다. 설정, 템플릿, 입력 디렉터리에는 모두 저장소 루트 기준의 확정된 경로를 사용해 호출자의 현재 위치에 의존하지 않도록 한다.

각 항목의 책임은 다음 표처럼 정리하는 것이 좋다.

항목 고정 방법 CI 검증
생성기 버전을 고정하고 버전 출력 저장소에 선언된 버전과 일치
설정과 템플릿 버전 관리에 포함 작업 공간에 임시 변경 사항 없음
입력 디렉터리와 확장자를 명시 정렬 후 집합이 안정적으로 유지됨
출력 전용 디렉터리 사용 먼저 비운 뒤 생성
환경 로케일 설정 고정 시간과 절대 경로를 기록하지 않음

도구 설치 방식은 팀마다 달라도 되지만, 버전 선언의 출처는 하나여야 한다. 스크립트에 한 버전, 개발 문서에 다른 버전을 적어 두고 클라우드 Mac에서는 PATH의 세 번째 버전을 읽게 해서는 안 된다.

컴파일 전에 차이 검사하기

생성 단계를 컴파일 전에 배치하면 실패를 더 빠르게 확인할 수 있고, 컴파일 오류가 실제 원인을 가리는 일도 막을 수 있다. 다음 스크립트는 모든 생성 파일이 Generated/에 있으며 해당 파일들이 저장소에 커밋되어 있다고 가정한다.

set -euo pipefail

repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"

export LANG=C
export LC_ALL=C

rm -rf Generated
mkdir -p Generated build

swiftgen config run --config swiftgen.yml
sourcery --config sourcery.yml

find Generated -type f -print |
  LC_ALL=C sort |
  while IFS= read -r file; do
    shasum -a 256 "$file"
  done > build/generated.sha256

git diff --exit-code -- Generated
git status --short --untracked-files=all Generated

git diff는 추적 중인 파일의 변경 사항만 보여 주므로 추적되지 않은 파일도 확인해야 한다. 실제 게이트에서는 git status에 출력이 있으면 명시적으로 실패 처리하고, 차이 패치와 generated.sha256을 빌드 아티팩트로 보관해야 한다. 해시 목록은 차이 검사를 대체할 수 없지만 “이번 CI에서 정확히 어떤 파일 집합을 생성했는가”라는 질문에는 답할 수 있다.

세 가지 실패 유형 구분하기

형식만 달라졌다면 템플릿의 줄바꿈, 도구 버전, 포매터 실행 순서를 확인한다. 파일이 추가되거나 삭제되었다면 입력 집합과 디렉터리 초기화 로직을 점검한다. 내용의 값이 달라졌다면 리소스, API 명세, 템플릿 매개변수를 확인한다. 유형을 분류한 뒤 원인을 수정해야 하며, CI에서 결과물을 덮어쓴 채 컴파일을 계속해서는 안 된다.

Xcode 빌드 단계의 동시 실행 문제 피하기

여러 target이 동일한 생성 디렉터리를 공유하는 상황에서 각 Run Script Phase에 생성 명령을 넣으면 같은 파일에 동시에 쓸 수 있다. 어떤 때는 중복 실행으로 끝나지만, 파일이 잘리거나 일시적으로 사라지는 경우도 있다. 더 안정적인 방법은 생성을 CI의 독립적인 선행 단계로 분리하고, 완료된 후 모든 xcodebuild 작업을 시작하는 것이다.

반드시 Xcode 빌드 단계에서 실행해야 한다면 소유자를 하나만 두고 전체 입력 및 출력 파일 목록을 선언해야 한다. 생성 스크립트 자체가 프로젝트 파일을 수정해서는 안 되며, 생성 디렉터리를 읽는 동시에 그 디렉터리를 비워서도 안 된다. 병렬 빌드에 독립적인 출력이 필요하다면 작업 식별자별로 임시 디렉터리를 만들고, 검증을 통과한 뒤 한 번의 원자적 교체로 고정 위치에 배포할 수 있다.

또 다른 흔한 실수는 생성 직후 포매터를 실행하면서 로컬의 커밋 전 hook에서는 반대 순서를 사용하는 것이다. 생성, 포맷팅, 차이 검사는 항상 같은 순서로 실행해야 하며, 동일한 진입점 스크립트에서 처리해야 한다.

병합 전 및 장애 발생 시 체크리스트

생성기나 입력 변경 사항을 커밋할 때는 코드 리뷰에서 최소한 다음 항목을 확인해야 한다.

  1. 생성기 버전 선언이 함께 업데이트되었는가.
  2. 설정, 템플릿, 입력 변경 사항이 동일한 커밋에 포함되었는가.
  3. 입력을 삭제한 뒤 해당 생성 파일도 실제로 사라졌는가.
  4. 결과물에 시간, 사용자 이름, 절대 경로가 섞여 있지 않은가.
  5. 로컬에서 두 번 연속 실행했을 때 두 번째 실행에서 차이가 전혀 없는가.
  6. CI가 컴파일 전에 추적 파일과 미추적 파일을 모두 검사하는가.
  7. 병렬 작업이 서로 격리된 디렉터리에 출력하는가.
  8. 실패 시 버전 정보, 차이 패치, 해시 목록을 보관하는가.

문제를 조사할 때는 자동 수정부터 실행해 기존 상태를 덮어쓰지 않아야 한다. 먼저 git status, 도구 버전, 차이를 저장한 다음 깨끗하게 체크아웃한 환경에서 생성 진입점을 다시 실행한다. 깨끗한 체크아웃에서도 결과가 일치하지 않는다면 환경이나 고정되지 않은 입력이 원인이다. 재사용된 작업 공간에서만 실패한다면 오래된 파일, 캐시, 동시 쓰기를 중점적으로 확인해야 한다. 최종 기준은 간단하다. 동일한 커밋을 어떤 클라우드 Mac 작업에서든 두 번 연속 생성했을 때 두 번째 실행에서는 관찰 가능한 차이가 없어야 한다.

자주 묻는 질문

생성된 Swift 파일을 저장소에 커밋해야 하나요?

로컬 생성기 없이 개발하거나 코드 리뷰를 해야 한다면 커밋하는 편이 적절합니다. 단, CI에서 다시 생성하고 차이가 남으면 실패하도록 구성해야 합니다.

로컬과 클라우드 Mac CI의 생성 결과가 다른 이유는 무엇인가요?

주요 원인은 생성기 버전, 입력 순서, 로케일, 줄바꿈 형식, 작업 디렉터리입니다. 컴파일 전에 모든 조건을 명시하고 기록해야 합니다.

VMMini M4

전용 물리 노드에서 다음 작업 실행

노드와 결제 주기를 선택하고 고정된 M4, 16GB RAM 및 256GB SSD 구성으로 배포를 시작하세요. 실제 사용 가능 여부는 콘솔에 실시간으로 표시되는 결과를 기준으로 합니다.

지금 클라우드 Mac 대여