동시성 프로그래밍(Concurrent Programming)을 할 때는 Data Race가 발생하지 않도록 해야합니다.
데이터 레이스란 멀티 스레드/프로세스 환경에서 일어나는 오류로 여러 스레드/프로세스가 공유 자원에 동시에 접근하려 할 때, 일어나는 경쟁 상황을 의미합니다.

예시로 위 이미지는 Counter 클래스에 있는 number를 1씩 증가하는 함수를 호출하는 코드입니다. DispatchQueue.global().async를 사용해서 비동기로 함수를 호출하기 때문에 counter.increment() 함수는 두 개의 스레드에서 동시에 실행됩니다. increment() 함수를 2번 호출했기 때문에 number의 값은 2가 되어야하지만 두 함수가 서로 다른 스레드에서 실행될 때 number의 값은 0이기 때문에 두 함수 모두 number의 값을 1로 증가하게 되고 결과값은 1이 됩니다. 이러한 문제를 해결하기 위해 Actor를 사용할 수 있습니다.
Actor는 내부 상태에 대한 접근을 직렬화하여, 동시에 여러 스레드가 접근하더라도 안전하게 상태를 관리할 수 있도록 합니다. Actor는 actor 키워드를 사용하여 정의하며, 내부에 상태를 저장할 수 있는 프로퍼티와 메서드를 가질 수 있습니다. 예를 들어, 다음과 같은 코드로 Actor를 정의할 수 있습니다.
actor Counter {
var number = 0
}
기존에 작성한 class명령어 자리에 actor를 작성하면 끝입니다. 메서드를 호출할 때 class와 비슷하지만 비동기 함수를 호출하듯 await 명령어를 앞에 붙여줘야 합니다. 위의 이미지를 actor로 정의하고 호출하면 아래 이미지처럼 에러가 발생하지 않습니다.

만약 다른 함수 안에서 호출할 경우 함수를 정의할 때 async 키워드를 달아서 비동기 함수임을 명시해줘야 합니다.
func someFunction() async {
let counter = Counter()
await counter.increment()
}
actor 내의 프로퍼티는 기본적으로 private이며, 외부에서 직접 접근할 수 없습니다. 이는 내부 상태가 외부의 영향을 받지 않도록 보장합니다. 즉, 모든 상태 변경은 actor 내의 메서드를 통해 이루어져야 하므로, 메서드 호출 시 스레드 안전성을 보장합니다.
위에서 언급했듯이 메서드를 사용할 땐 비동기로 actor에 접근해야하며, actor의 상태에 접근할 때는 해당 메서드가 완료될 때까지 다른 스레드가 접근할 수 없습니다. 이는 actor가 하나의 스레드에서만 상태를 변경하도록 강제하기 때문이며 이를 통해 데이터 레이스를 방지합니다.
Actor와 함께 알아두어야 할 개념으로는 Sendable과 cross-actor reference가 있습니다.
Sendable은 Swift의 프로토콜로, 객체가 안전하게 다른 스레드나 actor 간에 전송될 수 있음을 나타냅니다. Sendable 프로토콜을 준수하는 객체는 스레드 간에 안전하게 공유될 수 있으며, 이 객체는 불변(immutable) 상태여야 합니다.
Sendable은 다음과 같은 특징이 있습니다.
Cross actor reference는 서로 다른 actor 간의 참조를 의미합니다. Swift에서는 서로 다른 actor 간에 데이터를 안전하게 공유하기 위해 다음과 같은 규칙을 따릅니다.