[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/Concurrency & GCD

[Swift] Actor

inu 2022. 3. 8. 16:43
반응형

참고 및 출처

Actor

class Counter { 
    var count: Int = 0 

    func increment() { 
        self.count += 1 
    } 
}

이런 경우에

let counter = Counter() 

DispatchQueue.global().async { counter.increment() } // global 
counter.increment() // main

이런식으로 2개의 스레드에서 동시 접근을 하면 Xcode 상에서 경고가 발생합니다. 이는 하나의 프로퍼티에 대해 두 개의 스레드에서 접근하면서 data race가 발생하기 때문입니다. 기존에 이를 해결하기 위해서는 스레드 동기화 기술인 lock, semaphore 등을 활용하거나 GCD의 Serial Queue를 사용해야 했습니다. 이는 분명 유용하지만 사용이 복잡합니다.

 

이와 달리 Actor를 사용하면 상태접근에 대한 data race 방지를 Swift가 보증하도록 처리할 수 있습니다.

 

Actor는 struct, class와 같은 일종의 타입입니다. 참조타입이며 다른 타입들과 같이 내부에 property, method, initailizer, subscripts 모두 가질 수 있습니다. protocol 준수나 extension 처리도 가능합니다. 다만 내부에 존재하는 모든 것들이 한번에 하나의 작업 변경처리만 수행하도록 허용됩니다.

actor Counter { 
    var count: Int = 0 
    func increment() { 
        self.count += 1 
    } 
}

이런식으로 정의가 가능합니다.

 

Actor를 통해 '하나의 동시성 영역'에 존재하는 상태들의 모임을 결성하는 것이라고 생각하면 됩니다. 앞서 actor는 내부에 존재하는 모든 것들이 한번에 하나의 변경처리 작업만 수행하도록 허용된다고 했습니다. 그럼 동시에 접근할 경우엔 어떻게 해야할까요?

 

async await를 사용하면 됩니다.

func someMethode() async { 
    let counter = Counter() 
    await counter.increment() 
}

async로 비동기함수를 생성하고 await로 작업을 기다렸다 처리할 수 있도록 합니다.

Actor isolated

앞서 actor는 한번에 한번의 변경처리 작업만 수행된다고 언급했습니다. 이는 actor의 isolation 기능으로 실현되는 기능입니다.
actor 내부의 값들(stored, computed instance properties, instance methods, instance subscripts)은 기본적으로 모두 actor isolated 상태입니다. 이 상태인 값들은 오직 self를 통해서만 접근할 수 있습니다. (즉 내부에서만 접근이 자유롭다는 것입니다.)

cf. static property, static method는 actor-isolated하지 않다.

 

외부에서 다른 actor의 isolated 값들에 접근할 경우 에러가 발생합니다. 이러한 값에 접근하기 위해서는 반드시 비동기 함수 내부에서 await를 사용해야 합니다.

 

다만 같은 모듈에 있는 경우 불변 상태의 값에 대한 접근은 가능합니다. 외부 모듈에서는 비동기 함수 내부에서 await 처리해야 합니다. 이는 모듈의 상태가 변경되어 불변 상태의 값이 muttable하게 변경되었을 때 클라이언트에게 갈 영향을 줄이기 위함이라고 합니다.

 

actor는 컴파일타임에 이런 이런 참조여부를 확인하고 1. 같은 모듈에 있는 불변상태의 값에 접근한것인지 2. 그게 아니라면 비동기 함수 내부에서 await 처리를 했는지 확인합니다. 아닐 경우 에러를 발생시켜 data race로부터 값들을 보호하는 것입니다.

isolated parameter / nonisolated keyword

asnyc 함수 내부의 await는 해당 동작을 바로 실행하는 것이 아니라, 가능할 때 실행처리하도록 요청하는 것입니다. actor는 이 요청을 보유하고 있다가 다른 작업이 끝나고나면 하나씩 처리하게 됩니다. 이럴 경우 객체에 동시에 접근하지 않도록 처리할 수 있기 때문에 data race가 발생하지 않음이 보장되는 것입니다.

 

다만 이러한 특징때문에 발생하는 몇가지 한계가 있습니다.

  • 먼저 내부 값을 변경하는 메서드는 반드시 해당 객체를 통해서만 접근이 가능합니다. 내부값이 actor-isolated 상태인데다가, self를 통해 참조하는 것도 아니니 당연한 것입니다.
  • computed property를 생성해 활용하는 것도 불가능합니다. computed property도 당연히 isolated 상태이기 때문에 아무리 직접 변경을 하지 않는 computed property일지라도 접근이 불가능한 것입니다.
  • 마지막으로 불변 상태가 아닌 내부 프로퍼티를 활용해 Hashable을 준수할 수 없습니다. 당연히 actor isolated 상태인 프로퍼티는 외부에서 접근할 수 없기 때문에 이를 활용해 외부에서 접근하는 hash함수에서 이를 활용하는 것이 불가능한 것이죠.

그래서 actor에는 특정 함수의 파라미터를 isolated하게 만들거나, 특정 값만 actor isolated 하지 않도록 설정하는 기능이 존재합니다.

func deposit(amount: Double, to account: isolated BankAccount) { 
    assert(amount >= 0) 
    account.balance = account.balance + amount  
}

파라미터에 isolated 키워드를 붙여주면 함수가 isolated context를 가지게되면서 상태에 직접 접근이 가능해집니다.

actor BankAccount { 
    let accountNumber: Int 
    var balance: Double 
    let accountName: String 

    nonisolated var displayName: String { 
        return self.accountName + "입니다" 
    } 

    init() { 
        ... 
    } 
}

noisolated 키워드는 프로퍼티 혹은 함수 앞에 붙힐 수 있습니다. 이렇게되면 해당 프로퍼티 혹은 함수는 isolated 상태에서 벗어납니다. 다만 이는 모두에 적용할 수는 없고, data race가 발생하지 않는 경우(computed property나 let property, function)에만 적용이 가능합니다.

 

정리

  • actor는 일종의 타입인데 내부에 있는 값을 isolated 처리해버린다.
  • 그래서 변경가능성이 있는 값은 외부에서 접근할 수 없고 self에서만 접근이 가능하다.
  • 불변 상태(let)에 대한 접근은 같은 모듈이라면 가능하다.
  • 이 외의 경우에는 async await로 접근해야한다.
  • isolated parameter / nonisolated keyword 로 이들에 대한 조정이 어느정도 가능하다.
반응형

'🍎 Apple > Concurrency & GCD' 카테고리의 다른 글

[Concurrency] async / await  (2) 2023.03.31
[Swift] GCD 정리하기  (2) 2022.05.30
[Swift] Operation Queue  (0) 2022.05.27
[Swift][문서의역] Task  (0) 2022.03.08
[Swift][문서의역] Concurrency (async & await)  (0) 2022.03.08