/ IOS

[WWDC25] Swift 6.2 동시성 개선

왜 동시성이 어려웠을까요?

이전 Swift 6에서는 언어가 특정 작업을 개발자의 명시적 지시 없이 백그라운드 스레드나 다른 액터로 암시적으로 전환(오프로드)하는 경향이 있었습니다. 이 때문에 개발자가 의도하지 않더라도 데이터 경쟁(data race)에 취약한 코드가 컴파일러 오류를 유발할 수 있었죠. 동시성 프로그래밍은 여러 작업이 메모리를 공유하기 때문에 실수에 취약하며, 예상치 못한 결과로 이어질 수 있어 본질적으로 어렵습니다. **

이전 Swift 6은 암시적인 백그라운드 오프로드로 인해 의도치 않은 데이터 경쟁이 발생하기 쉬워 동시성 코딩이 복잡했습니다.


Swift 6.2의 새로운 동시성 접근 방식

Swift 6.2는 이러한 문제를 해결하기 위해 동시성에 대한 근본적인 접근 방식을 변경했습니다. 이제 개발자가 명시적으로 동시성을 도입하기로 결정할 때까지 코드가 기본적으로 단일 스레드 상태를 유지합니다. 이는 개발자가 가장 자연스럽게 작성하는 코드가 기본적으로 데이터 경쟁으로부터 자유롭도록 하여, 프로젝트에 동시성을 도입하는 더 쉬운 경로를 제공합니다. 즉, 코드를 병렬로 실행하고자 동시성을 선택적으로 도입할 경우에만 데이터 경쟁 안전이 보호됩니다.

Swift 6.2는 명시적인 동시성 도입 전까지 코드를 기본적으로 단일 스레드 상태로 유지하여 데이터 경쟁을 사전에 방지합니다.

image.png


핵심 동시성 개선 사항

image.png

1. 호출자의 액터에서 비동기 함수 실행 (Async Functions Run on the Caller’s Actor)

이전 Swift 6에서는 특정 비동기 함수가 언어에 의해 암시적으로 백그라운드로 오프로드되는 경우가 많았습니다. 이로 인해 개발자가 의도하지 않았음에도 데이터 경쟁에 취약한 코드가 컴파일러 오류를 유발할 수 있었습니다.

이전 Swift 6에서는 비동기 함수가 암시적으로 백그라운드에서 실행되어 데이터 경쟁을 유발할 수 있었습니다.

문제 상황 (Swift 6 이전):

예를 들어, PhotoProcessor 클래스에 extractSticker라는 비동기 메서드가 있고, 이 메서드가 MainActor에 격리된 StickerModel 클래스 내에서 호출될 때 문제가 발생할 수 있었습니다. extractSticker가 내부적으로 MainActor가 아닌 다른 스레드에서 실행되도록 암시적으로 오프로드될 경우, MainActor의 UI 코드와 공유 상태에 접근하려 할 때 데이터 경쟁 위험이 발생하여 컴파일러 오류가 발생할 수 있었습니다.

// Swift 6 (이전)
// 비동기 함수 호출 시 암시적 오프로드로 인한 잠재적 데이터 경쟁 오류
class PhotoProcessor {
    func extractSticker(data: Data, with id: String?) async -> Sticker? {
        // 이미지 처리 로직 (시간이 오래 걸릴 수 있음)
        // 이 함수가 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
        }
        // 여기서 photoProcessor.extractSticker 호출 시 데이터 경쟁 위험이 감지될 수 있음
        return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
        // 에러 메시지 예시: "Reference to property 'photoProcessor' in non-isolated actor-relative context cannot be used to satisfy an @MainActor requirement"
    }
}
  • StickerModel.extractSticker: StickerModel 클래스가 @MainActor 로 정의하고 있기 때문에 MainActor에서 실행
  • PhotoProcessor.extractSticker: MainActor에 격리되지 않아 백그라운드로 실행될 가능성 높음

Swift 6.2의 개선:

