Swift 6.2 접근 가능한 동시성 (Approachable Concurrency)
Swift 6.2의 동시성 모델을 채택하기 위한 패턴입니다. 이 모델에서는 코드가 기본적으로 단일 스레드에서 실행되며, 동시성은 명시적으로 도입됩니다. 일반적인 데이터 경합 오류를 제거하면서도 성능을 희생하지 않습니다.
활성화 시점
- Swift 5.x 또는 6.0/6.1 프로젝트를 Swift 6.2로 마이그레이션할 때
- 데이터 경합 안전성 컴파일러 오류를 해결할 때
- MainActor 기반 앱 아키텍처를 설계할 때
- CPU 집약적인 작업을 백그라운드 스레드로 오프로딩할 때
- MainActor에 격리된 타입에 프로토콜 적합성을 구현할 때
- Xcode 26에서 접근 가능한 동시성(Accessible Concurrency) 빌드 설정을 활성화할 때
핵심 문제: 암시적 백그라운드 오프로딩
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)
}
}
핵심 패턴 — 격리된 적합성 (Isolated Conformances)
MainActor 타입은 이제 비격리 프로토콜을 안전하게 준수할 수 있습니다:
protocol Exportable {
func export()
}
// Swift 6.1: 오류 — MainActor 격리 코드로 교차됩니다
// Swift 6.2: 격리된 적합성(isolated conformance)으로 OK
nonisolated func exportStickers(_ items: [any Exportable]) {
for item in items {
item.export() // 오류: @MainActor 적합성은 이곳에서 사용할 수 없습니다
}
}
컴파일러는 적합성이 MainActor에서만 사용되도록 보장합니다:
// 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 기본 유추 모드 (MainActor Default Inference Mode)
Swift 6.2는 MainActor가 기본적으로 유추되는 모드를 도입합니다 — 수동 어노테이션이 필요 없습니다:
// MainActor 기본 유추가 활성화된 경우:
final class StickerLibrary {
static let shared: StickerLibrary = .init() // 암시적으로 @MainActor
}
final class StickerModel {
let photoProcessor: PhotoProcessor
var selection: [PhotosPickerItem] // 암시적으로 @MainActor
}
extension StickerModel: Exportable { // 암시적으로 @MainActor 적합성
func export() {
photoProcessor.exportAsPNG()
}
}
이 모드는 선택 사항이며, 앱, 스크립트 및 기타 실행 가능한 타겟에 권장됩니다.
핵심 패턴 — 백그라운드 작업을 위한 @concurrent
실제 병렬 처리가 필요할 때는 @concurrent를 사용하여 명시적으로 오프로딩하세요:
중요: 이 예제는 접근 가능한 동시성 빌드 설정(SE-0466 MainActor 기본 격리 및 SE-0461 Nonisolated Nonsending by Default)이 필요합니다. 이러한 설정이 활성화되면
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 앱)
- 성능 최적화 — 특정 무거운 계산을 백그라운드로 오프로딩