| name | swift-concurrency-6-2 |
| description | Swift 6.2 Approachable Concurrency 패턴입니다. 기본은 단일 스레드 실행이며, 명시적 백그라운드 오프로딩에는 `@concurrent`, 메인 액터 타입에는 isolated conformance를 사용합니다. |
Swift 6.2 Approachable Concurrency
코드는 기본적으로 단일 스레드에서 실행되고, 동시성은 명시적으로 도입되는 Swift 6.2의 동시성 모델을 적용하는 패턴입니다. 성능을 해치지 않으면서 흔한 데이터 레이스 오류를 줄입니다.
사용 시점
- Migrating Swift 5.x or 6.0/6.1 projects to Swift 6.2
- Resolving data-race safety compiler errors
- Designing MainActor-based app architecture
- Offloading CPU-intensive work to background threads
- Implementing protocol conformances on MainActor-isolated types
- Enabling Approachable Concurrency build settings in Xcode 26
핵심 문제: 암묵적 백그라운드 오프로딩
Swift 6.1 이하에서는 async 함수가 암묵적으로 백그라운드 스레드로 이동할 수 있었고, 겉으로 안전해 보이는 코드에서도 데이터 레이스 오류가 발생할 수 있었습니다.
@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)
}
}
Swift 6.2에서는 이 점이 바뀌었습니다. async 함수는 기본적으로 호출한 actor 위에 그대로 남습니다.
@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 Conformance
이제 MainActor 타입도 non-isolated 프로토콜에 안전하게 적합할 수 있습니다.
protocol Exportable {
func export()
}
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
컴파일러는 이 적합성이 메인 액터에서만 사용되도록 보장합니다.
@MainActor
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item)
}
}
nonisolated struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item)
}
}
핵심 패턴 — 전역 및 정적 변수
전역/정적 상태는 MainActor로 보호합니다.
final class StickerLibrary {
static let shared: StickerLibrary = .init()
}
@MainActor
final class StickerLibrary {
static let shared: StickerLibrary = .init()
}
MainActor 기본 추론 모드
Swift 6.2에는 MainActor를 기본으로 추론하는 모드가 들어왔습니다. 수동 어노테이션이 크게 줄어듭니다.
final class StickerLibrary {
static let shared: StickerLibrary = .init()
}
final class StickerModel {
let photoProcessor: PhotoProcessor
var selection: [PhotosPickerItem]
}
extension StickerModel: Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
이 모드는 opt-in이며 앱, 스크립트, 기타 실행 타깃에 권장됩니다.
핵심 패턴 — 백그라운드 작업용 @concurrent
실제 병렬 실행이 필요할 때만 @concurrent로 명시적으로 오프로딩합니다.
중요: 이 예시는 Approachable Concurrency 빌드 설정이 필요합니다. SE-0466(MainActor 기본 격리), SE-0461(NonisolatedNonsendingByDefault)을 켜야 합니다. 이 설정이 있으면 extractSticker는 호출한 actor에 남아 mutable 상태 접근이 안전해집니다. 이 설정이 없으면 데이터 레이스가 발생하며, 컴파일러가 이를 지적합니다.
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 { }
}
let processor = PhotoProcessor()
processedPhotos[item.id] = await processor.extractSticker(data: data, with: item.id)
@concurrent를 쓰려면:
- Mark the containing type as
nonisolated
- Add
@concurrent to the function
- Add
async if not already asynchronous
- Add
await at call sites
주요 설계 결정
| Decision | Rationale |
|---|
| 기본 단일 스레드 | 가장 자연스러운 코드가 데이터 레이스 없이 동작하고, 동시성은 opt-in |
| async는 호출 actor에 남음 | 데이터 레이스를 유발하던 암묵적 오프로딩 제거 |
| isolated conformance | MainActor 타입이 위험한 우회 없이 프로토콜 적합 가능 |
@concurrent 명시적 opt-in | 백그라운드 실행은 우발적이 아니라 의도적인 성능 선택 |
| MainActor 기본 추론 | 앱 타깃에서 @MainActor 보일러플레이트 감소 |
| 점진적 도입 | 비파괴적 마이그레이션 경로 제공 |
마이그레이션 단계
- Enable in Xcode: Swift Compiler > Concurrency section in Build Settings
- Enable in SPM: Use
SwiftSettings API in package manifest
- Use migration tooling: Automatic code changes via swift.org/migration
- Start with MainActor defaults: Enable inference mode for app targets
- Add
@concurrent where needed: Profile first, then offload hot paths
- Test thoroughly: Data-race issues become compile-time errors
모범 사례
- Start on MainActor — write single-threaded code first, optimize later
- Use
@concurrent only for CPU-intensive work — image processing, compression, complex computation
- Enable MainActor inference mode for app targets that are mostly single-threaded
- Profile before offloading — use Instruments to find actual bottlenecks
- Protect globals with MainActor — global/static mutable state needs actor isolation
- Use isolated conformances instead of
nonisolated workarounds or @Sendable wrappers
- Migrate incrementally — enable features one at a time in build settings
피해야 할 안티패턴
- Applying
@concurrent to every async function (most don't need background execution)
- Using
nonisolated to suppress compiler errors without understanding isolation
- Keeping legacy
DispatchQueue patterns when actors provide the same safety
- Skipping
model.availability checks in concurrency-related Foundation Models code
- Fighting the compiler — if it reports a data race, the code has a real concurrency issue
- Assuming all async code runs in the background (Swift 6.2 default: stays on calling actor)
사용 대상
- All new Swift 6.2+ projects (Approachable Concurrency is the recommended default)
- Migrating existing apps from Swift 5.x or 6.0/6.1 concurrency
- Resolving data-race safety compiler errors during Xcode 26 adoption
- Building MainActor-centric app architectures (most UI apps)
- Performance optimization — offloading specific heavy computations to background