클라우드 Mac에서 Swift 6 동시성 전환 게이트 구축하기

클라우드 Mac에서 Swift 6 동시성 전환 게이트 구축하기

수년간 유지해 온 iOS 프로젝트를 Swift 6로 전환할 때 가장 위험한 방식은 전환 속도가 느린 것이 아니라, 언어 모드를 한 번에 활성화한 뒤 수백 건의 동시성 진단이 비즈니스 로직 변경 사항과 뒤섞인 상태로 발견되는 것입니다. 더 안정적인 방법은 클라우드 Mac에 별도의 검사 작업을 먼저 구축하는 것입니다. 도구 체인을 고정하고, 빌드 디렉터리를 격리하며, 현재 경고를 기록한 다음 모듈별로 검사를 단계적으로 강화합니다.

마이그레이션을 먼저 두 단계로 나누기

첫 번째 단계에서는 Swift 5 언어 모드를 그대로 사용하되 SWIFT_STRICT_CONCURRENCYcomplete로 설정합니다. 이 단계에서는 격리 도메인 간에 전달되는 비 Sendable 타입, 메인 스레드 격리 호출, 안전하지 않은 전역 가변 상태가 드러나지만 Swift 6의 모든 의미 체계 변경이 즉시 적용되지는 않습니다.

두 번째 단계에서 정리가 끝난 모듈만 Swift 6로 전환합니다. 의존성이 적은 기반 패키지부터 시작한 뒤 네트워크 계층, 데이터 계층, UI 계층 순으로 진행하는 것이 좋습니다. 리프 모듈을 먼저 안정화하면 상위 계층의 진단도 대체로 함께 줄어듭니다.

단계 언어 모드 검사 설정 병합 조건
문제 발견 Swift 5 complete 동시성 경고가 증가하지 않아야 함
모듈 정리 Swift 5 complete 대상 모듈의 경고가 0이어야 함
정식 전환 Swift 6 기본 엄격 규칙 모든 빌드와 테스트가 통과해야 함

동시성 마이그레이션 게이트의 목표는 첫날부터 모든 경고를 없애는 것이 아니라, 첫날부터 메인 브랜치에 새로운 문제가 추가되지 않도록 하는 것입니다.

재현 가능한 검사 작업 고정하기

클라우드 Mac에서 실행하는 작업은 Xcode 버전, Swift 컴파일러 버전, 커밋 버전을 명시적으로 기록해야 합니다. 개발자가 수동 빌드에서 남긴 DerivedData를 재사용하면 안 됩니다. 이전 모듈 캐시가 실제 의존 관계를 가릴 수 있기 때문입니다. 커밋마다 별도 디렉터리를 사용하고, 작업이 끝난 뒤 보존 정책에 따라 정리합니다.

#!/bin/zsh
set -o pipefail
export LC_ALL=C
root="$PWD/.ci"
derived="$root/DerivedData"
result="$root/ConcurrencyCheck.xcresult"
log="$root/concurrency.log"

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

xcodebuild -version
xcrun swiftc --version
git rev-parse HEAD

xcodebuild \
  -project Example.xcodeproj \
  -scheme Example \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$derived" \
  -resultBundlePath "$result" \
  SWIFT_VERSION=5.0 \
  SWIFT_STRICT_CONCURRENCY=complete \
  build 2>&1 | tee "$log"

build_status=${pipestatus[1]}
exit "$build_status"

project, scheme, 대상 플랫폼은 저장소 설정에서 제공해야 하며 여러 스크립트에서 중복 관리하지 않아야 합니다. 프로젝트가 workspace를 사용한다면 -project-workspace로 바꾸고, 공유 scheme이 버전 관리 저장소에 커밋되어 있는지도 확인합니다.

경고 예산으로 기존 메인 브랜치에 연결하기

오래된 프로젝트는 즉시 경고 0건을 달성하기 어려운 경우가 많습니다. 첫 실행 후 동시성 진단 수를 저장하고 이를 초기 예산으로 설정합니다. 이후 커밋에서 경고 수가 예산을 초과하면 작업을 실패 처리합니다. 문제를 한 묶음씩 해결할 때마다 같은 병합 요청에서 예산도 함께 낮춥니다.

budget=${CONCURRENCY_WARNING_BUDGET:-0}
count=$(grep -Ec \
  'warning:.*(Sendable|actor-isolated|concurrency-safe)' \
  .ci/concurrency.log || true)

printf 'concurrency warnings: %s, budget: %s
' "$count" "$budget"
test "$count" -le "$budget"

텍스트 일치는 과도기적 게이트로 적합하지만 LC_ALL=C를 고정해야 하며, Xcode를 업그레이드할 때마다 진단 문구를 다시 확인해야 합니다. 실패 시에는 빌드 결과 번들을 첨부 파일로 보존해, 일부가 잘린 터미널 로그만 남지 않도록 합니다. 최종적으로 예산이 0이 되면 모든 동시성 경고를 바로 금지할 수 있습니다.