Swift 6.2에서는 특정 액터에 연결되지 않은 비동기 함수(actor-isolated가 아닌 async 함수)는 이제 해당 함수가 호출된 액터에서 계속 실행됩니다. 즉, PhotoProcessor의 extractSticker와 같이 액터가 지정되지 않은 비동기 함수를 MainActor 내에서 호출하면, 해당 함수는 MainActor 컨텍스트 내에서 실행됩니다. 이는 비동기 함수에 전달된 값이 액터 외부로 전송되지 않도록 하여 데이터 경쟁을 방지합니다. 개발자는 비동기 함수가 내부적으로 작업을 오프로드하더라도 가변 상태에 대해 걱정할 필요가 없어집니다. 예를 들어, UI 업데이트 로직에서 MainActor를 잘못 타서 발생할 수 있었던 데이터 경쟁 문제를 예방할 수 있습니다.

// Swift 6.2
// 호출자의 액터에서 비동기 함수 실행
class PhotoProcessor {
    func extractSticker(data: Data, with id: String?) async -> Sticker? {
        // 이 함수는 여전히 MainActor에 있지 않지만,
        // MainActor에서 호출되면 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
        }
        // 이제 이 호출은 MainActor 컨텍스트 내에서 안전하게 이루어집니다.
        return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
    }
}

Swift 6.2에서는 비동기 함수가 호출된 액터에서 계속 실행되도록 변경되어, MainActor 내에서의 데이터 경쟁 위험을 줄였습니다.

2. 격리된 적합성 (Isolated Conformances)

Swift 6.2는 MainActor와 같은 액터에 격리된 유형(actor-isolated types)이 프로토콜을 준수하는 것을 더 쉽게 만들었습니다.

Swift 6.2는 액터에 격리된 타입이 프로토콜을 더 쉽게 준수하도록 개선했습니다.

문제 상황 (Swift 6 이전):

MainActor에 격리된 StickerModel 클래스가 Exportable이라는 프로토콜을 준수해야 한다고 가정해 봅시다. Exportable 프로토콜은 액터 격리를 요구하지 않습니다. Swift 6에서는 컴파일러가 StickerModel의 export() 메서드가 MainActor 외부에서도 호출될 수 있다고 가정하여, MainActor 상태를 사용하는 것을 막았습니다. 이는 잠재적인 데이터 경쟁 때문입니다.

// Swift 6 (이전)
protocol Exportable {
    func export()
}

extension StickerModel: Exportable {
    // error: Conformance of 'StickerModel' to protocol 'Exportable' crosses into main actor-isolated code and can cause data races
    // StickerModel이 @MainActor로 격리되어 있기 때문에,
    // export() 메서드가 MainActor 외부에서 호출될 경우 데이터 경쟁이 발생할 수 있다고 컴파일러가 판단합니다.
    func export() { photoProcessor.exportAsPNG() }
}

Swift 6.2의 개선:

MainActor 상태를 필요로 하는 적합성은 이제 @MainActor 속성을 사용하여 ‘격리된 적합성(Isolated Conformance)’으로 명시할 수 있습니다. 컴파일러는 MainActor 적합성이 MainActor 내에서만 사용되도록 보장하여 안전성을 유지합니다. 개발자는 코드가 동시적으로 적합성을 사용할 때만 데이터 경쟁 안전 문제를 해결하면 됩니다.

// Swift 6.2
protocol Exportable {
    func export()
}

// @MainActor 속성을 사용하여 StickerModel의 Exportable 적합성이 MainActor에 격리됨을 명시
extension StickerModel: @MainActor Exportable {
    func export() { photoProcessor.exportAsPNG() }
}

// MainActor 격리된 ImageExporter 구조체 내에서 StickerModel을 사용하는 것은 안전합니다.
@MainActor struct ImageExporter {
    var items: [any Exportable]

    mutating func add(_ item: StickerModel) {
        items.append(item)
    }

    func exportAll() {
        for item in items {
            item.export()
        }
    }
}

// 하지만 만약 ImageExporter가 nonisolated로 선언된다면, MainActor에 격리된 StickerModel의 적합성을
// nonisolated 컨텍스트에서 사용하려 할 때 컴파일러 오류가 발생합니다.
// 이는 컴파일러가 MainActor의 상태가 안전하게 유지되도록 강제하는 것입니다.
nonisolated struct ImageExporter {
    var items: [any Exportable]

    mutating func add(_ item: StickerModel) {
        items.append(item) // error: Main actor-isolated conformance of 'StickerModel' to 'Exportable' cannot be used in nonisolated context
    }

    func exportAll() {
        for item in items {
            item.export()
        }
    }
}

