이와 같은 공통점이 있지만, 구조체에서는 안되지만 클래스에서는 할 수 있는 기능이 다음과 같이 있습니다.
- 클래스의 특성을 다른 클래스에게 물려줄 수 없음
- 실행 시 컴파일러가 클래스 인스턴스의 타입을 미리 파악하고 검사할 수 있음
- 인스턴스가 소멸되기 직전에 처리해야 할 구문을 미리 등록해 놓을 수 있음 (deinit)
- 클래스 인스턴스가 전달될 때에는 참조 형식으로 제공되며, 참조가 가능한 개수는 제약이 없음
또한 클래스는 참조형식, 구조체는 값형식 프로퍼티로 클래스는 값이 변경됨에 따라 참조된 클래스의 값에 영향을 주고 구조체는 영향을 주지 않는 성질을 갖고 있습니다.
// 구조체
struct User {
var name: String
var age: Int
}
var userStruct = User(name: "", age: 0)
var cheolsu = userStruct
var yuri = userStruct
print("cheolsu: \(cheolsu)")
print("yuri: \(yuri)")
yuri.name = "yuri"
yuri.age = 5
print("cheolsu: \(cheolsu)")
print("yuri: \(yuri)")
// 출력 결과
cheolsu: User(name: "", age: 0)
yuri: User(name: "", age: 0)
cheolsu: User(name: "", age: 0)
yuri: User(name: "yuri", age: 5)
// yuri의 값을 변경했지만 cheolsu 객체에 영향을 주지 않는다.
// 클래스
class User {
var name: String = ""
var age: Int = 0
}
var userClass = User()
var cheolsuClass = userClass
var yuriClass = userClass
yuriClass.name = "yuri"
yuriClass.age = 5
print(yuriClass.age)
print(yuriClass.name)
print(cheolsuClass.age)
print(cheolsuClass.name)
// 출력 결과
5
yuri
5
yuri
// yuri의 값을 변경했지만, cheolsu객체의 특성도 변경되었다.
Swift는 구조화된 방식으로 비동기 및 병렬 코드를 작성하기위해 기본적으로 기능을 제공합니다.
Swift의 언어자원을 사용하지 않고도 동시코드를 작성할 수 있지만, 읽기가 더 어렵고 복잡합니다.
사용하지 않은 예
listPhotos(inGallery: "Summer Vacation") { photoNames in
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
downloadPhoto(named: name) { photo in
show(photo)
}
}
위 예제에서도 코드를 완료 핸들러로 작성해야 하기 때문에 중첩된 클로저를 사용하게 되고, 코드가 더 복잡하여 다루기 힘들어질 수 있습니다.
비동기 함수 및 호출
Swift에서 제공하는 비동기 함수 또는 비동기 메서드는 실행 중간에 일시 중단될 수 있는 특수한 종류의 함수 또는 메서드 입니다.
비동기 함수는 동기함수처럼 실행되거나 오류를 발생시키거나 반환하지않는 세가지 중 하나를 수행하지만, 중간에 일시 중단할 수 도 있습니다. 본문 내부에서 실행을 일시 중단할 수 있는 위치를 표시합니다.
먼저 함수나 메서드가 비동기함수임을 나타내려면 선언에서 매개변수 뒤 async 키워드를 작성하면됩니다. 이는 throws throw 함수를 표시하는데 사용하는 방법과 비슷합니다.
함수나 메서드가 값을 반환하는 경우 return 화살표 앞에 async 를 쓰면 됩니다.
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
- 비동기면서 에러를 반환하는 throws기능을 하는 함수나 메서드인 경우 throws 앞에 async를 작성합니다.
비동기 메서드를 호출할 때 해당 메서드가 반환될 때까지 실행이 중단됩니다. 이때 await 키워드를 작성하여 비동기 함수를 호출 하는 지점 앞에 중단지점을 표시합니다. 이는 try throw 함수를 호출할 때 오류가 발생하는 경우 프로그램 흐름에 대한 변경 가능 사항을 표시하기 위해 작성하는 것과 같습니다. 중단은 중단지점 없이 암묵적으로 진행되거나 먼저 진행되지 않습니다.
코드의 실행 과정
let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)
1. 첫째줄 코드가 실행을 시작하여 listPhotos함수가 실행되고 반환될 때 까지 일시 중단됩니다.
2. 이 코드가 중단되는 동안 다른 동시코드들은 실행이 됩니다.
3. listPhotos가 반환되고 반환된 값을 photoNames에 할당합니다. 코드는 일시중단된 지점에서 다시 시작합니다.
4. sortedName를 정의하고 name에 값을 할당합니다. await 표시가 없는 동기 함수이므로 그대로 실행됩니다.
5. 다음 함수는 downloadPhoto를 호출하는 함수입니다. 해당 함수가 반환될때 까지 일시 중단하여 다른 동시코드가 실행할 수 있도록 기회를 제공합니다.
6. downloadPhoto 반환 후 반환된 값이 할당되고 show함수를 호출할 때 인자로 전달됩니다.
-> 비동기함수를 호출할 때 await 키워드가 적힌 곳은 현재 코드가 실행을 일시 중지할 수 있음을 나타냅니다. 이를 스레드 양보라고 합니다. Swift가 내부적으로 현재 스레드에서 코드 실행을 일시 중지하고 대신 해당 스레드에서 다른 코드를 실행하기 때문입니다.
Task.yield()
Task.yield() 메서드를 호출하여 명시적으로 중단지점을 삽입 할 수 있습니다.
func generateSlideshow(forGallery gallery: String) async {
let photos = await listPhotos(inGallery: gallery)
for photo in photos {
// ... render a few seconds of video for this photo ...
await Task.yield()
}
}
예시로 비디오를 렌더링하는 코드가 동기식이라면, 그 코드는 await같은 정지지점을 포함하지 않습니다. 비디오를 렌더링 하는 작업은 오랜시간이 걸릴 수 있으므로 위와 같이 주기적으로 호출하여 정지점을 명시적으로 추가할 수 있습니다.
위 함수는 비동기적이면서 오류를 리턴할 수 있는 async throw 함수입니다. 위와 같은 함수를 호출할 때는 try await 모두 작성합니다.
let photos = try await listPhotos(inGallery: "A Rainy Weekend")
비동기 함수는 throw함수와 유사하지만 중요한 차이점이 있습니다.
throw코드는 do-catch블록으로 감싸서 오류를 처리하거나 Result<Data, Error>오류를 저장하여 다른곳에서 처리할 수 있습니다.
func availableRainyWeekendPhotos() -> Result<[String], Error> {
return Result {
try listDownloadedPhotos(inGallery: "A Rainy Weekend")
}
}
반면 비동기 코드를 래핑하여 동기 코드에서 호출하고 결과를 기다릴 수 있는 안전한 방법은 없습니다. 이를 직접 구현하려고 하면 미묘한 경합, 스레딩 문제 및 교착상태와 같은 문제가 발생할 수 있습니다.
비동기 시퀀스
이전에는 배열의 모든 요소가 준비된 후 전체배열을 한번에 비동기적으로 반환했지만 비동기 시퀀스를 사용하여 한번에 한개의 요소를 기다는 방법도 있습니다. 비동기 시퀀스를 반복하는 모습은 다음과 같습니다.
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}
일반적은 for-in루프를 사용하는 대신 뒤에 try await을 사용합니다.
비동기 함수를 병렬로 호출
비동기 함수를 호출하면 한번에 한개의 코드만 실행됩니다. 비동기 코드가 실행되는 동안 호출자는 다음 코드 줄을 실행하기 전 해당 코드가 완료될 때까지 기다립니다. 예를 들어 갤러리에서 처음 세 장의 사진을 가져오려면 다음과 같이 downloadPhoto 함수에 대한 세 번의 호출을 기다릴 수 있습니다.
let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
이 접근 방식에는 중요한 단점이 있습니다. 다운로드가 완료되기 전에 다음 다운로드가 진행되지 않는다는 점 입니다.
이러한 작업을 기다릴 필요 없이 독립적으로 또는 동시에 다운로드할 수 있습니다.
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
비동기 함수를 호출하고 주변 코드와 병렬로 실행하려면 상수를 정의할 때 let앞에 async를 쓰고 상수를 사용할 때마다 뒤에 await를 씁니다. 위 예제에서 세 개의 호출은 모두 이전 호출이 완료될 때까지 기다리지 않고 시작합니다. 코드가 함수의 결과를 기다리지 위해 일시 중지 되지 않기 때문에 함수 호출 중 어느곳도 await로 표시되지 않습니다. 대신 photos가 정의된 줄까지 실행이 계속 됩니다. 이 비동기 호출의 결과가 필요하므로 세장의 사진 모두 다운로드가 완료될 때까지 실행을 일시 중지하기 위해 await를 작성합니다.
두가지 접근 방식의 차이점은 다음과 같습니다.
다음 줄의 코드가 해당 함수의 결과에따라 달라지는 경우 await를 사용하여 비동기 함수를 호출합니다. 이렇게 하면 순차적으로 수행되는 작업이 생성됩니다.
해당 함수의 결과와 상관없는경우 async-let으로 비동기 함수를 호출합니다. 이렇게하면 병렬로 수행할 수 있는 작업이 생성됩니다.
await 및 async-let 둘 다 일시중단된 동안 다른코드가 실행되도록 허용합니다.
await는 두 경우 모두 필요한 경우 비동기 함수가 반환될 때 까지 실행이 일시 중지됨을 나타냅니다.
작업 및 작업 그룹 Task and Task Groups
Task는 프로그램의 일부가 비동기적으로 실행할 수 있는 작업단위입니다. 모든 비동기 코드는 일부 작업의 일부로 실행됩니다. Task 자체는 한가지 작업만 수행하지만 여러 작업을 만들면 동시에 실행되도록 예약할 수 있습니다.
이전 섹션에서 설명한 async-let구문은 암시적으로 자식 Task를 생성합니다. 이 구문은 프로그램에서 실행해야 하는 작업을 이미 알고 있는 경우에 효과적입니다. Task Group을 생성하고 해당 그룹에 자식 Task를 명시적으로 추가할 수도 있습니다. 이를 통해 우선수위와 작업 취소를 더 제어하기 쉽고 동적인 작업 수를 생성할 수 있습니다.
Task는 계층구조로 배열됩니다. 주어진 Task Group 에서 각 Task는 동일한 부모 작업을 갖고 자식 Task를 가질 수 있습니다.
Task와 Task Group간의 명시적 관계 때문에 이 접근 방식을 구조적 동시성 이라고 합니다. 이런 관계에는 여러가지 이점이 있습니다.
부모 작업에서는 자식 작업이 완료될 때까지 기다리는 것을 잊을 수 없다.
자식 작업에 더 높은 우선순위를 설정하면 부모 작업의 우선순위가 자동으로 높아진다.
부모 작업이 취소되면, 해당 자식 작업도 모두 자동으로 취소된다.
작업 로컬 값은 자식 작업으로 효율적이고 자동으로 전파됩니다.
다음은 많은 사진 다운로드 할 수 있는 코드입니다.
await withTaskGroup(of: Data.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
group.addTask {
return await downloadPhoto(named: name)
}
}
for await photo in group {
show(photo)
}
}
위 코드는 새로운 Task Group을 만든 다음, 갤러리의 각 사진을 다운로드하기 위한 자식 Task를 만듭니다. Swift는 조건이 허락되면 작업을 동시에 실행합니다. 자식 Task가 사진 다운로드를 완료하자마자 해당 사진이 표시됩니다. 완료되는 순서에 대한 보장은 없어서 갤러리의 사진은 어떤 순서로든 표시될 수 있습니다.
- 사진을 다운로드하는 코드에서 오류가 발생하면 대신 withThrowingTaskGruop을 호출하면 됩니다.
위 코드는 사진을 다운로드된 다음 표시되므로 Task Group은 결과를 반환하지 않습니다.
결과를 변환하는 Task Group인 경우 클로저 내부에 결과를 누적하여 전달하는 코드를 추가합니다.
let photos = await withTaskGroup(of: Data.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
group.addTask {
return await downloadPhoto(named: name)
}
}
var results: [Data] = []
for await photo in group {
results.append(photo)
}
return results
}
이전 예제와 마찬가자로 이 예제도 각 사진에 대한 다운로드할 자식 Task를 생성합니다. for-await-in 루프는 다음 작업이 완료될 때까지 기다리고 해당 작업의 결과를 결과배열에 추가한 다음 모든 자식 Task가 완료될 때까지 계속 기다립니다. 마지막으로 다운로드한 사진배열을 전체 결과로 반환합니다.
작업 취소 Task Cancellation
Swift 동시성은 협력 취소 모델을 사용합니다. 각 Task는 실행의 적절한 지점에서 취소되었는지 확인하고 적절하게 대응합니다.
취소에 대한 대응은 일반적으로 다음 중 하나를 의미합니다.
CancellationErro와 같은 오류가 발생했을 때
nil 또는 빈 컬렉션을 반환할 때
부분적으로 완료된 작업을 반환할 때
데이터가 크거나 네트워크가 느린경우 다운로드하는데 시간이 오래걸릴 수 있습니다. 모든 작업이 완료될 때까지 기다리지 않고 사용자가 작업중지할 수 있도록 하려면 작업이 취소되었는지 확인하고 취소된 경우 실행을 중지해야 합니다. 여기엔 두가지 방법이 있습니다.
Task.checkCancellation() type메서드를 호출하거나 Task.isCancelled type 속성을 읽는것
작업이 취소되면 check Cancellation() 호출시 오류가 발생합니다. throw 작업은 오류를 작업 외부로 전파하여 Task의 모든 작업을 중지할 수 있습니다. 이는 구현과 이해가 간단하다는 장점이 있습니다.
유연성을 높이려면 isCancelled속성을 사용하는 방법이 있습니다. 이 속성을 사용하면 네트워크 연결 닫기, 임시 파일 삭제 등 작업 중지의 일부로 정리 작업을 수행할 수 있습니다.
let photos = await withTaskGroup(of: Optional<Data>.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
let added = group.addTaskUnlessCancelled {
guard !Task.isCancelled else { return nil }
return await downloadPhoto(named: name)
}
guard added else { break }
}
var results: [Data] = []
for await photo in group {
if let photo { results.append(photo) }
}
return results
}
위 코드는 이전 버전과 비교해 여러 가지 변경 사항을 적용했습니다.
각 작업은 취소 후 새 작업을 시작하는 것을 방지하기 위해 TaskGroup.addTaskUnlessCancelled(priority: operation: ) 사용합니다.
addTaskUnlessCancelled 에 대한 호출 후 코드는 새 자식 Task가 추가되었음을 확인합니다. 그룹이 취소되면 added 값은 false가 됩니다. 이 경우 코드는 추가 사진을 다운로드 하려는 시도를 중단합니다.
각 Task는 사진을 다운로드하기 전에 취소 여부를 확인합니다. 취소된 경우 Task는 nil로 반환됩니다.
마지막으로 각 Task Group은 결과를 확인할 때 nil 값을 건너뜁니다. nil 반환을 통해 취소를 처리한다는 것은 Task Group이 완료된 작업을 삭제하는 대신 취소 시 이미 다운로드한 사진으로 부분 결과를 반환할 수 있음을 의미합니다.
- 해당 작업 외부에서 작업이 취소되었는지 확인하려면 Task.isCancelled type 속성 대신 instance 속성을 사용합니다.
let task = await Task.withTaskCancellationHandler {
// ...
} onCancel: {
print("Canceled!")
}
// ... some time later...
task.cancel() // Prints "Canceled!"
취소 핸들러를 사용할때 작업 취소는 여전히 협력적입니다. 작업은 완료될 때까지 실행되거나 취소를 확인한 후 일찍 중지됩니다. 취소 핸들러가 시작될 떄 작업이 계속 실행 중이므로 작업과 해당 취소 핸들러 사이 상태를 공유하지 마세요. 이로 인해 데이터 레이스 현상이 발생할 수 있습니다.
구조화되지 않은 동시성 Unstructured Concurrency
이전 섹션에서는 동시성에 대한 구조화된 접근방식이였습니다. 그외에도 Swift는 구조화되지 않은 동시성도 지원합니다.
Task Group의 일부 Task와 다르게 구조화되지 않은 작업에는 부모 작업이 없습니다. 프로그램이 의도한 방식대로 구조화되지 않은 작업을 관리할 수 있다는 완전한 유연성을 갖고 있지만 정확성에 대한 전적인 책임도 프로그램한테 존재합니다. Actor에서 실행되는 구조화되지 않은 Task를 생성하려면 Task.init(priority: Operation:) 생성자를 호출하세요. 액터의 일부가 아닌 구조화되지 않은 작업을 생성하려면 Task.detached(priority: Operation:) 클래스 메서드를 호출하세요. 두 작업 모두 결과를 기다리거나 취소하는 등 상호작용할 수 있는 Task를 반환합니다.
let newPhoto = // ... some photo data ...
let handle = Task {
return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value
Task를 사용하여 프로그램을 격리도니 동시적 부분으로 나눌 수 있습니다. 작업은 서로 격리되어 있어 동시에 실행해도 안전하지만, 때로는 작업 간에 일부 정보를 공유해야 합니다. actor를 사용하면 동시적 코드 간에 안전하게 정보를 공유할 수 있습니다.
클래스와 마찬가지로 actor는 참조유형입니다. 참조 유형과 값 유형의 비교는 클래스뿐만 아니라 actor에도 적용됩니다.
클래스와 달리 actor는 한번에 하나의 작업만 변경 가능한 상태에 액세스하도록 허용하므로 여러 작업의 코드가 액터의 동일한 인스턴스와 상호 작용하는 것이 안전합니다.
여기 온도를 기록하는 actor가 있습니다.
actor TemperatureLogger {
let label: String
var measurements: [Int]
private(set) var max: Int
init(label: String, measurement: Int) {
self.label = label
self.measurements = [measurement]
self.max = measurement
}
}
actor 키워드로 소개한 다음 중괄호 쌍으로 정의합니다. TemperatureLogger actor는 외부의 다른 코드가 액세스할 수 있는 속성을 가지고 있으며 actor 내부의 코드만 최대값을 업데이트할 수 있도록 max 속성을 제한합니다. 구조체 및 클래스와 동일한 초기화 구문을 사용하여 actor의 인스턴스를 생성합니다. actor의 속성이나 메서드에 액세스할 때 await 키워드를 사용하여 잠재적 일시 중단 지점을 표시합니다.
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"
이 예시에서 logger.max에 액세스하는 것이 일시 정지 지점이 될 수 있습니다. actor는 한번에 하나의 작업만 변경 가능한 상태에 액세스하도록 허용하므로 다른 작업의 코드가 이미 logger와 상호 작용하고 있는 경우 이 코드는 속성에 액세스하기를 기다리는 동안 일시 중지됩니다.
대조적으로 actor의 일부인 코드는 actor의 속성에 액세스할 때 대기를 작성하지 않습니다.
예시로 새로운 온도로 TemperatureLogger를 업데이트하는 방법은 다음과 같습니다.
extension TemperatureLogger {
func update(with measurement: Int) {
measurements.append(measurement)
if measurement > max {
max = measurement
}
}
}
이 update 메서드는 이미 actor에서 실행 중이므로 max와 같은 속성에 대한 엑세스를 await로 표시하지 않습니다. 또한 이 방법은 actor가 변경 가능한 상태와 상호 작용하기 위해 한 번에 하나의 작업만 허용하는 이유 중 하나를 보여줍니다. actor의 상태에 대한 일부 업데이트는 일시적으로 불변성을 깨트립니다. TemperatureLogger actor는 온도 목록과 최대 온도를 추적하고 새 측정 값을 기록할 때 최대 온도를 업데이트합니다. 업데이트 도중에 새 측정값을 추가한 후 최대값을 업데이트하기 전에 TemperatureLogger가 일시적으로 일관되지 않은 상태에 있습니다. 여러 작업이 동일한 인스턴스와 동시에 상호작용하는 것을 방지하면 다음과 같은 일련의 이벤트와 같은 문제를 방지할 수 있습니다.
1. update 메서드를 호출합니다. 이것은 measurements 배열을 처음으로 업데이트합니다.
2. max가 업데이트 되기전에 다른 곳의 코드가 max와 온도 배열을 읽습니다.
3. 코드는 max를 변경하여 업데이트를 완료합니다.
이 경우 다른 곳에서 실행되는 코드는 데이터가 일시적으로 유효하지 않은 동안 update 호출 중간에 actor에 대한 엑세스가 인터리브 되었기 때문에 잘못된 정보를 읽습니다. swift actor를 사용하면 한번에 해당 상태에서 하나의 작업만 허용하고 해당 코드는 정지 지점을 표시하는 await가 정지 지점을 표시하는 위치에서만 중단될 수 있기 때문에 이 문제를 방지할 수 있습니다. update에는 정지 지점이 포함되어 있지 않으므로 업데이트 도중에는 다른 코드가 데이터에 엑세스할 수 없습니다.
actor 외부의 코드가 구조체나 클래스의 속성에 접근하는 것처럼 해당 속성에 직접 접근하려고 하면 컴파일 타임 오류가 발생합니다.
print(logger.max) // Error
await를 쓰지 않고 logger.max에 액세스하면 actor의 격리된 로컬 상태의 일부이기 때문에 실패합니다. 이 속성에 액세스하는 코드는 actor의 일부로 실행되어야 하며, 이는 비동기 작업이며 await키워드가 필요합니다. Swift는 actor에서 실행되는 코드만 해당 actor의 로컬 상태에 액세스할 수 있음을 보장합니다. 이러한 보장을 actore isolation 이라고 합니다.
Swift 동시성 모델의 다음 측면은 공유된 변경 가능 상태에 대해 더 쉽게 추론할 수 있도록 함께 작동합니다.
일시 중단 가능성이 있는 지점 사이에 있는 코드는 다른 동시 코드로 인해 중단될 가능성 없이 순차적으로 실행됩니다.
actor의 로컬 상태와 상호 작용하는 코드는 해당 actore내부에서만 실행됩니다.
actor는 한번에 하나의 코드만 실행합니다.
이러한 보장으로 인해 await를 포함하지 않고 actor 내부에 있는 코드는 프로그램의 다른 위치에서 일시적으로 유효하지 않은 상태를 관찰할 위험 없이 업데이트를 수행할 수 있습니다.
위의 코드는 측정 배열을 한 번에 하나씩 변환합니다. map 작업이 진행되는 동안 일부 온도는 화씨, 다른 온도는 섭씨로 표시됩니다. 그러나 코드에는 await가 포함되어 있지 않으므로 이 메서드에는 잠재적인 일시 중단 지점이 없습니다. 이 메서드가 수정하는 상태는 actor에 속하며 해당 코드가 액터에서 실행될 때를 제외하고는 코드를 읽거나 수정하지 못하도록 보호합니다. 즉, 단위 변환이 진행되는 동안 다른 코드에서 부분적으로 변환된 온도 목록을 읽을 수 있는 방법이 없습니다.
잠재적인 중단 지점을 생략하여 일시적인 유효하지 않은 상태를 보호하는 actor에 코드를 작성하는 것 외에도 해당 코드를 동기식 메서드로 이동할 수 있습니다. 위의 ConvertFahrenheitToCelsius()메서드는 동기 메서드이므로 잠재적인 정지 지점이 포함되지 않음이 보장됩니다. 이 기능은 데이터 모델을 일시적으로 불일치하게 만드는 코드를 캡슐화하고, 작업을 완료하여 데이터 일관성이 복원되기 전에는 다른 코드를 실행할 수 없다는 것을 코드를 읽는 사람이 더 쉽게 인식할 수 있도록 합니다. 앞으로 이 함수에 동시 코드를 추가하여 일시 중단 지점을 도입하려고 하면 버그가 발생하는 대신 컴파일 시간 오류가 발생하게 됩니다.
보낼 수 있는 유형 Sendable Types
Task와 Actor를 사용하면 프로그램을 안전하게 동시에 실행할 수 있는 부분으로 나눌 수 있습니다. Task나 Actor 인스턴스 내부에서 변수와 속성과 같은 변경 가능한 상태를 포함하는 프로그램 부분을 동시성 도메인(Concurrency Domain) 이라고합니다. 일부 데이터는 변경 가능한 상태를 포함하기 때문에 동시성 도메인 간에 공유할 수 없지만 중복 액세스로부터 보호하지는 않습니다.
한 동시성 도메인에서 다른 동시성 도메인으로 공유할 수 있는 유형을 Sendable type이라고 합니다.
예를 들어, actor 메서드를 호출할 때 인수로 전달하거나 Task의 결과로 반환할 수 있습니다. 이 장의 앞부분에 나온 예제에서는 sendability에 대해 설명하지 않았는데, 이러한 예제에서는 동시성 도메인 간에 전달되는 데이터에 대해 항상 안전하게 공유할 수 있는 간단한 값 유형을 사용하기 때문입니다. 반면, 일부 유형은 동시성 도메인 간에 전달하는 것이 안전하지 않습니다. 예를 들어, 변경 가능한 속성을 포함하고 해당 속성에 대한 액세스를 직렬화하지 않는 클래스는 다른 직업간에 해당 클래스의 인스턴스를 전달할 때 예측할 수 없고 잘못된 결과를 생성할 수 있습니다
Sendable protocol을 사용하여 보낼 수 있는 유형으로 표시합니다. 해당 프로토콜에는 코드 요구 사항이 없지만 Swift에서 적용하는 의미적 요구 사항이 있습니다.
일반적으로 유형을 보낼 수 있는 방법에는 세 가지가 있습니다.
유형은 값 유형이고, 변경 가능한 상태는 다른 전송 가능한 데이터로 구성됩니다. 예를 들어, 전송 가능한 저장된 속성이 있는 구조체나 전송 가능한 연관된 값이 있는 열거형이 있습니다.
이 유형에는 변경 가능한 상태가 없으며, 변경 불가능한 상태는 다른 전송 가능한 데이터(예: 읽기 전용 속성만 있는 구조체나 클래스)로 구성됩니다.
이 유형에는 변경 가능한 상태의 안정성을 보장하는 코드가 있는데, 이는 @MainActor 표시된 클래스나 특정 스레드나 큐에서 속성에 다ㅐ한 액세스를 직렬화하는 클래스와 같습니다.
일부 유형은 항상 보낼 수 있습니다.
예를 들어, 보낼 수 있는 속성만 있는 구조체와 보낼 수 있는 연관된 값만 있는 열거형이 있습니다.
struct TemperatureReading: Sendable {
var measurement: Int
}
extension TemperatureLogger {
func addReading(from reading: TemperatureReading) {
measurements.append(reading.measurement)
}
}
let logger = TemperatureLogger(label: "Tea kettle", measurement: 85)
let reading = TemperatureReading(measurement: 45)
await logger.addReading(from: reading)
TemperatureReading은 sendable 속성만 있는 구조이고, 구조가 public 또는 @usableFromInline으로 표시되지 않은 경우 암묵적으로 sendable입니다. 다음은 Sendable protocol 준수가 암묵적으로 포함된 구조 버전입니다.
struct TemperatureReading {
var measurement: Int
}
Sendable protocol에 대한 암묵적 적합성을 재정의하여 유형을 보낼 수 없음으로 명시적으로 표시하려면 extension을 사용합니다.
- RandomNumberGenerator 프로토콜은 호출될때마다 Double 값을 리턴하는 Random 인스턴스 메소드를 요구한다.
- 프로토콜의 일부로 지정되지 않았지만, 이 값은 최소 0.0 에서 최대 1.0 사이의 숫자로 지정된다.
- RandomNumberGenerator 프로토콜은 각 난수가 어떻게 생성되는지 지정하지 않았다.
- 단지 Random 메소드가 채택된 다른 곳에서 새로운 난수를 생성하는 표준 방법을 제공하기만 하면 된다.
RandomNumberGenerator 프로토콜을 채택한 클래스
- 이 클래스는 선형 합동 생성기로 일려진 난수 생성기를 구현한다.
class LinearCongruentialGenerator: RandomNumberGenerator {
var lastRandom = 42.0
let m = 139968.0
let a = 3877.0
let c = 29573.0
func random() -> Double {
lastRandom = ((lastRandom * a + c)
.truncatingRemainder(dividingBy:m))
return lastRandom / m
}
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"
Mutating Method Requirements
- 메소드가 자신이 속한 인스턴스를 수정해야하는 경우가 있다.
- 값 유형(구조체, 열거형)의 경우 mutating 키워드를 func 키워드 앞에 배치하여 해당 메소드가 속한 인스턴스의 모든 프로퍼티가 수정가능함을 알린다.
- 프로토콜을 채택하는 모든 타입의 인스턴스를 수정하려면, 프로토콜 정의 내에서 일부 메소드를 mutating 키워드를 붙여야한다.
- 이를 통해 구조체와 열거형이 프로토콜을 채택하고 해당 메소드의 요구사항을 충족할 수 있다.
- 클래스에 대한 해당 메소드 구현을 작성할때 mutating 키워드를 작성할 필요가 없다. 구조체와 열거형에서만 사용된다.
아래 예시는 toggle 메소드가 정의된 Togglable 프로토콜을 정의한다.
- 이름에서 알다시피 toggle 메소드는 해당 프로퍼티를 수정하여 적합한 상태로 전환하거나 반전시키는 것이다.
- toggle 메소드는 Togglable 프로토콜 정의부분에서 mutating 키워드를 붙였다.
- 메소드가 호출될때 적합한 인스턴스의 상태를 변경할 것으로 예상된다.
protocol Togglable {
mutating func toggle()
}
- 구조체나 열거형에 Togglable 프로토콜을 구현하는 경우, 해당 구조체나 열거형은 mutating으로 표시된 toggle() 메소드를 구현하여 프로토콜을 준수할 수 있다.
아래 예시는 OnOffSwitch 열거형을 정의한다.
- 이 열거형은 두가지 상태 사이를 전환한다. ( case On , case Off )
- 이 열거형의 toggle은 Togglable 프로토콜과 일치하도록 mutating을 표시하여 구현하도록 한다.
enum OnOffSwitch: Togglable {
case off, on
mutating func toggle() {
switch self {
case .off:
self = .on
case .on:
self = .off
}
}
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch is now equal to .on