MiniBin 엔지니어링 노트

클라우드 Mac 시간대와 로케일을 고정해 테스트 안정화하기

클라우드 Mac 시간대와 로케일을 고정해 테스트 안정화하기

같은 날짜 테스트가 로컬에서는 통과하지만 클라우드 Mac에서는 간헐적으로 하루 차이가 난다면, 먼저 XCTest를 의심할 필요는 없습니다. 빌드 프로세스, 시뮬레이터, 앱이 서로 다른 시간대, Locale 또는 Calendar를 사용하는 경우가 더 흔합니다. 이런 암묵적 의존성은 일광 절약 시간 전환 시점이나 월말, 자정을 지날 때 비로소 드러납니다.

네 가지 시간 의존성부터 식별하기

시간 문제는 단순히 “컴퓨터가 몇 시로 설정되어 있는가”에 그치지 않습니다. 프로젝트에는 적어도 네 계층의 상태가 존재합니다.

계층 일반적인 출처 대표적인 영향
호스트 시스템 시간대, 네트워크 시간 동기화 로그 시간, 격리되지 않은 스크립트
빌드 프로세스 TZ, LANG, LC_ALL Shell 도구, 생성 스크립트, 테스트 프로세스
시뮬레이터와 앱 실행 환경, 지역 설정 날짜 표시, 달력 계산
비즈니스 코드 Date(), Calendar.current 만료 판단, 날짜별 집계, 카운트다운

“현재 시간”은 안정적인 테스트 입력이 아닙니다. 단언이 실행 순간에 의존하는 한 테스트를 재현할 수 없습니다.

먼저 실패 유형을 분류해야 합니다. 항상 몇 시간씩 차이가 난다면 시간대를 우선 확인하고, 특정 언어 환경에서만 실패한다면 Locale을 확인합니다. 월말, 윤일 또는 일광 절약 시간 전환 시점 부근에서 실패한다면 Calendar와 날짜 연산을 살펴봐야 합니다. 자정 무렵에만 실패하는 경우에는 대개 Date()를 여러 번 읽었거나 현지 날짜 경계를 잘못 사용한 것이 원인입니다.

추측 대신 노드 기준선 수집하기

각 작업을 시작할 때 환경을 기록하되, 접근 자격 증명이나 전체 환경 변수를 출력해서는 안 됩니다. 다음 명령만으로도 시간 관련 기준선을 충분히 확보할 수 있습니다.

sw_vers
xcodebuild -version
date '+%Y-%m-%dT%H:%M:%S%z'
date -u '+%Y-%m-%dT%H:%M:%SZ'
sudo systemsetup -gettimezone
sudo systemsetup -getusingnetworktime
defaults read -g AppleLocale
defaults read -g AppleLanguages
locale

출력은 커밋 버전 및 작업 번호와 함께 보관합니다. 문제를 분석할 때는 실패한 작업만 살펴보지 말고 “성공한 작업”과 “실패한 작업”을 비교해야 합니다. MiniBin은 전용 물리 노드를 제공하지만, 동일한 노드에서 여러 빌드 작업이 동시에 실행될 수 있으므로 전역 설정은 여전히 작업 간 공유 상태가 됩니다.

작업을 실행할 때마다 systemsetup -settimezone을 반복해서 호출하지 마세요. 이 명령은 호스트 수준의 설정을 변경하므로 병렬 작업이 서로의 설정을 덮어쓸 수 있습니다. 노드 전체가 단일 작업만 직렬로 처리하고 해당 작업이 현지 시간대를 명시적으로 요구하는 경우가 아니라면, 프로세스 수준의 격리를 우선해야 합니다.

CI 프로세스에 결정론적 기준선 설정하기

빌드 스크립트에서는 UTC와 안정적인 문자 로케일을 일관되게 사용하고, 재현 가능한 타임스탬프를 지원하는 도구에 현재 커밋 시간을 제공할 수 있습니다.

#!/bin/zsh
set -euo pipefail

export TZ=UTC
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"

date -u '+build_started=%Y-%m-%dT%H:%M:%SZ'
xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -derivedDataPath "$PWD/.derived-data" \
  test

SOURCE_DATE_EPOCH는 Xcode의 모든 단계에 적용되는 만능 설정이 아닙니다. 이 규약을 지원하는 스크립트나 패키징 도구의 입력값으로만 사용해야 하며, 앱 내부의 시간 주입을 대체할 수는 없습니다.

또한 TZ=UTC는 해당 환경을 상속한 프로세스에만 영향을 줍니다. 이미 실행 중인 시뮬레이터나 백그라운드 서비스의 환경은 자동으로 갱신되지 않습니다. 따라서 출처가 불분명한 장기 실행 세션을 재사용하지 말고, 각 작업이 필요한 프로세스를 직접 시작한 뒤 작업 종료 시 정리해야 합니다.

앱에 시계, 시간대, 달력 주입하기

비즈니스 코드 곳곳에서 Date()Calendar.current를 직접 사용하면 테스트에서 경계 조건을 제어하기 어렵습니다. 더 안정적인 방법은 “현재 시각”을 의존성으로 추상화하는 것입니다.

protocol Clock {
    var now: Date { get }
}

struct SystemClock: Clock {
    var now: Date { Date() }
}

struct FixedClock: Clock {
    let now: Date
}