이제 @MainActor 속성으로 프로토콜 적합성을 명시하여, MainActor 내에서 프로토콜을 안전하게 사용할 수 있습니다.

3. 기본 추론 모드 (Opt-in Mode to Infer MainActor by Default)

이 모드는 프로젝트의 모든 가변 상태를 기본적으로 MainActor로 보호하여, 안전하지 않은 전역 및 정적 변수 호출과 같은 데이터 경쟁 안전 오류를 제거하는 선택적(opt-in) 기능입니다. 이는 대부분 단일 스레드인 코드에서 동시성 관련 주석을 줄이는 데 도움이 되며, 앱, 스크립트 및 기타 실행 대상에 권장됩니다.

‘기본 추론 모드’는 프로젝트의 모든 가변 상태를 MainActor로 자동 보호하여 동시성 관련 주석을 줄여줍니다.

문제 상황 (Swift 6 이전):

전역 및 정적 변수는 어디서든 접근할 수 있는 가변 상태를 가질 수 있어 데이터 경쟁에 취약합니다. StickerLibrary.shared와 같은 정적 변수는 Sendable 타입이 아닌 경우 동시성 안전하지 않다는 경고 또는 오류를 발생시킬 수 있었습니다.

// Swift 6 (이전)
// 안전하지 않은 정적 변수 사용
final class StickerLibrary {
    static let shared: StickerLibrary = .init() // error: Static property 'shared' is not concurrency-safe because non-'Sendable' type 'StickerLibrary' may have shared mutable state
}

Swift 6.2의 개선:

이 모드를 활성화하면, 기본적으로 모든 가변 상태가 MainActor로 추론되어 보호됩니다. 따라서 개발자가 PhotoProcessor와 같은 특정 유형이나 파일 내에 캡슐화된 동시성 코드를 제외하고 대부분의 코드를 단일 스레드처럼 작성하더라도, MainActor 주석을 명시적으로 추가할 필요 없이 안전한 코드를 작성할 수 있습니다.

// Swift 6.2 (기본 MainActor 추론 모드 활성화 시)
// 명시적인 @MainActor 주석 없이도 안전한 코드
// (프로젝트 설정에서 'Infer Main Actor by Default' 활성화 가정)

final class StickerLibrary {
    // 이제 이 정적 변수는 자동으로 MainActor에 격리된 것으로 추론됩니다.
    static let shared: StickerLibrary = .init()
}

final class StickerModel {
    let photoProcessor: PhotoProcessor
    var selection: [PhotosPickerItem]
    // ...
}

extension StickerModel: Exportable {
    // 이 적합성도 MainActor에 격리된 것으로 자동으로 추론됩니다.
    func export() { photoProcessor.exportAsPNG() }
}

이 모드를 활성화하면 명시적인 MainActor 주석 없이도 대부분의 코드가 MainActor로 안전하게 보호됩니다.

4. @concurrent 속성 (The Attribute)

CPU 집약적인 작업을 백그라운드로 오프로드할 때 앱의 반응성을 유지하기 위해 @concurrent 속성이 도입되었습니다.

@concurrent 속성은 CPU 집약적인 작업을 백그라운드 스레드에서 실행하여 앱의 반응성을 유지하는 데 사용됩니다.

사용 목적:

이 속성을 사용하면 함수가 항상 동시 스레드 풀(concurrent thread pool)에서 실행되도록 하여, 현재 액터(예: MainActor)가 다른 작업을 동시에 수행할 수 있도록 합니다. 이는 UI 스레드를 차단하지 않고 무거운 작업을 처리할 때 특히 유용합니다.

샘플 코드:

PhotoProcessor 클래스에서 이미지를 분석하여 스티커를 추출하는 extractSubject 메서드가 CPU 집약적인 작업이라고 가정해 봅시다. 이 작업을 @concurrent로 표시하면, 이 함수가 호출될 때 MainActor와 같은 호출 액터를 차단하지 않고 별도의 동시 스레드 풀에서 실행될 것임을 보장합니다. 실제 CPU 집약적인 작업에는 Core ML 모델 실행, 복잡한 이미지 필터링, 대용량 데이터 처리 등이 포함될 수 있습니다.

