Actor에 대해 알아봅시다.

som·2024년 1월 12일
0

SwiftAnatomy

목록 보기
8/9

저번에 Sendable에 대해 공부하면서 새로 등장한 타입, Actor에 대해 포스팅한다고 했습니다!

그 약속 지키러 왔습니다! 후훗~

공부하면서 느낀 부분인데, Actor 요 녀석! 아주 요물입니다!
그러면 차근차근 알아보러 같이 떠나시죠!

배경

먼저 Actor가 어떤 타입인지 알아보기 전에, Apple이 왜 만들었을까?를 파헤쳐보는 것이 좋을 거 같습니다.
이 질문에 대한 답변은 WWDC 2021 - Protect mutable state with Swift actors에서 자세히 다루고 있습니다.
관심 있으신 분들은 보시고, Actor에 대해 공부하시는 걸 추천드립니다.

Swift에는 프로그램 전체에서 공유되는 변경 가능한 상태를 선언하는 메커니즘을 제공하는 클래스가 포함되어 있습니다. 그러나 클래스는 동시 프로그램 내에서 올바르게 사용하기가 매우 어렵기로 악명 높으며, 데이터 레이스를 피하기 위해 오류가 발생하기 쉬운 수동 동기화가 필요합니다. 우리는 공유 변경 가능 상태를 사용하는 동시에 데이터 레이스 및 기타 일반적인 동시성 버그에 대한 정적 감지 기능을 제공하고자 합니다.

Sendable을 class 타입에 어떻게 준수해야하는지 다들 기억 나시나요?
생각 안 나시는 분들은 Sendable 다시 읽고 와주십쇼!

결론부터 말하자면, 제약이 엄청 많았습니다.
저는 참조 타입의 변동성을 컴파일러가 알 수 없어, 저런 제약이 걸려있다라고 생각했지만 반은 맞고 반은 틀렸네요.

클래스는 변경 가능한 상태라 데이터 레이스를 피할 수 없어 이러한 제약이 걸려 있었군요!

그렇다면 참조 타입은 클래스 밖에 없는데, 참조 타입으로는 데이터 레이스를 해결하기 힘들까요?

그래서 등장한 것이 Actor 타입입니다!

개념

Actor는 공유된 변경 가능 상태에 대한 동기화 메커니즘입니다. Actor에는 자체 상태가 있으며 해당 상태는 프로그램의 나머지 부분과 격리됩니다. 해당 상태에 접근하는 유일한 방법은 Actor를 통과하는 것입니다. 그리고 Actor를 통과할 때마다 Actor의 동기화 메커니즘은 다른 코드가 Actor의 상태에 동시에 접근하지 않도록 보장합니다. 이는 잠금 또는 직렬 디스패치 큐를 수동으로 사용하여 얻는 것과 동일한 상호 배제 속성을 제공 하지만 Actor의 경우 Swift에서 기본적으로 제공합니다. 동기화를 수행해주지 않으면 Swift가 컴파일러 오류를 생성하므로 동기화 수행을 잊어서는 안 됩니다.

Actor 타입의 특징은 인스턴스 데이터를 프로그램의 나머지 부분과 분리하고 해당 데이터에 대한 동기화된 접근을 보장한다는 것입니다.
어떻게 가능할까요?

아래 인용문과 예제 코드는 swift-evolution > proposals > 0306-actors.md을 참고하였습니다.

Actor 격리는 액터가 변경 가능한 상태를 보호하는 방법입니다. Actor의 경우 이 보호를 위한 기본 메커니즘은 저장된 인스턴스 속성에만 자체적으로 직접 액세스할 수 있도록 허용하는 것입니다. 예를 들어, 다음은 한 계좌에서 다른 계좌로 돈을 이체하는 방법입니다.

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'
  }
}

BankAccount가 클래스인 경우 transfer(amount:to:) 메서드는 형식이 잘 구성되어 있지만 외부 잠금 메커니즘 없이 동시 코드에서 데이터 레이스가 발생할 수 있습니다.

Actor의 경우 other.balance를 참조하려고 하면 컴파일러 오류가 발생합니다. 왜냐하면 balance는 자체에서만 참조될 수 있기 때문입니다. 오류 메시지에는 balanceActor 격리되어 있다는 점, 즉 balance가 연결되거나 "격리된" 특정 Actor 내에서만 직접 접근할 수 있음을 나타냅니다. 이 경우 self가 참조하는 BankAccount 인스턴스입니다. 저장 및 계산된 인스턴스 프로퍼티(예: balance), 인스턴스 메서드(예: transfer(amount:to:)) 및 인스턴스 첨자를 포함하여 Actor 인스턴스에 대한 모든 선언은 기본적으로 모두 Actor 격리됩니다. Actor 분리 선언은 동일한 행위자 인스턴스(self)의 다른 Actor 분리 선언을 자유롭게 참조할 수 있습니다. Actor 분리되지 않은 선언은 분리되지 않으며 Actor 분리 선언에 동기적으로 접근할 수 없습니다.

정리하자면, Actor의 인스턴스 프로퍼티는 그 자체에서만 참조될 수 있기 때문에 외부에서 참조할 경우 컴파일러 오류가 발생합니다. 이러한 특징을 swift에서는 isolation(격리)라고 표현합니다.

Actor의 내부 동기화 메커니즘은 하나의 increment()가 완료된 후에 다음 increment()가 실행되도록 해줍니다. 따라서 실제로 실행해보면 하나가 먼저 처리되고 그 다음이 처리됩니다.

격리 기능으로 인한 것은 이해했지만, 정확히 어떤 원리로 가능한 것인지 아직은 의문이 생깁니다. 위의 WWDC를 보면서 그 의문이 조금 해결되었습니다.

외부에서 Actor와 상호작용할 때마다 비동기로 처리됩니다. 만약 Actor가 사용 중이라면 사용 중인 CPU가 Actor에 접근 중인 코드를 일시 정지하고 다른 작업을 수행합니다. 이후에 Actor의 사용이 끝나면 정지해둔 코드를 다시 실행해서 Actor에 접근합니다. 위 코드에서 await 키워드는 Actor에 대한 비동기 호출이 일시 정지될 수도 있음을 나타냅니다.
(이 부분은 해석이 잘 안 되어서 [WWDC 2021] Protect mutable state with Swift actors 블로그를 참고했습니다.)

오... 제 뇌피셜이긴 한데, 컴파일러가 Actor의 사용을 추적하여 작업 순서를 정해주는 느낌이 듭니다. 그러면 당연히 비동기 작업이 잠시 일시정지 될 수도 있겠군요!

다음 내용들은 Actorprotocol을 어떻게 적용하는지, closure와 함수를 다루는 내용들도 있고.. 문서의 경우, Cross-actor를 다루고 있습니다. 관심 있으신 분들은 위의 링크를 눌러 공부해보셔도 좋을 거 같습니다👍

일단 여기까지 Actor에 대해 간단히 알아보았습니다.
앞서 말씀드린 내용들도 같이 다뤄보고 싶었지만... 이번주는 취업 준비로 시간이 많이 없었네요🥺 죄송합니다🙇‍♀️

간단정리

Actor는 공유된 변경 가능 상태에 대한 동기화 메커니즘입니다. Actor 격리 기능은 액터가 변경 가능한 상태를 보호하여 데이터 레이스를 방지해줍니다.

profile
얼레벌레 취준 공부 중인 초보 개발자

1개의 댓글

comment-user-thumbnail
2024년 1월 13일

너무 잘 읽었습니다 !! ㅎㅎ

답글 달기