개발 작업 노트

클라우드 Mac CI 파일 타임스탬프 드리프트 진단

클라우드 Mac CI 파일 타임스탬프 드리프트 진단

코드는 바뀌지 않았는데도 클라우드 Mac에서 두 번째 Xcode 빌드가 여전히 많은 타깃을 컴파일하고, 같은 커밋을 새 작업 공간에서 빌드하면 다시 정상적으로 동작할 수 있습니다. 이런 문제의 원인이 반드시 DerivedData인 것은 아닙니다. 소스 코드, 생성 파일 또는 캐시 산출물에 잘못된 수정 시각이 기록되어 있을 수도 있습니다. 캐시만 삭제하면 증상은 일시적으로 사라지지만, 동기화와 복원 절차를 바꾸지 않으면 타임스탬프 드리프트가 다시 작업 공간으로 유입됩니다.

타임스탬프 드리프트인지 먼저 확인하기

먼저 커밋, Scheme, Xcode 경로와 빌드 매개변수를 고정한 뒤 동일한 빌드를 연속으로 두 번 실행합니다. 첫 번째 빌드에서는 전체 중간 산출물이 생성되도록 두고, 두 번째 빌드에서 어떤 작업이 다시 실행되는지 확인합니다. 같은 Swift 컴파일, 리소스 처리 또는 스크립트 단계가 반복된다면 깨끗한 작업 공간과 재사용한 작업 공간의 결과를 비교합니다.

전체 빌드 시간만 확인해서는 안 됩니다. 반복 실행된 작업의 입력 경로, 스크립트 출력과 생성 디렉터리를 중점적으로 기록하고, 빌드 시작 전에 파일을 다시 쓰는 도구가 있는지도 점검해야 합니다. 대표적인 징후는 다음과 같습니다.

증분 빌드는 파일 내용에만 의존하지 않습니다. 입력과 출력의 시간 관계가 어긋나면 빌드 시스템은 이미 완료된 작업도 다시 실행해야 한다고 계속 판단할 수 있습니다.

미래 시각 파일과 비정상 디렉터리 스캔하기

먼저 시스템 시각과 시간대를 확인한 뒤 현재 시각보다 5분 넘게 미래로 설정된 파일을 스캔합니다. 5분의 허용 범위는 매우 짧은 측정 오차를 피하면서도 뚜렷한 드리프트를 발견하기에 충분합니다.

#!/bin/zsh
set -euo pipefail

root="${1:-$PWD}"
limit=$(( $(date +%s) + 300 ))

find "$root" -type f -print0 |
while IFS= read -r -d '' file; do
  modified=$(stat -f '%m' "$file")
  if (( modified > limit )); then
    printf '%s\t%s\n' \
      "$(date -r "$modified" '+%Y-%m-%d %H:%M:%S %z')" \
      "$file"
  fi
done

스캔 범위에는 최소한 소스 코드, 프로젝트 파일, 스크립트, 리소스, 코드 생성 디렉터리와 복원된 캐시가 포함되어야 합니다. 처음부터 사용자 디렉터리 전체를 스캔하면 패키지 관리자 캐시, 로그와 시스템 파일 때문에 불필요한 결과가 대량으로 발생합니다. 탐지된 파일에는 stat -x 文件路径을 추가로 실행해 수정 시각, 변경 시각과 소속 볼륨을 확인합니다.

비정상 파일이 특정 디렉터리에 집중되어 있다면 다운로드, 압축 해제, 동기화 또는 코드 생성 단계 중 하나로 원인을 빠르게 좁힐 수 있습니다. 저장소 전체에 분산되어 있다면 작업 공간 복사 방식과 작업 시작 전에 실행되는 초기화 스크립트를 점검해야 합니다.

원인에 따라 복구 방법 결정하기

원인마다 처리 방법이 다르므로 모든 문제를 touch로 덮어서는 안 됩니다.

현상 일반적인 원인 권장 처리
소수의 소스 파일 시각이 미래로 설정됨 압축 파일 또는 동기화 원본 이상 파일을 다시 가져오고 원본 시스템의 시각 확인
생성 파일의 시각이 매번 갱신됨 생성기가 조건 없이 파일을 덮어씀 내용이 바뀐 경우에만 원자적으로 교체
캐시 디렉터리의 시간 관계가 뒤섞임 복원 시 잘못된 메타데이터가 보존됨 해당 캐시를 폐기하고 캐시 키 재구성
작업 공간 전체의 시각이 복사 시점에 가까움 복사 매개변수가 메타데이터를 변경함 체크아웃 방식을 통일하고 여러 동기화 전략을 혼용하지 않음
출력이 입력보다 오래된 상태로 계속 유지됨 캐시 패키지 내부 시각 오류 캐시를 다시 생성하고 매니페스트 검증