프로덕션 환경에는 SystemClock을, 테스트에는 FixedClock을 전달합니다. 하나의 비즈니스 작업에서는 now를 한 번만 읽어 실행 도중 초, 분 또는 자정을 넘어가는 상황을 방지해야 합니다.

날짜 연산 규칙도 명시적으로 지정해야 합니다. 서버 프로토콜의 시간에는 고정 형식, en_US_POSIX, 그레고리력과 UTC를 사용하고, 사용자 인터페이스에는 현재 Locale을 사용합니다. 형식이 적용된 한국어나 영어 날짜 문자열로 비즈니스 로직을 판단해서는 안 됩니다. 또한 “다음 현지 날짜”를 고정된 24시간의 초 단위 값으로 대체해서도 안 됩니다. 일광 절약 시간 전환으로 하루의 실제 길이가 달라질 수 있기 때문입니다.

경계 조건 샘플 포함하기

최소한 다음과 같은 고정 샘플을 준비해야 합니다.

  • UTC 날짜와 현지 날짜가 일치하지 않는 시각
  • 월말, 연말, 윤일
  • 일광 절약 시간의 시작 및 종료 시점 부근
  • 12시간제와 24시간제
  • 월요일 또는 일요일을 한 주의 시작으로 사용하는 지역
  • 비라틴 숫자 또는 서로 다른 날짜 순서를 사용하는 Locale

이 샘플은 시간대가 포함된 ISO 8601 입력값으로 직접 작성한 뒤 Date로 변환해야 합니다. 테스트가 실행되는 당일 날짜에 의존해서는 안 됩니다.

시뮬레이터와 UI 테스트 격리하기

UI 테스트에서는 “비즈니스 시간”과 “상태 막대 표시”를 구분해야 합니다. 상태 막대를 변경하면 스크린샷의 모양은 일정하게 만들 수 있지만, 앱이 Date()를 읽는 방식은 바뀌지 않습니다. 비즈니스 시간을 실제로 고정해야 한다면 실행 환경을 통해 테스트 값을 앱에 전달할 수 있습니다.

SIMCTL_CHILD_UITEST_FIXED_NOW='2026-07-23T12:00:00Z' \
xcrun simctl launch --terminate-running booted com.example.App

앱은 테스트 빌드에서만 UITEST_FIXED_NOW를 읽어야 합니다. 값 파싱에 실패하면 시스템 시간으로 조용히 되돌아가지 말고 테스트를 즉시 실패시켜야 합니다. 프로덕션 빌드에서는 이 입력 경로를 허용하지 마세요.

각 테스트 배치에서는 기기, 시스템 런타임, 언어, 지역 설정도 명확히 지정해야 합니다. 특정 테스트 케이스가 전역 Locale을 변경한 뒤 다음 배치에 그 상태를 남기게 해서는 안 됩니다. 여러 지역을 검증해야 한다면 지역별로 테스트 작업을 분리하거나 각 배치를 시작하기 전에 시뮬레이터 상태를 다시 구성하세요. 동일한 세션에서 설정을 계속 전환하는 것보다 결과를 해석하기 쉽습니다.

시간 조건을 빌드 인수 기준에 포함하기

최종 인수 단계에서는 단순히 “테스트 통과” 여부만 확인해서는 안 됩니다. 로그에 UTC 시간과 시간대 오프셋이 모두 포함되어 있는지, 실패 보고서에 Locale, Calendar, 테스트용 고정 시간이 기록되는지 확인해야 합니다. 또한 작업이 호스트의 전역 시간대를 변경하지 않았는지도 검증해야 합니다.

점검 순서는 기준선 수집, 프로세스 환경 설정, 새로운 테스트 세션 시작, 고정 시계 주입, 경계 조건 샘플 실행, 컨텍스트 보관으로 고정할 수 있습니다. 이렇게 하면 날짜가 다시 어긋났을 때 반복 실행으로 운에 맡기는 대신, 차이가 호스트, 프로세스, 시뮬레이터 또는 비즈니스 코드 중 어디에서 발생했는지 판단할 수 있습니다.

자주 묻는 질문

CI에 TZ=UTC를 설정하면 모든 날짜 테스트 문제가 해결되나요?

아닙니다. 해당 변수를 상속한 프로세스에만 적용됩니다. 시뮬레이터, 이미 실행 중인 앱, 명시적으로 구성한 Calendar는 다르게 동작할 수 있으므로 고정 시계와 관련 의존성도 주입해야 합니다.

모든 DateFormatter를 en_US_POSIX로 고정해야 하나요?

프로토콜 값과 기계가 처리하는 로그에는 적합합니다. 사용자 화면은 현재 Locale을 따라야 하며, 테스트에서는 확인할 대상 Locale을 명시적으로 전달해야 합니다.

각 CI 작업에서 시스템 전체 시간대를 바꾸면 안 되는 이유는 무엇인가요?

병렬 작업이 같은 전역 설정을 덮어쓰면 실행 순서에 따라 결과가 달라질 수 있습니다. 프로세스 범위 TZ와 앱 의존성 주입을 사용하면 작업을 더 안전하게 격리할 수 있습니다.

독점 물리 클라우드 Mac

재현 가능한 개발 환경을 독점 물리 노드에 배포

구성, 이용 기간과 다섯 개 노드 중 하나를 선택해 Xcode 빌드, 자동화 테스트, 원격 개발 또는 모델 추론에 사용하세요.

구성 선택 후 주문