Swift 6.2 접근 가능한 동시성
코드가 기본적으로 단일 스레드로 실행되고 동시성이 명시적으로 도입되는 Swift 6.2 동시성 모델을 채택하기 위한 패턴입니다. 성능 저하 없이 일반적인 데이터 경합 오류를 제거합니다.
활성화 시점
- Swift 5.x 또는 6.0/6.1 프로젝트를 Swift 6.2로 마이그레이션할 때
- 데이터 경합 안전성 컴파일러 오류를 해결할 때
- MainActor 기반 앱 아키텍처를 설계할 때
- CPU 집약적 작업을 백그라운드 스레드로 오프로드할 때
- MainActor 격리 타입에 프로토콜 준수를 구현할 때
- Xcode 26에서 접근 가능한 동시성 빌드 설정을 활성화할 때
핵심 문제: 암시적 백그라운드 오프로드
Swift 6.1 및 이전 버전에서는 async 함수가 암시적으로 백그라운드 스레드로 오프로드될 수 있어 겉보기에 안전한 코드에서도 데이터 경합 오류가 발생했습니다:
// Swift 6.1: 오류
@MainActor
final class StickerModel {
let photoProcessor = PhotoProcessor()
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
guard let data = try await item.loadTransferable(type: Data.self) else { return nil }
// 오류: 'self.photoProcessor'를 전송하면 데이터 경합이 발생할 위험이 있습니다
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}
Swift 6.2에서는 이 문제가 수정되었습니다. async 함수는 기본적으로 호출한 액터에 유지됩니다.
// Swift 6.2: OK — async가 MainActor에 유지되며 데이터 경합 없음
@MainActor
final class StickerModel {
let photoProcessor = PhotoProcessor()
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
guard let data = try await item.loadTransferable(type: Data.self) else { return nil }
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}
핵심 패턴 — 격리된 준수
MainActor 타입은 이제 비격리 프로토콜에 안전하게 준수할 수 있습니다:
protocol Exportable {
func export()
}
// Swift 6.1: 오류 — 메인 액터 격리 코드로 넘어갑니다
// Swift 6.2: 격리된 준수로 OK
@MainActor extension StickerModel: Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
컴파일러는 준수가 메인 액터에서만 사용되도록 보장합니다:
// OK — ImageExporter도 @MainActor
@MainActor
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item) // 안전: 동일한 액터 격리
}
}
// 오류 — 비격리 컨텍스트는 MainActor 준수를 사용할 수 없음
nonisolated struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item) // 오류: MainActor 격리 준수를 여기서 사용할 수 없음
}
}
핵심 패턴 — 전역 및 정적 변수
MainActor로 전역/정적 상태를 보호하세요:
// Swift 6.1: 오류 — Sendable이 아닌 타입이 공유 가능한 변경 상태를 가질 수 있음
final class StickerLibrary {
static let shared: StickerLibrary = .init() // 오류
}
// 해결 방법: @MainActor로 주석 추가
@MainActor
final class StickerLibrary {
static let shared: StickerLibrary = .init() // OK
}
MainActor 기본 추론 모드
Swift 6.2는 MainActor가 기본적으로 추론되는 모드를 도입합니다. 수동 주석이 필요하지 않습니다:
// MainActor 기본 추론 활성화 시:
final class StickerLibrary {
static let shared: StickerLibrary = .init() // 암시적으로 @MainActor
}
final class StickerModel {
let photoProcessor: PhotoProcessor
var selection: [PhotosPickerItem] // 암시적으로 @MainActor
}
@MainActor extension StickerModel: Exportable { // 암시적으로 @MainActor 준수
func export() {
photoProcessor.exportAsPNG()
}
}
이 모드는 옵트인 방식이며 앱, 스크립트 및 기타 실행 가능 타겟에 권장됩니다.
핵심 패턴 — 백그라운드 작업을 위한 @concurrent
실제 병렬 처리가 필요할 때는 @concurrent로 명시적으로 오프로드하세요:
중요: 이 예제는 접근 가능한 동시성 빌드 설정(SE-0466 MainActor 기본 격리 및 SE-0461 NonisolatedNonsendingByDefault)이 필요합니다. 이 설정이 활성화되면
extractSticker는 호출자의 액터에 유지되어 변경 가능한 상태 접근이 안전해집니다. 이 설정이 없으면 이 코드에 데이터 경합이 있습니다 — 컴파일러가 이를 플래그합니다.
nonisolated final class PhotoProcessor {
private var cachedStickers: [String: Sticker] = [:]
func extractSticker(data: Data, with id: String) async -> Sticker {
if let sticker = cachedStickers[id] {
return sticker
}
let sticker = await Self.extractSubject(from: data)
cachedStickers[id] = sticker
return sticker
}
// 비용이 많이 드는 작업을 동시 스레드 풀로 오프로드
@concurrent
static func extractSubject(from data: Data) async -> Sticker { /* ... */ }
}
// 호출자는 await 해야 함
let processor = PhotoProcessor()
processedPhotos[item.id] = await processor.extractSticker(data: data, with: item.id)
@concurrent를 사용하려면:
- 포함하는 타입을
nonisolated로 표시 - 함수에
@concurrent추가 - 아직 비동기가 아니면
async추가 - 호출 지점에
await추가
주요 설계 결정
| 결정 | 근거 |
|---|---|
| 기본 단일 스레드 | 가장 자연스러운 코드는 데이터 경합이 없음; 동시성은 옵트인 |
| Async는 호출 액터에 유지 | 데이터 경합 오류를 유발한 암시적 오프로드 제거 |
| 격리된 준수 | MainActor 타입이 안전하지 않은 해결 방법 없이 프로토콜 준수 가능 |
@concurrent 명시적 옵트인 | 백그라운드 실행은 의도적인 성능 선택이며 우연이 아님 |
| MainActor 기본 추론 | 앱 타겟에 대한 @MainActor 주석 상용구 감소 |
| 옵트인 채택 | 중단 없는 마이그레이션 경로 — 기능을 점진적으로 활성화 |
마이그레이션 단계
- Xcode에서 활성화: 빌드 설정에서 Swift 컴파일러 > 동시성 섹션
- SPM에서 활성화: 패키지 매니페스트에서
SwiftSettingsAPI 사용 - 마이그레이션 도구 사용: swift.org/migration을 통한 자동 코드 변경
- MainActor 기본값부터 시작: 앱 타겟에 대해 추론 모드 활성화
- 필요한 곳에
@concurrent추가: 먼저 프로파일링한 후 핫 경로 오프로드 - 철저히 테스트: 데이터 경합 문제가 컴파일 타임 오류로 전환됨
모범 사례
- MainActor에서 시작 — 먼저 단일 스레드 코드를 작성한 후 최적화
- CPU 집약적 작업에만
@concurrent사용 — 이미지 처리, 압축, 복잡한 계산 - 대부분 단일 스레드인 앱 타겟에 MainActor 추론 모드 활성화
- 오프로드 전에 프로파일링 — Instruments를 사용하여 실제 병목 지점 찾기
- MainActor로 전역 변수 보호 — 전역/정적 변경 가능 상태는 액터 격리 필요
nonisolated해결 방법이나@Sendable래퍼 대신 격리된 준수 사용- 점진적으로 마이그레이션 — 빌드 설정에서 기능을 하나씩 활성화
피해야 할 안티 패턴
- 모든 async 함수에
@concurrent적용 (대부분 백그라운드 실행이 필요하지 않음) - 격리를 이해하지 않고 컴파일러 오류를 억제하기 위해
nonisolated사용 - 액터가 동일한 안전성을 제공할 때 레거시
DispatchQueue패턴 유지 - 동시성 관련 Foundation Models 코드에서
model.availability검사 건너뛰기 - 컴파일러와 싸우기 — 데이터 경합을 보고하면 코드에 실제 동시성 문제가 있는 것
- 모든 async 코드가 백그라운드에서 실행된다고 가정 (Swift 6.2 기본값: 호출 액터에 유지)
사용 시기
- 모든 새로운 Swift 6.2+ 프로젝트 (접근 가능한 동시성이 권장 기본값)
- Swift 5.x 또는 6.0/6.1 동시성에서 기존 앱 마이그레이션
- Xcode 26 채택 중 데이터 경합 안전성 컴파일러 오류 해결
- MainActor 중심 앱 아키텍처 구축 (대부분의 UI 앱)
- 성능 최적화 — 특정 무거운 계산을 백그라운드로 오프로드