// Swift 6.2
class PhotoProcessor {
    var cachedStickers: [String: Sticker] = [:]

    func extractSticker(data: Data, with id: String) async -> Sticker {
        if let sticker = cachedStickers[id] {
            return sticker // 캐시된 스티커가 있다면 즉시 반환
        }

        // extractSubject 메서드는 CPU 집약적이므로 @concurrent 속성을 사용하여 백그라운드 스레드에서 실행
        let sticker = await Self.extractSubject(from: data)
        cachedStickers[id] = sticker
        return sticker
    }

    // @concurrent 속성: 이 함수는 항상 동시 스레드 풀에서 실행됩니다.
    @concurrent static func extractSubject(from data: Data) async -> Sticker {
        // 실제 CPU 집약적인 이미지 처리 및 피사체 추출 로직
        // 예: Core ML 모델 실행, 복잡한 이미지 필터링 등
        // 이 작업은 MainActor를 차단하지 않습니다.
        return Sticker() // 예시 반환
    }
}

@concurrent는 MainActor를 차단하지 않고 무거운 작업을 병렬 스레드에서 효율적으로 처리하도록 보장합니다.


더 쉬워진 동시성, 더 강력해진 Swift

image.png

이러한 언어 변경 사항들은 함께 작동하여 동시성을 더 쉽게 다룰 수 있도록 돕습니다. 개발자는 먼저 데이터 경쟁 위험이 없는 MainActor에서 기본적으로 실행되는 코드를 작성하고, 필요할 때 병렬 실행을 위해 특정 코드를 안전하게 오프로드할 수 있습니다. 또한, Swift 6.2는 비동기 코드의 디버깅 경험을 크게 개선하여 LLDB가 스레드 간 전환 시에도 비동기 함수 실행을 추적하고, 작업 이름 지정 및 가시성을 제공합니다.

이러한 개선은 Swift 커뮤니티의 피드백을 통해 이루어졌으며, Swift가 초보자와 전문가 모두에게 더 쉽게 사용될 수 있도록 언어를 발전시키는 데 기여합니다.

Swift 6.2의 개선 사항들은 동시성 코딩을 단순화하고 디버깅 경험을 향상시켜 Swift를 더욱 강력하게 만듭니다.


어떻게 적용할 수 있나요?

이러한 동시성 관련 언어 변경 사항들은 선택 사항으로 구성되었으며, Xcode 빌드 설정의 Swift 컴파일러 - 동시성 섹션에서 활성화할 수 있습니다. SwiftSettings API를 사용하여 Swift 패키지 매니페스트 파일에서도 이 기능을 활성화할 수 있습니다.

Xcode 빌드 설정에서 활성화:

Swift Compiler - Concurrency 섹션에서 “Strict Concurrency Checking”을 Complete로 설정하고 “Infer Main Actor by Default”를 활성화할 수 있습니다.

또한, Swift 6.2에는 필요한 코드 변경 사항을 자동으로 적용할 수 있는 마이그레이션 도구가 포함되어 있습니다. 이 도구는 주로 액터 격리 관련 경고 및 오류를 수정하고, Sendable 준수와 관련된 변경 사항을 제안하여 기존 코드를 새 동시성 모델에 맞게 업데이트하는 데 도움을 줍니다.

Swift 6.2의 동시성 개선은 개발자들이 더욱 안전하고 효율적인 애플리케이션을 만들 수 있도록 돕는 중요한 진전입니다. 지금 바로 Xcode를 업데이트하고 새로운 동시성 기능을 경험해 보세요!

Swift 6.2 동시성 기능은 Xcode 빌드 설정이나 Swift Package Manager를 통해 활성화할 수 있으며, 기존 코드 마이그레이션 도구도 제공됩니다.

참고: 이 글은 WWDC25 비디오 (“Improve memory usage and performance with Swift”, “What’s new in Swift”) 및 관련 Swift 6.2 발표 자료를 바탕으로 작성되었습니다.