Swift Concurrency에서 Task는 비동기 코드를 실행하기 위한 기본 실행 단위이며, Task.detached(priority:operation:)
를 사용하면 부모 Task와 분리된 독립적인 Task
를 생성한다.
또한 Structured Concurrency를 기반으로 하나의 Task
에서 여러 개의 비동기 작업을 실행 하는 경우, child task를 생성하여 해당 child task들의 생명 주기를 관리하고 결과를 수집한다.
struct Task<Success, Failure> where Success : Sendable, Failure : Error
Task
정의에서도 알 수 있듯이, Task
는 실행이 완료되면, 결과 값을 반환 하는데, 이 결과 값의 타입은 Sendable
프로토콜을 따라야 하며, 이것은 여러 Task
사이에서 데이터를 안전하게 전달하기 위함이다.
비동기 작업은 일반적으로 동시에 실행되는 detached task나 child task 간의 데이터 공유가 발생할 수 있다. 만약 공유되는 값이 스레드 안전성을 보장하지 않는다면, data race, 메모리 손상, 예측 불가능한 동작이 발생할 수 있다. 따라서 Sendable
제약을 통해 Swift 컴파일러가 데이터의 안전한 전달 가능성을 검증하며, 이를 통해 동시성 환경에서도 안정성과 일관성을 유지할 수 있다.
참고로
Task
에서 아무 값도 반환 하지 않을 경우Success
제네릭 타입은Void
가 된다.
그렇다면 여기서 말하는 Sendable 프로토콜이란 무엇일까?
💡
Sendable
프로토콜은 동시성 컨텍스트 사이에서 데이터 경합 없이 안전하게 데이터를 공유할 수 있는 thread-safe 타입을 말한다.
Swift에서 Sendable
프로토콜을 채택할 수 있는 타입은 아래와 같다.
@Sendable
)Sendable
프로토콜은 다른 프로토콜과 달리 명시적인 프로퍼티 요구나 메서드 요구 사항은 없지만, Sendable
프로토콜을 채택하기 위한 조건이 있는데, 각 타입별 조건은 아래와 같다.
구조체와 열거형에서 Sendable
프로토콜을 채택하기 위해서는 구조체와 열거형이 가지는 멤버와 연관값이 모두 Sendable
을 만족하면 된다.
또한 아래의 조건을 만족하는 경우 암시적으로 Sendable
프로토콜을 채택한다.
public
으로 선언 되지 않거나 @usableFromInline
로 표시되지 않은 경우Sendable
프로토콜을 따르는 프로퍼티만 가지는 구조체, Sendable
프로토콜을 따르는 연관값 만을 가지는 열거형Swift의 actor
타입은 암시적으로 Sendable
프로토콜을 준수한다.
Swift class
타입은 Sendable
프로토콜을 채택하기 위해서는 아래와 같은 조건을 만족해야 한다.
final
클래스Sendable
protocol을 채택한 경우NSObject
를 부모 클래스로 가지는 경우또한 클래스가 @MainActor
로 선언되어 있는 경우, 암시적으로 Sendable
protocol을 따른다.
클로저는 파라미터 정의 앞에 @Sendable
속성을 명시하여 해당 클로저가 안전하게 동시성 환경에서 사용될 수 있음을 보장할 수 있다. @Sendable
이 적용된 함수와 클로저는 캡처 시 값 복사를 통해만 동작하며, 캡처된 모든 값은 반드시 Sendable
프로토콜을 준수해야 한다.
let sendableClosure = { @Sendable (number: Int) -> String in
if number > 12 {
return "More than a dozen."
} else {
return "Less than a dozen"
}
}
튜플 내부 요소가 모두 Sendable
protocol을 채택한다면, 암묵적으로 해당 튜플은 Sendable
protocol을 채택한다.
이처럼, Sendable
프로토콜은 독립적인 Task 간에 값이 전달될 때 발생할 수 있는 데이터 경합을 사전에 차단하여, 안전하게 데이터를 공유할 수 있도록 보장한다.
하지만 Sendable
타입은 데이터를 안전하게 전달할 수 있도록 가이드만 제공할 뿐, 여러 Task에서 동시에 동일한 인스턴스에 접근하는 경우, 직접적으로 데이터를 동기화할 수는 없다.
actor
타입은 Swift Concurrency에서 동시 접근 제어를 보장하는 Swift의 새로운 타입이다.
actor
타입은 다음과 같은 특징을 가진다.
actor
도 일종의 타입이다.protocol
준수, extension
도 모두 사용 가능하다.class
와 동일하게 참조 타입이다.class
와 달리 상속이 불가능하다.actor
타입과 다른 타입들의 가장 큰 차이점은 데이터 경합으로부터 actor
의 상태를 보호한다는 것이다. 그리고 actor
의 가변 상태 데이터를 보호하기 위해 actor
와 그 인스턴스 멤버에 접근 방식에 제한을 주게 되는데 이것을 actor isolation이라고 한다.
actor isolation의 첫 번째 규칙은 다음과 같다.
actor
의 저장 인스터스 프로퍼티를self
를 통해서만 접근이 가능하도록 제한한다.
다음은 BankAccount
라는 actor
타입을 선언하고 transfer(amount:to:)
메서드를 통해 계좌이체를 구현한 예시이다.
actor BankAccount {
let accountNumber: Int
var balance: Double
init(accountNumber: Int, initialDeposit: Double) {
self.accountNumber = accountNumber
self.balance = initialDeposit
}
}
extension BankAccount {
enum BankError: Error {
case insufficientFunds
}
func transfer(amount: Double, to other: BankAccount) throws {
if amount > balance {
throw BankError.insufficientFunds
}
print("Transferring \(amount) from \(accountNumber) to \(other.accountNumber)")
balance = balance - amount
other.balance = other.balance + amount // error: actor-isolated property 'balance' can only be referenced on 'self'
}
}
이때 transfer(amount:to:)
메서드와 같이 other.balance
로 actor의 프로퍼티에 직접적으로 접근하게 되면 에러가 발생하게 되는데 에러 메세지 중 actor-isolated라는 의미는 특정 actor 내부에서만 접근이 가능함을 의미한다. 즉, self
키워드를 통해서만 접근이 가능하지만 self
로 접근하지 않았기 때문에 에러가 발생한 것이다.
actor
타입에서 저장, 연산 프로퍼티, 인스턴스 메서드, 인스턴스 서브스크립트 등은 기본적으로 actor-isolated 상태이다. 동일한 actor 인스턴스 내부에서 선언되어 있는 서로 다른 actor-isolated의 접근은 자유롭게 참조 가능하다.
actor 외부에서 actor-isolated 상태의 선언을 참조하는 것을 cross-actor reference라고 한다. cross-actor reference는 두 가지 방법으로만 접근할 수 있도록 허용하는데 그 방법은 아래와 같다.
other.accountNumber
이런 식의 접근은 가능하다는 것이다.cross-actor reference 조건을 바탕으로 위의 코드를 아래와 같이 리팩토링하여 에러를 제거할 수 있다.
extension BankAccount {
...
func transfer(amount: Double, to other: BankAccount) async throws {
if amount > balance {
throw BankError.insufficientFunds
}
print("Transferring \(amount) from \(accountNumber) to \(other.accountNumber)")
balance = balance - amount
await other.deposit(amount: amount)
}
private func deposit(amount: Double) async {
assert(amount >= 0)
balance = balance + amount
}
}