작년에 왔던 각설이가 죽지도 않고 또 오듯 올해도 WWDC 가 왔다. 서비스를 위한 앱 개발을 하는 입장에서 Swift, UIKit, SwiftUI 의 중요한 부분은 반드시 봐야겠다고 생각해서 세션을 보고 난 후 정리 겸 포스트를 작성한다. 나머지는 필요할 때 따로 공부할 생각이다.
Swift6 의 업데이트가 가시화 되었다고 본다. Apple 에서 공식적으로 출시일을 언급한 부분은 찾지 못했지만 9월로 많이 얘기하는 것 같다. 9월은 애플이 뭔 이벤트 하나 여는 때이기도 하니 합리적인 의심이라 생각된다.
이번 Swift 세션을 전체적으로 보면서 든 생각은 새로운 기능보다는 내부적인 개선 에 중점을 맞추었다고 본다.
현재 프리랜서인 필자는 Swift 를 통해 최대한 많은 프로젝트를 수행하는 것이 이득이다. 그런 입장에서 두 가지 키워드가 눈에 띄었다.
임베디드 프로젝트랑 간단한 서버 개발도 Swift 개발자가 할 수 있는 건가?
실제로 임베디드 관련 프로젝트는 잘잘하게 끊이지 않는 것 같다.
하지만 Swift 를 바로 시도할 만한 기업은 많지 않을 것 같다. 아직 이 자체가 Preview 이기도 하고, 업계 표준은 쉽게 바뀌지 않는다. 수많은 C 의 향연을 위의 이미지에서 볼 수 있다.
그리고 가장 중요한 문제는 Swift 로 만든 임베디드 프로그램이 디바이스 공급자가 만든 API 를 호출하기만 할 뿐이라는 것이다. 더 깊은 수준의 커스텀이 필요하다면 Swift 가 과연 좋은 선택일지는 의문이다.
Swift 기본, 설계 철학에 대한 세션이었다. 유튜브에서 "개발 공부 어떻게 하나요" 에 항상 빠지지 않는 얘기가 사용하는 언어의 철학에 대해 공부하라고 하는데 과연 그 질문에 대답할만한 내용을 담았는지 한번 살펴보고자 한다.
이 세션은 각 주제에 대해 깊이 다루기보단 모든 주제의 중심이 될만한 내용을 다루는 것이다.
이 세션에서 다룬 주제는 다음과 같다.
Value type 에 대한 간단한 코드를 보여주면서 알아둬야 할 내용을 정리해준다.
var x: Int = 1
var y: Int = x // 1
x = 42 // 42
y // 1
여기서 더 복잡한 데이터 타입은 struct 이다.
struct User {
let username: String
var isVisible: Bool = true
var friends: [String] = []
}
var alice = User(username: "alice")
alice.friends = ["charlie"]
var bruno = User(username: "bruno")
bruno.friends = alice.friends // 복사!!
alice.friends.append("dash")
bruno.friends
타입 자체는 더 복잡해져도 위의 3가지 사항을 어기지는 않는다. 여기서 또 다른 중요한 사항이 나온다.
위의 코드에 나온 변수들은 User 타입이고, 이는 값 타입이므로 안의 프로퍼티도 값 타입이다.
여기서 대놓고 Swift 는 struct 를 class 보다, let 을 var 보다 더 중요하게 생각한다고 하였는데 이는 전문 자체를 남기는 것이 좋을 것 같다.
Swift emphasizes value types and immutability because controlling when a value can change makes it much easier to reason about code, especially in tricky domains like concurrent programming.
스위프트는 값 타입과 불변성을 더 집중합니다. 그 이유는 변하는 시점을 컨트롤하는 것은 비동기 프로그래밍 같은 까다로운 영역에서 특히 코드를 더욱 읽기 쉽게 만듭니다.
Swift 의 에러 핸들링 철학은 3개 라고 한다.
이 세션에서는 Index-Out-Of-Bounds 보다는 데이터 자체의 Threshold 즉, 데이터가 가지지 말아야 할 상태 등에 대한 에러에 대해 상세히 얘기하려 한다(고 생각한다). 즉 Validation 에 대해 설명한다.
struct User {
let username: String
var isVisible: Bool = true
var friends: [String] = []
mutating func addFriend(username: String) throws {
guard username != self.username else {
throw SocialError.befriendingSelf
}
guard !friends.contains(username) else {
throw SocialError.duplicateFriend(username: username)
}
friends.append(username)
}
}
enum SocialError: Error {
case befriendingSelf
case duplicateFriend(username: String)
}
var alice = User(username: "alice")
do {
try alice.addFriend(username: "charlie")
try alice.addFriend(username: "charlie")
} catch {
error
}
var allUsers = [
"alice": alice
]
func findUser(_ username: String) -> User? {
allUsers[username]
}
if let charlie = findUser("charlie") {
print("Found \(charlie)")
} else {
print("charlie not found")
}
let dash = findUser("dash")!
코드에서 프로그래머는 중복된 데이터를 배열에 넣을 경우 정의된 에러를 Throw 하도록 하려 한다. enum 은 에러를 정의하는데 굉장히 유용하기 때문에 enum 을 사용한다.
이제 에러를 throw 할 수 있는 함수를 그냥 호출하려 하면 컴파일러는 에러를 handling 하지 않았다면서 경고를 표시한다. do-catch 블록으로 이를 해결한다.
에러와 동시에 원치 않는 상황을 핸들링하는 데엔 Optional 도 좋은 선택이다.
Optional은 값이 nil 혹은 특정 타입의 데이터인 값이다. 값을 사용하기 위해서는 이 Optional 을 걷어내기 위해 Optional-Binding 을 사용한다. 값이 nil 인 상황에서 발생할 크래시를 막는 데 Optional 이 유용하다.
Swift 의 코드 구조는 Modules, Package 라는 2개의 세부구조로 나뉜다. 패키지와 패키지는 서로 의존성을 가질 수 있다.
패키지는 Swift-Package-Manager 라는 툴을 통해 생성하고 관리할 수 있는데, 커맨드 라인을 통해 build/test/run 할 수 있다. IDE(Xcode, VSCode) 에서도 수정하고 실행할 수 있다. 패키지는 Swift-Package-Index 에 배포할 수 있다.
패키지를 만들 때 코드 베이스에서 중요한 것은 접근 제한자를 잘 선택하는 것이다. (대부분 public 으로 가는 것 같지만...)
공유 가능하며 수정 가능한 타입이 필요할 때 Swift 의 참조타입을 쓰면 되는데, Classes 가 대표적이다.
class Pet {
func speak() {}
}
class Cat: Pet {
override func speak() { print("meow") }
func purr() { print("purr") }
}
let pet: Pet = Cat()
pet.speak()
if let cat = pet as? Cat {
cat.purr()
}
Swift 는 단일 상속만을 지원한다. 다중 상속을 하려면 상속의 상속을 해야 한다 (A 가 B / C 를 상속하고 싶다면, B 가 C 를 상속하도록 한 다음 A 가 B 를 상속하는 등의 방법이 있지만 프로토콜 썼으면 좋겠다)
참조 타입을 다룰 때 중요하게 다뤄져야 하는 것이 메모리 관리이다. 얘기하자면 말이 길어지니 짧게 정리해본다.
다형성, 추상화는 객체지향 프로그래밍의 기본 특성이고 Swift 또한 그런 특성을 갖는다. 프로토콜은 타입이 추상화를 위해 구현해야 할 요구사항을 정의해놓은 선언문이다.
Swift 의 프로토콜의 예시로서 가장 많이 사용되는 것은 Collection 프로토콜이다. Collection 프로토콜을 implement 할 경우, 연속된 값을 다루는 Array/Dictionary/Set/String(Character 의 배열) 가 가진 map, filter, reduce, append, for-loop 에서 순환하기, 인덱스로 특정 데이터 접근하기 등을 사용할 수 있는 새로운 타입을 만들 수 있다.
/// An in-memory store for users of the service.
public class UserStore {
var allUsers: [String: User] = [:]
}
extension UserStore {
/// If the username maps to a User and that user is visible,
/// returns the User. Returns nil otherwise.
public func lookUpUser(_ username: String) -> User? {
guard let user = allUsers[username],
user.isVisible else {
return nil
}
return user
}
/// If the username maps to a User and that user is visible,
/// returns the User. Otherwise, throws an error.
public func user(for username: String) throws -> User {
guard let user = lookUpUser(username) else {
throw SocialError.userNotFound(username: username)
}
return user
}
public func friendsOfFriends(_ username: String) throws -> [String] {
let user = try user(for: username)
let excluded = Set(user.friends + [username])
return user.friends
.compactMap { lookUpUser($0) } // [String] -> [User]
.flatMap { $0.friends } // [User] -> [String]
.filter { !excluded.contains($0) } // drop excluded
.uniqued()
}
}
extension Collection where Element: Hashable {
func uniqued() -> [Element] {
let unique = Set(self)
return Array(unique)
}
}
Collection 프로토콜을 직접 구현하는 경우도 흔하지만, 위와 같이 uniqued() 메소드를 따로 사용하기 위해 타입을 extension 하는 경우는 흔한 것 같다.
저 코드에서 개인적으로 아쉬운 것은 위와 같은 extension 에는 private extension 을 우선적으로 고려했으면 좋겠다는 것이다. 다른 소스코드나 타입 등에 영향을 끼쳐서 전체 코드베이스에 안좋은 결과를 안길 수 있다.
프로토콜은 Generic 과 더하여 타입을 확장하는 데 아주 훌륭한 장점을 갖는다. 클래스의 상속을 통한 추상화보다 더 유연하기 때문에 자주 쓰이고 잘 알아둬야 한다.
Swift 의 동시성을 대표하는 것은 Task 이다.
Task 는 독립적이고 동시적으로 실행 가능한 컨텍스트를 뜻한다. Task 는 가벼워서 오버헤드가 적고, 취소하거나 작동을 멈추도록 할 수도 있다.
Task 에서 고려해봐야 할 점은 멈췄다가 실행하고 멈췄다가 실행한다 는 것이다. Task 가 멈춰있을 때는 CPU 를 yield 하기 때문에 CPU 자원도 효과적으로 사용할 수 있다.
Data-race(경쟁상태) 는 여기서 발생한다. 만약 수정가능한 상태값이 있을 때 서로 다른 스레드에서 접근하여 수정하거나 읽으려고 할 경우 원치 않는 결과를 가져올 수 있다.
Swift 6 는 경쟁상태를 해결하기 위해 컴파일 시점에 동시성에 사용할 값 타입에 대해 Sendable 프로토콜을 구현할 것을 요구한다. 이를 구현하였다는 것은 컴파일러에게 동시적으로 해당 값에 접근할 수 있음을 뜻하는 것이며, 값을 읽거나 쓸 때 많은 lock 이 실행되게 된다.
이를 안전하게 실행하기 위해 Actor 가 제공된다. Actor 는 참조 타입인데, 공유 가능하고 수정 가능한 상태값을 동기화 된 접근으로부터 캡슐화 한다. 하나의 Task 만이 Actor 의 실행 중 접근이 가능하다. Actor 의 메소드를 캡슐화 된 영역 바깥에서부터 호출하는 것이 비동기 프로그래밍이다.
extension UserStore {
static let shared = UserStore.makeSampleStore()
}
router.get("friendsOfFriends") { request, context -> [String] in
let username = try request.queryArgument(for: "username")
return try await UserStore.shared.friendsOfFriends(username) // actor 로 인한 비동기 접근
}
public actor UserStor {
package var allUsers: [String: User] = [:]
static func makeSampleStore() {
...
}
}
class 였던 UserStore 를 actor 로 바꿈으로써 UserStore 에 대한 비동기적 접근이 가능해졌다.
타입 확장은 프로토콜에서도 한번 다뤘지만, boiler-plate code, duplication 을 줄이는데 효과적이다.
대표적인 예시는 property wrapper 이다.
struct FriendsOfFriends: AsyncParsableCommand {
@Argument var username: String
mutating func run() async throws {
// ...
}
}
애플의 예시는 서버 애플리케이션의 Argument 로서 동작할 username 에 대한 코드라 좀 어려운 것 같다. 좀 더 간단한 예시는 역시 RGB 컬러를 저장하는 타입이지 않을까 싶다.
@propertyWrapper struct RGBElement {
private var value: Int = 0
var wrappedValue: Int {
get { self.value }
set { // RGB 색상에서 red/green/blue 는 0 부터 255 의 값만 가질 수 있다.
if 0 ... 255 ~= newValue {
self.value = newValue
} else {
self.value = newValue < 0 ? 0 : 255
}
}
}
}
struct SafeRGBColor {
@RGBElement var red: Int
@RGBElement var green: Int
@RGBElement var blue: Int
var color: UIColor {
UIColor(red: red, green: green, blue: blue, alpha: 1.0)
}
}
이 세션은 먼저 Swift 10주년 기념인 만큼 Swift 가 걸어온 길을 간단히 소개한다. (그러므로 나는 소개하지 않으려 한다. 본인도 흥미로운 부분들이 많았으니 궁금하신 분은 세션을 통해 직접 확인 바란다.)
개인적으로 눈에 띄는 부분은 ABI 가 공식적으로 지원되지 않았다는 것이었다. 머리털나고 처음 듣는 내용이라 위키를 찾아보니 아래와 같이 설명하고 있다.
응용 프로그램 이진 인터페이스(Application Binary Interface, ABI)는 응용 프로그램과 운영 체제 또는 응용 프로그램과 해당 라이브러리, 마지막으로 응용 프로그램의 구성요소 간에서 사용되는 낮은 수준의 인터페이스이다.
그리고 해당 세션의 전문에서는 이렇게 설명한다.
In Swift 5 we introduced the stable ABI on Apple Platforms. For app developers, this meant a smaller download size, because you no longer bundled a complete copy of the Swift standard library in your app.
Swift 5 에서 애플 플랫폼의 안정적인 ABI 를 공개했습니다. 앱 개발자에게 있어 더 이상은 Swift standard library 를 앱에 완전히 복사하여 번들링 할 필요가 없게 되었습니다.
가끔 클라이언트 측으로부터 앱 사이즈 측정을 요청받는 경우가 있는데 이거랑 연관이 있는건가 싶었다. Swift 4 가 2017년이니 ABI 로 앱 사이즈 개선이 많이 이뤄졌다는 것이 현업에 전달되기는 아직 이르다.
그 뒤에는 Swift 의 새로운 기능을 설명하는데 목록은 아래와 같다.
Cross compilation 이 뭘까? 영상에서는 이렇게 설명하고 있다.
MacOS 실행가능한 앱을 iPad 에서 실행하는 것과 같이 MacOS 실행가능한 앱을 Linux 에서 실행할 수 있도록 지원한다는 것이다. 앱은 MacOS 에서 개발하고 배포 및 실행은 Linux 에서 하는 것이다.
이를 위해 Linux SDK for Swift 를 제공한다. 앱 실행 및 개발을 위해 Linux SDK for Swift 외에 추가적인 Swift Package 설치가 필요하지 않다.
세션에서는 리눅스 서버에서 랜덤 이모지를 반환하는 간단한 API 를 제공하는 프로그램을 Swift Package 로 개발 및 배포하는 것을 보여준다.
swift sdk install ~/preview-static-swift-linux-0.0.1.tar.gz
명령어를 통해 Linux SDK for Swift 를 설치한다.swift build --swift-sdk aarch64-swift-linux-musl
로 다시 빌드하고 실행가능한 앱을 리눅스 서버로 복사(배포)한다.Swift Package 를 만드는 것에 익숙하지 않은 나로선 리눅스에서 실행 가능한 앱을 만든것보다 Swift Package 로 앱을 만드는 과정이 더 흥미로웠다.
Foundation 은 모든 애플 플랫폼에서 사용할 프레임워크로 만들어졌다. 이는 Swift 에서도 마찬가지였기 때문에 swift-corelib-foundation 이 만들어졌고, 이 둘을 하나의 프레임워크처럼 모든 애플 플랫폼에서 사용하기 위해 swift-foundation 이 만들어졌다.
swift-foundation 은 오픈소스이다. 누구든 contribute 할 수 있다.
새로운 프레임워크이고 오픈소스이다. 최신 Swift 기능을 탑재하였으며 코드베이스와 seamlessly integrate 한다고 한다.
// XCTest framework
import XCTest
func testRating(videoId: Int, videoName: String, expectedRating: String) {
let video = Video(id: videoId, name: videoName)
#expect(video.rating == expectedRating)
}
// Swift Testing
import Testing
@Test("Recognized rating", // display name
.tags(.critical), // tag
arguments: [ // add argument as test function
(1, "A Beach", "⭐️⭐️⭐️⭐️⭐️"),
(2, "Mystery Creek", "⭐️⭐️⭐️⭐️"),
])
func rating(videoId: Int, videoName: String, expectedRating: String) {
let video = Video(id: videoId, name: videoName)
#expect(video.rating == expectedRating)
}
위처럼 기존에는 test
라는 이름을 넣었어야 하는 것과 다르게 새로 추가된 매크로인 @Test
를 통해 여러 방식으로 커스텀이 가능해졌다.
자세한 사항은 따로 세션이 준비되어 있으므로 따로 참고하기 바란다.
빌드 과정에 향상된 부분이 있다고 한다.
하나의 모듈(소스코드 및 파일의 집합)은 다른 모듈에 의존성을 갖고 이는 SDK 까지 이어질 수도 있다. 빌드 과정에서 이 의존성을 파악하는 과정은 내부적으로 이루어진다.
이 과정은 굉장히 많은 작업량을 갖는데, 모듈의 의존성을 파악하는 과정이 병렬적이 아닌 순차적이기 때문이다. 여기다가 디버거가 위의 과정을 똑같이 반복하면 디버거가 처음 무언가를 출력하기 전까지는 또 한번의 긴 멈춤을 확인할 수 있다.
이번에 소개된 Explicitly built modules 를 통해 내부적인 빌드과정을 외부로 이동시키게 되었다. 이를 통해 아래의 개선사항이 있다고 한다.
물론 이를 강제하지는 않는 것 같다. Xcode 에 아래의 빌드 세팅이 추가되는 것을 기다려보자.
Swift 관련 프로젝트를 관리하는 레포지토리를 관리하기 위해 깃허브 프로젝트로 swiftlang 을 만들었다고 한다.
현재시각 2024-6-16 12:15PM. 레포지토리 수는 7개이다. 관련 프로젝트 몇가지만 확인되고 세션에서 언급한 Swift Compiler, Foundation 등은 확인되지 않는다. 앞으로 쭉 진행한다고 했으니 순차적으로 진행할 것인가보다.
개인적으로 깃허브 내 Swift 코드를 직접 참조 링크로 건 일이 있는데 주소가 바뀌면 한번 확인해봐야 겠다.
Swift 는 6버전이 되면서 data-race safety, 제한된 embedded 환경 제공을 위한 업데이트가 이뤄질 것이다. 이를 위해 아래 5가지 업데이트 사항이 제공될 것이다.
값 타입, 참조 타입 모두 Copyable 타입이었다. 하지만 이를 차단하는 Noncopyable 타입이 생겼다.
struct File: ~Copyable {
private let fd: CInt
init(descriptor: CInt) {
self.fd = descriptor
}
func write(buffer: [UInt8]) {
// ...
}
deinit {
close(fd)
}
}
Noncopyable 타입은 위와 같이 ~Copyable
로 선언할 수 있다. deinit 의 close 는 자동으로 실행된다고 하는데 직접 써보지 않으면 어떨지 모르겠다. Noncopyable 타입으로 가장 적절한 것은 앱이 사용하는 시스템의 자원이다. 위의 예시처럼 파일이 대표적일 것이다.
이를 통해 동시성에 의한 런타임 이슈인 스레드 간 경쟁상태를 제거하고, 자동 cleanup 함수를 사용하지 않아 파일이 닫히지 않은 resource leak 을 방지할 수 있다.
Noncopyable 타입이 resource leak 을 방지할 순 있지만 한 가지 주의사항이 있다. 하지만 위의 예시에서 문제가 하나 있는데 initializer 이다.
guard let fd = open(name) else { // open function
return
}
let file = File(descriptor: fd) // initialization
file.write(buffer: data)
위 코드에서 File descriptor 를 사용하는데 이는 직관적이지 않고 안전하지도 않다. open 함수에 의해 initialization 가 실행되지 않고 종료되면 deinit 이 실행되지 않고 resource leak 이 발생한다. 아래와 같이 코드를 변경해보자.
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)
}
}
우선 init 함수 자체가 직관적이다. 그리고 Optional init에 의해 Optional Noncopyable 타입이 생성되었다. Optional 은 대표적인 제네릭 타입인데, Swift 5.10 의 Noncopyable 타입은 제네릭 타입이 아닌 구체타입만 제공된다. 그 대신 Swift 6 은 제네릭으로 제공된다.
Noncopyable 의 목적은 무엇일까? 그것은 Unique ownership 을 통한 퍼포먼스 향상
이다. Noncopyable 타입으로 선언된 데이터는 특정 스레드나 Task 등만 소유하고 write/update 할 수 있다. 이를 통해 data-race 를 방지하고 성능도 향상한다는 얘기이다. 사람에 따라서는 다르게 들릴 수도 있을 것 같다.
Swift 개발자로 처음 입문하는 개발자들이 점점 진입하기 어려워지는 것 같다는 느낌도 든다...
Embedded Swift 는 Javascript 와 Typescript 의 관계처럼 Swift 의 서브셋이다. Swift 라는 말로 들린다. Embedded Swift 는 우선 작다. 작게 만들기 위해 새로운 컴파일 모델을 생성했다는 것이다.
이를 위해 제한된 사항 중 대표적인 것은 Mirror(swift reflection)
, any
이다. 컴파일 과정에서는 full generics specialization(무슨 소리인지 잘 모르겠다), static linking 을 통해 작은 바이너리를 생성한다.
ARM / RISC-V 등 여러 칩에서 작동 가능할 것이다.
작년의 C++ 상호작용과 관련하여 올해는 아래와 같은 개선사항이 있다고 한다. C++ 은 잘 몰라서 아래의 스크린샷으로 대체한다...
특정 타입의 에러를 핸들링 하기 위해서는 catch 구문에서 에러에 대해 as
로 타입 변환을 해줘야 했다. 이를 줄이기 위해 throws 키워드 뒤에 실제 에러의 concrete 타입을 정의할 수 있도록 하였다.
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'
}
이제 위와 같이 에러 타입을 선언하지 않는 것은 any Error 를 선언한 것과 같이 된다. 만약 throws 자체를 하지 않는 경우는 throws(Never)
와 같아진다.
이는 아래와 같이 응용할 수 있다. Collection 프로토콜의 map 을 확장하는 예시인데, map 에 전달할 클로저가 throws 를 할 경우 throws 할 에러의 타입을 명시할 수 있다.
extension Collection {
func map<T, Failure>(body: (Element) throws(Failure) -> T) throws(Failure) -> [T] {
// ....
}
}
물론 에러 타입을 제한하는 것이 제한사항은 아니다.
Swift 6 compiler 의 Swift 6 language mode 는 Data-race safety 를 기본 제공한다.(!!!)
Swift concurrency 가 나오고 나서 부터 Data-race safety 는 Swift concurrency 의 목표 중 하나였다. Swift concurrency 가 data isolation 메커니즘을 갖도록 디자인된 것도 이러한 이유였다.
Data-race safety default 를 제공하는 Swift 6 language mode 는 제한사항이 아니다(우선 지금은 그렇다). 모듈별로 준비가 되면 하나하나 스위치 켜듯 Swift 6 compiler 로 업데이트 하여 flag 값이 수정되면 진행된다.
구체적으로 어떤 일이 일어날까?
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)
// 에러 발생시키기!
logger.log("Opened account for \(client.name)")
}
Swift 5.10 에서는 addClient 쪽에 컴파일러 warning 이 뜬다. MainActor 에서 실행되는 actor 가 다루는, 새로 만들어진 client 변수의 Client 는 Sendable 을 구현하지 않기 때문이다.
하지만 client 는 MainActor 에서 addClient 더 이상 사용되지 않는다. 이를 통해 client 가 더 이상 공유되지 않는다는 것을 컴파일러가 인지하고 warning 이 발생하지 않아 컴파일이 정상적으로 작동한다.
하지만 MainActor 에서 client 를 통해 무언가를 하려고 하면 warning 이 아닌 error 를 볼 것이다.
추가로 저레벨의 Synchronization module 을 소개한다.
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))
import Synchronization
final class LockingResourceManager: Sendable {
let cache = Mutex<[String: Resource]>([:])
func save(_ resource: Resource, as key: String) {
cache.withLock {
$0[key] = resource
}
}
}
Swift 5 의 변천사를 가볍게 살펴보자. (출처)
이해가 가지 않는 몇가지 요소는 뺏다. 지금까지 살펴본 Swift 6 에 비해 굉장히 넓은 범위로 업데이트가 이뤄졌다.
Swift 6 는 이러한 업데이트를 더욱 향상된 방식으로 사용할 수 있도록 추가 업데이트를 계속 해나갈 생각인 것 같다. 그 첫번째는 Noncopyable 타입과 Swift 6 language mode 등을 통한 data-race safety, 그리고 Typed throws 를 통한 에러 핸들링 방식 개선이다.
Swift 5 에서 6 로 넘어가는 것은 생각보다 수월하지 않을까 생각한다.