생성 코드로 인한 오염 방지하기

API 클라이언트나 모델이 도구로 생성된다면 생성기 버전을 저장소 설정에 고정해야 합니다. 즉시 수정할 수 없는 외부 생성 코드는 별도 대상 모듈로 분리할 수 있지만, 비즈니스 모듈 전체를 엄격한 검사에서 제외해서는 안 됩니다. 그렇게 하면 새로 작성한 수동 코드까지 게이트를 우회하게 됩니다.

일괄 억제 대신 문제 유형별로 수정하기

Sendable 진단이 나타나면 먼저 해당 데이터를 실제로 태스크 간에 전달해야 하는지 판단합니다. 읽기 전용 설정은 값 타입 스냅샷으로 바꾸는 방식을 우선 고려합니다. 가변 캐시가 있는 참조 타입은 actor 안에 둘 수 있으며, 반드시 공유해야 하는 동기화 객체는 잠금과 접근 경계를 명확하게 캡슐화해야 합니다.

UI 상태는 구체적인 타입이나 메서드에 @MainActor를 적용해 격리해야 합니다. 검사를 빠르게 통과하려고 데이터 계층 전체나 모든 프로토콜에 @MainActor를 지정하면 안 됩니다. 이렇게 하면 백그라운드 작업이 암묵적으로 메인 스레드로 돌아오고 격리 도메인 간 호출도 더 늘어납니다.

@unchecked Sendable은 내부 잠금, 불변 설계 또는 직렬 큐로 안전성이 보장된 타입에만 적합합니다. 코드 리뷰에서는 최소한 세 가지 질문에 답해야 합니다. 어떤 필드가 보호되는지, 모든 읽기와 쓰기가 동일한 경계를 통과하는지, 앞으로 추가되는 필드도 어떻게 계속 보호할 것인지입니다. 답할 수 없다면 이 선언을 사용해서는 안 됩니다.

콜백을 async로 브리지할 때는 continuation이 한 번만 재개되는지도 검증해야 합니다. “콜백이 호출되지 않는 경우”와 “콜백이 중복 호출되는 경우”를 모두 테스트에 포함해야 하며, 컴파일러 경고만 사라지는 수준에서 수정을 끝내서는 안 됩니다.

모듈별로 전환하고 검증 증거 보존하기

각 모듈을 전환하기 전에 Swift 5 complete 모드에서 해당 모듈의 경고를 먼저 0으로 만든 뒤 Swift 6를 활성화합니다. 검증에는 최소한 Debug 빌드, Release 빌드, 단위 테스트, 핵심 통합 테스트가 포함되어야 합니다. 시뮬레이터용 Debug 빌드만 실행해서는 최적화 설정이나 조건부 컴파일 분기에서 발생하는 문제를 발견하기 어렵습니다.

각 마이그레이션마다 모듈 이름, 담당자, 전환 커밋, 남아 있는 예외, 결과 번들 경로를 기록하는 것이 좋습니다. Xcode를 업그레이드한다면 별도 브랜치에서 기준선을 먼저 다시 설정해야 합니다. 도구 체인 업그레이드, 동시성 수정, 비즈니스 기능을 하나의 변경 사항에 함께 넣지 마십시오.

마이그레이션이 끝난 뒤에도 엄격한 검사 작업은 유지해야 합니다. Swift 6는 기본 제약을 강화하지만 의존성 업데이트, Objective-C 경계, 생성 코드로 인해 격리의 빈틈이 다시 생길 수 있습니다. 안정적인 최종 상태란 “한때 경고가 0이었다”는 것이 아니라, 병합할 때마다 회귀가 없음을 입증할 수 있는 상태입니다.

자주 묻는 질문

프로젝트 전체를 즉시 Swift 6로 전환해야 하나요?

권장하지 않습니다. Swift 5 모드에서 complete 동시성 검사를 먼저 적용하고 하위 모듈부터 수정한 뒤 대상을 하나씩 전환해야 합니다.

@unchecked Sendable을 빠른 해결책으로 사용해도 되나요?

불변성, 잠금 또는 직렬 격리로 안전성이 이미 보장된 형식에만 사용하고 근거를 기록한 뒤 별도 코드 리뷰를 거쳐야 합니다.

기존 프로젝트에서 경고 게이트를 어떻게 시작하나요?

현재 경고 수를 초기 예산으로 기록하고 증가만 차단합니다. 문제를 수정할 때마다 예산을 낮춰 최종적으로 0을 만듭니다.

독점 물리 Mac mini

작업 기간에 맞는 클라우드 Mac 이용 기간 선택

두 가지 M4 구성을 일·주·월·분기 단위로 이용할 수 있으며, 노드와 실제 사용 가능 여부는 콘솔에서 실시간으로 확인되는 정보를 기준으로 합니다.

구성 선택 및 주문