Git으로 관리하는 소스 코드는 커밋하지 않은 변경 사항이 없음을 확인한 뒤, 저장소 전체의 시각을 다시 쓰는 대신 영향을 받은 디렉터리를 다시 체크아웃하는 것이 일반적으로 가장 안전합니다. 생성 파일은 생성기가 먼저 임시 파일에 기록하고, 내용을 비교한 다음 교체하도록 구성해야 합니다.

generate_config > Config.generated.swift.tmp
if ! cmp -s Config.generated.swift.tmp Config.generated.swift; then
  mv Config.generated.swift.tmp Config.generated.swift
else
  rm Config.generated.swift.tmp
fi

이렇게 하면 내용이 바뀌지 않았을 때 원본 파일의 수정 시각도 유지되므로 해당 파일에 의존하는 컴파일 작업이 불필요하게 다시 실행되지 않습니다.

캐시와 동기화 경계 점검하기

캐시를 복원하기 전에 어떤 디렉터리를 작업 간에 재사용할 수 있는지 명확히 정해야 합니다. 빌드 산출물, 모듈 캐시와 패키지 관리자 캐시는 수명 주기가 서로 다르므로 추적할 수 없는 하나의 대형 패키지로 묶어서는 안 됩니다. 캐시 매니페스트에는 최소한 캐시 키, Xcode 버전, 아키텍처, 생성 명령과 파일 수를 기록해야 합니다. 복원 후에는 먼저 타임스탬프 사전 검사를 수행한 뒤 빌드가 본 단계로 진입하도록 해야 합니다.

동기화 도구가 수정 시각을 보존하는지도 명확히 정해야 합니다. 수정 시각을 보존하면 증분 빌드 판단에는 유리하지만, 원본 시스템의 시각이 잘못된 경우 그 오류도 그대로 전파됩니다. 반대로 보존하지 않으면 모든 파일이 방금 수정된 것처럼 보일 수 있습니다. 핵심은 특정 매개변수를 고정하는 것이 아니라 팀 전체가 검증된 한 가지 전략만 사용하고, 그 전략을 작업 스크립트에 명시하는 것입니다.

코드를 압축 파일로 전달받는다면 먼저 격리된 디렉터리에 압축을 풉니다. 미래 시각 스캔, 파일 수 확인과 Git 상태 검사를 마친 뒤 정식 작업 공간으로 원자적으로 전환합니다. 이전 중간 산출물이 남아 있는 디렉터리에 곧바로 덮어쓰지 마십시오.

작업 진입점에 시간 기준선 추가하기

최종적으로는 의존성 해석과 Xcode 빌드 전에 이 검사를 실행해야 합니다. 미래 시각 파일이 발견되면 즉시 작업을 실패 처리하고 제한된 수의 경로만 출력합니다. 자동으로 시각을 수정한 뒤 빌드를 계속해서는 안 됩니다. 자동 수정은 장애 원인을 입증할 증거를 지우기 때문입니다.

각 작업에서 다음 정보를 보관하는 것이 좋습니다.

  1. date와 systemsetup -gettimezone의 출력
  2. 현재 커밋, Xcode 경로와 SDK 정보
  3. 미래 시각 파일 수와 그중 앞부분의 경로 몇 개
  4. 캐시 키와 복원 결과
  5. 첫 번째 빌드와 두 번째 빌드의 작업 차이

수정 후에는 같은 커밋으로 세 번 연속 실행합니다. 첫 번째 실행에서 캐시를 만들고, 두 번째 실행에서 증분 빌드 효과를 검증하며, 세 번째 실행에서 결과가 우연이 아님을 확인합니다. 두 번째와 세 번째 실행에서도 같은 작업이 반복된다면 다시 전체를 정리하지 말고 해당 작업의 입력 파일을 따라가며 추적해야 합니다. 타임스탬프 관리의 목적은 모든 파일의 시각을 같게 만드는 것이 아니라 소스 코드, 생성물과 캐시 사이에 설명하고 검증할 수 있는 시간적 선후 관계를 유지하는 것입니다.

자주 묻는 질문

미래 시각 파일을 찾으면 저장소 전체에 touch를 실행해도 되나요?

권장하지 않습니다. 모든 수정 시각이 바뀌어 대규모 재컴파일이 발생할 수 있습니다. 원인을 확인한 뒤 이상 파일만 수정하거나 해당 디렉터리를 다시 체크아웃하세요.

클린 빌드는 정상인데 증분 빌드만 계속 느려지는 이유는 무엇인가요?

증분 빌드는 기존 의존성 그래프와 출력 파일을 비교합니다. 미래 시각 입력이나 잘못 복원된 캐시는 의존성을 매번 오래된 상태로 판단하게 만들 수 있습니다.

전용 개발 환경

다음 개발 작업을 위한 클라우드 Mac 구성

Oak M4, Oak M4 Plus 또는 Oak M4 Pro를 선택하고 팀의 위치에 따라 5개 물리 노드 중에서 대여 구성을 설정하세요.

클라우드 Mac 구성