(WWDC24 What's new in Swift 영상을 정리: https://www.youtube.com/watch?v=17fZZ1v_N2U&list=WL&index=10)
NonCopyable Types
Swift에서는 값타입, 참조타입 모두 기본적으로 복사가 가능함.
Non Copyable Types는 이런 복사 가능성을 제한 시킬 수 있음.
파일의 예시.
파일과 같은 고유 시스템 리소스는 자동으로 close하는 deinit을 사용해서 nonCopyable 구조체로 표현할 수 있음.
이 표현은 동일한 파일을 여러 곳에서 write하는 런타임 문제를 방지시켜주고,
자동으로 정리해주지 않으면 발생할 수 있는 메모리누수 문제도 해결해줌.
struct File: ~Copyable {
private let fd: CInt
init(descriptor: CInt) {
self.fd = descriptor
}
func write(buffer: [UInt8]) {
// ...
}
deinit {
close(fd)
}
}
하지만 위 코드의 문제점.
File 이니셜라이저에서 open file descripter를 사용해야 함.
이는 직관적이지도 않고 안전하지도 않음.
guard let fd = open(name) else {
return
}
let file = File(descriptor: fd)
file.write(buffer: data)
파일 열기와 이니셜라이저 사이에 exit을 작성하거나 throw되면 앱이 종료되고, deinit이 불리지 않아 리소스 누수가 발생됨.
💡
Swift 5.10에서는 concrete type에 대해서만 noncopyable types를 지원했었지만,
Swift 6.0에서부터는 모든 Generic 컨텍스트와 표준 라이브러리의 Optional, Result, Unsafe Pointers도 지원됨.
그래서 위 코드를 아래와 같이 개선이 가능해짐.
파일의 이름으로 초기화해서 failable 이니셜라이저로
파일 이름이 유효하면 파일을 열고 초기화하고, 그렇지 않으면 실패하게 만들 수 있음.
struct File: ~Copyable {
private let fd: CInt
init?(name: String) {
guard let fd = open(name) else {
return nil
}
self.fd = fd
}
func write(buffer: [UInt8]) {
// ...
}
deinit {
close(fd)
}
}
고유한 ownership의 표현 외에도 noncopyable 타입을 사용하면 성능을 세밀하게 조정이 가능.
메모리, 스토리지, 런타임 자원 등 심한 리소스 제약이 있는 낮은 수준의 시스템에서는 copy 비용이 매우 높아질 수 있어서 noncopyable을 활용하기 좋음.
Embedded Swift
작고 standalone한 바이너리를 만들 때, 런타임에 필요한 feature들(reflection, any 타입)의 기능을 제외 시킬 수 있음.
그리고 정적 링크, 전체 generics specialization을 사용해서 컴파일러가 바이너리를 생성해줌.
다양한 ARM, RISV-V microcontroller에서 사용할 수 있음.
Secure Enclave에서도 Embedded Swift를 사용함.
(C++ 상호운용성..은 생략)
Typed throws
Swift에서 에러는 Error 프로토콜을 따르고, throws 키워드를 통해 에러를 던지는 함수를 작성할 수 있음.
enum IntegerParseError: Error {
case nonDigitCharacter(String, index: String.Index)
}
func parse(string: String) throws -> Int {
for index in string.indices {
// ...
throw IntegerParseError.nonDigitCharacter(string, index: index)
}
}
do {
let value = try parse(string: "1+234")
}
catch let error as IntegerParseError {
// ...
}
catch {
// 에러가 'any Error'
}
위에서 catch된 에러는 any Error로 우리가 정의해 준 에러타입이 type erasure된 untyped.
그래서 동적으로 catch let error as IntegerParseError 와 같이 잡아주어야 했음.
이런 처리는 구문도 깔끔하지 못하지만, 런타임 자원이 제한적인 시스템에서는 문제가 될 수 있음.
그래서 Swift 6.0에서는 Typed throws를 지원.
throws 뒤 괄호내에 Error의 구체적 타입을 적어주면,
catch에서 type-erasure 되지 않은 구체적 Error타입으로 받을 수 있게 됨.
enum IntegerParseError: Error {
case nonDigitCharacter(String, index: String.Index)
}
func parse(string: String) throws(IntegerParseError) -> Int {
for index in string.indices {
// ...
throw IntegerParseError.nonDigitCharacter(string, index: index)
}
}
do {
let value = try parse(string: "1+234")
}
catch {
// error is 'IntegerParseError'
}
기존의 untyped throwing 함수는 any Error를 throw하는 함수와 동일하고,
func parse(string: String) throws -> Int {
//...
}
func parse(string: String) throws(any Error) -> Int {
//...
}
non-throwing 함수는 Never 타입을 throw하는 함수와 동일함.
func parse(string: String) -> Int {
//...
}
func parse(string: String) throws(Never) -> Int {
//...
}
발생한 에러 타입의 유연성을 가지려면 기존처럼 untyped throw를 사용하면 되고,
내부 함수나 매개변수에서 에러를 전달하려는 함수로 작업할 때나, 혹은 untyped throw로 비용이 많이 드는 환경에서는 typed throw가 적합함.
Data-race safety
Data-race는 여러 스레드가 데이터를 공유한 상태에서 어느 하나가 해당 데이터를 변경하려고 할 때 발생할 수 있음.
Data-race는 예상치 않은 런타임 동작을 하거나 크래시가 발생시킬 수 있음.
Swift 5.5에서 async/await이 도입될 때부터 data-race safety를 목표로,
Swift 5.6에서 Preconcurrency Incremental migration을
Swift 5.7에서 Sendable, Distributed actors를
Swift 5.9에서 Custom executors, Isolation assertion을
Swift 5.10에서 Full Data Isolation, Isolated Globals를 점진적으로 도입.
Swift 동시성을 Data 격리를 달성하기 위한 매커니즘, 변경가능한 상태를 보호하기 위한 actors, 안전한 Data 공유를 위한 Sendable을 중심으로 설계되었으며 Swift 5.10에서 완전한 동시성 체크 flag 사용하에 Data-race safety를 달성했음.
Swift 6.0 언어모드에서는 기본적으로 Data-race safety를 달성함.
앱의 모든 data-race문제를 컴파일타임에 오류를 발생시켜줌.
이는 앱의 보안을 대폭 개선시켜주고, crunch-time debugging을 줄여줌.
새로운 언어모드를 사용하면 코드의 조정이 필요할 수 있기 때문에, 준비가 되었을 때 도입할 수 있도록 함.
모듈별로 채택할 수도 있음. Swift 6.0 언어모드로 마이그레이션 되었을 수도, 안되었을 수도 있는 모듈간 상호운용성을 제공함.
Data-race safety는 Swift 6.0 언어모드를 활성화해서 적용되는 유일한 업데이트임.
다른 모든 신규 기능들은 Swift 6.0 컴파일러로 업데이트시 모두 사용할 수 있게됨.
Data-race 체크도 대폭 향상됨.
Swift 6.0에서는 원본 격리 상태로부터 더 이상 참조할 수 없는 시나리오에서 non-Sendable 값을 전달하는 것이 safety하다는 것을 인식할 수 있음.
MainActor에서 ClientStore actor로 non-Sendable Client 참조를 전달하면,
Swift 5.10에서는 전체 동시성 검사와 함께 컴파일러 경고가 발생됨.
class Client {
init(name: String, balance: Double) {}
}
actor ClientStore {
static let shared = ClientStore()
private var clients: [Client] = []
func addClient(_ client: Client) {
clients.append(client)
}
}
@MainActor
func openAccount(name: String, balance: Double) async {
let client = Client(name: name, balance: balance)
await ClientStore.shared.addClient(client) // ⚠️경고
}
그러나 실제로 Client 참조는 ClientStore actor로 전달된 후 더이상 MainActor에서 참조되지 않음.
MainActor와 ClientStore actor사이에 Client 상태가 공유되지 않으므로, data-race가 있을 수 없음.
Swift 6.0에서는 이러한 케이스에서 data-race 체크 기능이 향상되서 성공적으로 컴파일됨.
그리고 Client 참조를 ClientStore actor로 전달 후 아래와 같이 사용하면,
data-race를 알리는 컴파일 에러를 발생시킴.
...
@MainActor
func openAccount(name: String, balance: Double) async {
let client = Client(name: name, balance: balance)
await ClientStore.shared.addClient(client) // 🔴 Sending 'Client' risks causing data-races 🔴
logger.log("Opened account for \(client.name)")
}
Synchronization 모듈
Synchronization 모듈에 Atomics가 도입.
Atomic은 항상 let 프로퍼티로 저장되어야 함.
Atomic의 모든 작업은 C, C++ 메모리 모델과 유사한 메모리 순서 지정 인수를 명시적으로 사용.
import Dispatch
import Synchronization
let counter = Atomic<Int>(0)
DispatchQueue.concurrentPerform(iterations: 10) { _ in
for _ in 0 ..< 1_000_000 {
counter.wrappingAdd(1, ordering: .relaxed)
}
}
print(counter.load(ordering: .relaxed))
Synchronization 모듈에서 Mutex도 도입됨.
Atomic처럼 let 프로퍼티로 저장되어야 하고 동시에 안전하게 접근할 수 있게 해줌.
Mutex로 보호되는 저장소에 대한 모든 접근은 withLock 메서드에 전달된 클로저를 통해 이루어지고, 이를 통해 상호배타적인 접근을 보장.
import Synchronization
final class LockingResourceManager: Sendable {
let cache = Mutex<[String: Resource]>([:])
func save(_ resource: Resource, as key: String) {
cache.withLock {
$0[key] = resource
}
}
}
Swift 6.0 언어모드로 마이그레이션을 하기 위해서는 아래 가이드를 참조.
migration guide: https://www.swift.org/migration/documentation/migrationguide/
WWDC24 Migrate your app to Swift 6: https://www.youtube.com/watch?v=75-c6jSE8kU&list=WL&index=7