WWDC

WWDC 2021 - Protect mutable state with Swift actors 정리

Goniii 2025. 9. 9. 22:23

https://developer.apple.com/videos/play/wwdc2021/10133/

 

Protect mutable state with Swift actors - WWDC21 - Videos - Apple Developer

Data races occur when two separate threads concurrently access the same mutable state. They are trivial to construct, but are notoriously...

developer.apple.com

 

 

 

Data Race는 두 개의 개별 스레드가 동일한 변경 가능한 상태에 접근할 때 발생하며 디버깅이 매우 어렵다

해당 세션은 아래의 내용을 다룬다

  1. Swift Actor를 사용하여 Data Race를 중단하는 방법
  2. actor의 작동 방법과 actor 끼리 값을 공유하는 방법
  3. actor의 격리(isolation)가 프르토콜 준수에 미치는 영향
  4. Main Actor: 코드가 메인스레드에서 실행되는 것을 보장하는 actor

먼저 “Meet async/await in Swift"를 시청하는 것이 좋다

 


1. 동시성 문제와 Data Race (0:22)

  • 동시성 프로그램을 작성할 때 어려움 중 하나는 Data Race를 피하는 것이다

 

DataRace란?

  • 두 개의 개별 스레드가 동일한 데이터에 동시에 접근하고, 한 번 이상의 쓰기를 할 때 발생한다
  • Data Race는 쉽게 구성되지만, 디버깅 하기는 매우 어려움

 

간단한 카운터 예제를 살펴보자 (0:42)

다음은 카운터를 증가시키고 새 값을 반환하는 하나의 작업이 있는 클래스다

 

 

두 개의 Concurrent Task에서 증가를 시도해보자

결과는 실행 타이밍에 따라서 1과 2가 출력되거나, 2와 1이 출력될 수 있다

이 결과를 예상했겠지만, 두 작업 모두 1과 1을 출력할 수도 있다

또는 return 문이 두 증가 연산 후 실행되면 2와 2를 출력할 수도 있다

따라서 Data Race는 피하기 어렵고 디버깅하기 어렵다

💡
- 데이터 경합을 유발하는 접근을 코드 한 부분만 보고는 찾을 수 없고, 프로그램의 다른 부분까지 고려해야 하므로 문제를 이해하려면 코드 전체를 아우르는 “비로컬 추론”이 필요하다
- 또한, 운영체제의 스케줄러가 동시에 실행 중인 여러 작업을 조금씩 나눠서 실행시키는데, 이 순서는 개발자가 직접 제어할 수 없기 떄문에 예측 불가능하다
   → 위 예제에서 작업들의 예측 결과가 여러 개인 이유
- 따라서 동시성 버그 재현이 어렵다 

 

Data Race를 피하는 방법

데이터 경헙은 공유 변경 상태로 인해 발생한다

데이터를 변경할 수 없거나, 여러 동시 작업에 공유되지 않으면 데이터 경합이 발생할 수 없음

  1. value 타입을 사용하여 공유 변경 가능한 상태를 제거하는 것
    • 값 타입은 모두 복사되어 로컬에서 변경하기 때문에 외부에서 경합이 발생하지 않음
  2. let 속성은 값을 변경할 수 없으므로 동시 작업에 접근하는 것이 안전함
    • 값이 바뀔 일이 없기 때문

→ Swift에서 Value 타입을 장려하는 이유!!!!

  • 동시 프로그램에서 안전하게 사용할 수 있기 떄문

 

Value 타입의 예시를 보자

  • 몇 가지 값이 있는 배열을 만들고, 해당 배열을 두 번째 변수에 할당
  • 배열의 각 복사본에 다른 값을 추가
  • 마지막 두 배열을 출력하면 초기화 값은 포함되지만, 추가된 값은 각 복사본에만 존재함

  • Dictionary, Array와 같은 컬렉션 타입을 포함하여 Value 타입이다

 

 

위 예시에서 카운터 클래스를 구조체로 변환하여 값 타입으로 만들어보자

  • value 속성을 수정하도록 increment 함수에는 mutating 키워드 추가
  • 기존 counter 변수를 var로 변경
  • 그러나 위 코드는 두 동시 작업에서 참조되기 때문에 경합 조건이 생겨 컴파일 에러 발생

 

  • 각 Task 내에 로컬 변수로 카운터를 할당하면 오류를 해결할 수 있음

  • 이 예제를 실행하면 두 작업은 항상 1을 출력함
  • 이 코드는 데이터 경합은 없지만, 우리가 원하는 결과를 출력하지 않음
  • 변경 가능한 상태를 공유하지 못함

 

데이터 경합을 피하는 동기화 방법

  • 동시 프로그램에서 변경 가능한 상태를 공유하는 경우, 데이터 경합이 발생하지 않도록 동기화가 필요함
  • atomics 또는 locks와 같은 하위 수준 도구와 Serial Dispatch Queues와 같은 상위 수준 구조로 동기화할 수 있음
  • 각 요소는 다양한 강점이 있지만, 동일한 치명적인 약점이 있음
    • 매번 정확하게 사용하려면 신중한 규율이 필요하며, 그렇지 않으면 경합이 발생함

→ 따라서, actor가 필요한 것!

 

Actor (4:40)

  • actor는 공유할 수 있으며 변경 가능한 상태에 대한 동기화 매키니즘이다
  • actor에는 자체 상태가 있으며 해당 상태는 프로그램의 나머지 부분과 격리됨
  • 해당 상태에 접근하는 유일한 방법은 액터를 통과하는 것
  • 액터를 통과할 때마다 액터의 동기화 메커니즘은 다른 코드가 액터의 상태에 동시에 접근하지 않도록 함
  • locks, serial dispatch queue를 사용하여 수동으로 막는 것과 동일한 상호 배제 속성을 제공하며, Swift가 보장해줌
  • Swift가 컴파일 오류를 생성하므로 동기화 수행을 잊을 수 없음

 

Actor는 Swift의 새로운 종류의 타입

  • 다른 타입과 동일한 기능을 제공함
  • 속성, 메서드, 이니셜라이저, subscripts 등을 가질 수 있음
  • 프로토콜을 준수하고 extension할 수 있음
  • 클래스와 마찬가지로 참조 타입 (actor의 목적이 공유, 변경 가능한 상태를 표현하는 것이기 때문)
  • actor의 주요 특징은 인스턴스 데이터를 프로그램의 나머지 부분과 격리하고 해당 데이터에 대한 동기화된 접근을 보장하는 것

여기서는 카운터를 액터 타입으로 정의했다

클래스 타입과의 차이점은 actor가 value에 동시에 접근하는 것을 막는 다는 것이다

이러한 보장은 actor에서의 데이터 경합의 가능성을 제거함

 

Data Race 예시를 다시보자

  • 동일한 카운터를 증가시키려는 두 개의 동시 작업이 있을 때,
  • actor의 내부 동기화 메커니즘은 하나의 increment 호출이 완료될 때까지 다른 호출은 대기해야 함

→ 따라서, 위 코드는 1과 2 또는 2와 1의 출력을 할 수 있지만, 데이터 경합이 제거되었기 때문에 2와 2를 동시에 얻을 수 없음

 

 

두 동시 작업이 동시에 카운터를 증가시키려고 할 때, 실제로 어떤 일이 발생하는 지 생각해보자

우선, 하나의 작업이 먼저 실행되면, 다른 하나의 작업은 차례를 기다려야 함

두 번쨰 작업이 다음 차례를 기다리려면 어떻게 해야 될까?

→ 외부에서 actor에 접근할 때마다 비동기식으로 상호작용하면 됨!

  • 액터가 사용 중이면 실행 중인 CPU가 다른 유용한 작업을 수행할 수 있도록 코드가 일시 중단됨
  • 액터가 자유로워지면 실행을 재개하여 액터에서 호출을 실행할 수 있음
  • await 키워드는 actor에 대한 비동기 호출에 일시 중단이 포함될 수 있음을 의미함

 

오래 걸리는 작업에서의 예시를 보자

아래의 코드는 value를 0으로 다시 설정한 후 원하는 횟수만큼 메서드를 호출해 값을 변경한다

resetSlowly 메서드는 Counter라는 Actor의 Extension에 정의되어 액터 내부에 있다

즉, 액터의 상태에 직접 접근할 수 있어, value를 0으로 설정할 수 있고, increment 메서드 호출과 같이 액터의 다른 메서드를 동기적으로 호출할 수 있다

이미, 액터에서 실행 중임을 알기에, 기다릴 필요가 없음 → actor의 중요한 속성

따라서 actor에서도 동기 코드에 대해 순차적으로 실행할 수 있지만, 비동기 코드와 상호 작용할 수 있다

 

Actor reentrancy (재진입)

이미지 다운로더 actor 예시를 보자

  • 이미지 다운로드는 다른 메서드에서 진행하며, 다운로드한 이미지를 캐시에 저장함
  • 캐시를 확인하고, 이미지를 다운로드한 뒤 반환 전에 캐시에 이미지를 기록하는 메서드
  • actor를 사용하므로 low-level data races에서 자유로워 원하는 수의 이미지를 동시에 다운로드할 수 있다
  • actor의 동기화 메커니즘으로 한 번에 하나의 작업만 캐시 인스턴스 속성에 접근을 보장하므로 캐시가 손상될 수 없음
  • 중요한 점은 awiat 키워드!!!
    • await은 이 시점에 함수를 일시 중단할 수 있음을 의미하며, 프로그램이 다른 코드를 실행할 수 있도록 CPU를 포기하여 전체 프로그램 상태에 영향을 줌
    • 함수가 다시 시작되는 시점에서 전체 프로그램 상태가 변경됨
    • 따라서, 대기 전의 상태가 대기 후에도 유효할 것이라고 생각하면 안 됨!!

 

동일한 이미지를 동시에 가져오는 두 개의 동시 작업이 있다고 가정해보자

  • 첫 번째 작업은 캐시가 없는 것을 확인 후 이미지를 다운로드 하기 위해 일시 중단 됨

이때, 첫 번째 작업이 URL로 웃는 고양이 이미지를 다운로드하는 동안, 서버에서 동일한 URL을 우는 고양이 이미지로 변경했다고 가정해보자

 

두 번째 작업은 첫 번째 작업이 아직 다운로드 완료되지 않았기 때문에, 캐시에 없으므로 다운로드를 시작하여 일시 중단될 것이다

 

 

 

잠시후 다운로드 중 하나(첫 번째라고 가정, 순서 보장x)가 완료되어 해당 작업이 액터에서 재개된다

웃는 고양이 이미지를 캐시를 등록하고 이미지를 반환한다

 

 

두 번째 작업이 완료되면 캐시에 동일한 URL의 값으로 우는 고양이 이미지를 덮어쓰고 반환한다

→ 따라서 캐시에 이미 이미지가 있더라도 동일한 URL에 대해 다른 이미지를 얻게 됨

 

이미지를 캐싱하면 수동으로 삭제할 때까지 항상 동일한 URL에 대해 동일한 이미지를 가져올 것으로 예상했지만, 캐시된 이미지가 예기치 않게 변경되었다

낮은 수준의 데이터 경쟁은 없지만 대기 기간 동안 상태가 동일할 것이라는 가정을 했기 때문에 이러한 버그가 발생했던 것이다

 

await 이후 상태를 확인하도록 수정해보자

 

  • 다시 재개될 때, 캐시에 이미 항목이 있는 경우 원래 버전을 사용하고 다운 받은 이미지는 버린다
  • 더 좋은 해결책은 중복 다운로드를 완전히 피하는 것

 

 

actor의 재진입은 교착 상태를 방지하고 진행을 보장하지만 각 대기에 대한 가정을 확인해야 함

  • 재진입을 잘 설계하려면 동기 코드 내에서 actor 상태의 변형을 수행해야 함
  • 이상적으로는, 모든 상태 변경이 잘 캡슐화되도록 동기 함수 내에서 수행
  • 전역 상태, clocks, timers 또는 액터에 대한 모든 가정은 await 후에 확인해야 함

Actor Isolation (13:03)

actor 격리는 actor 타입의 동작에 기본이 된다

actor 격리가 프로토콜 준수, 클로저 및 클래스를 포함한 다른 기능과 상호 작용하는 방법에 대해 알아보자

 

프로토콜

예를 들어, LibraryAccount actor가 Equatable 프로토콜을 준수해보자

 

 

  • static equality method은 ID 번호를 기반으로 두 개의 도서관 계정을 비교
  • 메서드가 정적이기 때문에 액터에 격리되지 않음 (객체 없이 접근하므로)
  • 대신, actor type의 두 매개변수를 외부에서 주입 받는 형태
  • actor의 불변 상태에만 접근하기 때문에 괜찮은 형태

LibraryAccount가 Hashable 프로토콜을 준수하도록 변경해보자

Hashable을 준수하려면 hash(into:) 작업을 구현해야 한다

그러나 컴파일 오류가 발생한다

  • 왜냐, Hashable을 준수한다는 것은 이 함수가 액터 외부에서 호출될 수 있다는 것을 의미하지만,
  • hash(into)는 비동기가 아니므로 액터 격리를 유지할 방법이 없기 때문

 

이 문제를 해결하기 위해 nonisolated 키워드가 등장한다

  • nonisolated는 이 메서드가 구문적으로 액터에 작성되어 있더라도 액터 외부에 있는 것으로 처리됨을 의미함
  • 따라서 액터에 속하지 않다는 것을 의미하기 때문에 Hashable 프로토콜 요구사항을 충족할 수 있음
  • nonisolated 메서드는 actor 외부에 있는 것으로 처리되므로, actor 내부의 상태를 참조할 수 없음
  • 이 예제에서는 idNumber를 참조하는데 idNumber는 let으로 선언된 변경할 수 없는 값이기에 사용 가능

 

만약, booksOnLoan 변수 기준으로 해시를 시도한다면?

  • 외부에서 변경 가능한 상태에 접근하기 때문에 컴파일 오류 발생

 

클로저

클로저는 하나의 함수 내에 정의된 작은 함수로, 나중에 호출하기 위해 다른 함수에 전달될 수 있음

함수와 마찬가지로, 클로저는 actor 격리일 수도 있고, 비격리일 수도 있음

아래의 예시는 대여 중인 각 책의 일부를 읽은 총 페이지 수를 반환한다

  • reduce는 클로저가 포함됨
  • 클로저 내부 readSome에 대한 호출에는 await이 없음
  • actor의 격리 함수 “read” 메서드 내에서 형성된 클로저는 그 자체로 actor 격리임
    • 격리 메서드 내에서 호출한 클로저도 격리
    • reduce 작업이 동기적으로 실행되고, 동시에 접근을 유발할 수 있는 다른 스레드로 클로저를 이스케이프 할 수 없기 때문에 안전하기 때문

 

클로저 내부 분리된 작업을 만들어보자

  • 분리된 작업은 actor가 수행 중인 다른 작업과 동시에 클로저를 실행함
  • 따라서 클로저는 actor 외부에 존재하며 데이터 경합이 생길 수 있음
  • actor 외부에서 호출하는 것이므로 await 키워드를 사용하여 비동기적으로 호출해야 함

 

actor isolation and data (17:02)

LibraryAccount 예제에서 Book 타입이 구조체와 같은 Value 타입으로 가정해보자

  • Value 타입은 LibraryAccount Actor의 인스턴스에 대한 모든 상태가 자체 포함되기 때문에 좋은 선택임

 

랜덤 책을 선택해서 title을 바꾸는 메서드

  • 계속해서 이 메서드를 호출하여 임의의 책을 선택하면 책의 사본을 얻을 수 있음
  • 책 사본을 변경해도 actor에 영향을 미치지 않으며, 그 반대의 경우도 마찬가지

 

그러나 책은 class 타입으로 바꾸면?

  • 이제 LibraryAccount actor가 book 클래스의 인스턴스를 참조함
  • 그 자체로는 문제가 되지 않음
  • 그러나, 위 메서드를 호출하게 되면 액터 외부에서 액터의 변경 가능한 상태에 대한 참조가 생김

     → Data Race 발생

  • 이제 책 제목을 업데이트 하면 actor 내에서 접근할 수 있는 상태에서 수정이 발생함
  • visit 메서드가 actor 내부에 없기 때문에 이 수정은 데이터 경합이 될 수 있음

→ value 타입인 struct는 복사되어 변경을 해도 actor에 변함이 없지만, 클래스는 참조 타입이기 때문에 actor의 값에도 변화가 적용됨

 

 

이때 동시에 사용해도 안전한 형식인 Sendable을 사용해야 함!


Sendable (18:32)

  • Sendable 타입은 여러 액터 간에 값을 공유할 수 있는 타입
  • 한 위치에서 다른 위치로 값을 복사하고, 해당 값의 자체 복사본을 안전하게 수정할 수 있는 경우 Sendable이 될 수 있음
  • Value 타입은 각 복사본이 독립적이기 때문에 Sendable
  • actor 타입은 변경 가능한 상태에 대한 접근을 동기화하기 떄문에 Sendable
  • Class는 Sendable이 될수도 있지만 신중하게 구현된 경우에만 가능함
    • 클래스와 모든 하위 클래스가 변경할 수 없는 데이터만 보유하는 경우 Sendable
    • 클래스가 안전한 동시 접근을 보장하기 위해 내부적으로 동기화(lock 등)를 수행한 경우 Sendable
    • 그러나, 대부분의 클래스는 둘 다 아니며, Sendable이 아님
  • actor(모든 동시 코드)는 주로 Sendable 형식으로 통신해야 함
  • Sendable 형식은 데이터 경합으로부터 코드를 보호함
  • Sendable 형식이 아닌 형식으로 전달하면 오류남

 

Sendable 타입인지 어떻게 알까?

  • Sendable은 프로토콜이라 다른 프로토콜과 마찬가지로, 준수하면 됨
  • Book 구조체는 저장된 모든 속성이 Sendable 타입인 경우 Sendable일 수 있음

 

하지만, Author가 클래스 타입이라면?

  • Swift는 Book이 Sendable이 아니라는 컴파일 오류 생성

 

제네릭 형식의 경우 Sendable

  • 제네릭 형식의 경우 Sendable 여부는 제네릭 인수에 따라 달라질 수 있음
  • 적절한 조건부를 사용하여 Sendable을 사용할 수 있음
  • 쌍 형식은 두 제네릭 인수가 모두 Sendable일 경우에만 Sendable이 됨

 

@Sendable (21:14)

값을 동시에 공유하기에 안전한 형식에 Sendable을 도입하는 것이 좋다

액터 내에서는 @Sendable 타입을 사용한다

  • 함수 자체는 Sendable이 될 수 있고, 이는 액터 간에 함수 값을 전달하는 것이 안전하다는 것을 의미
  • 이는 데이터 경합 방지를 위해 클로저가 수행할 수 있는 작업을 제한하는 클로저에 중요함
    • 예를 들어, Sendable 클로저는 지역 변수에 대한 데이터 경합을 허용하기 때문에 변경 가능한 지역 변수를 캡처할 수 없음 (사용할 수 없음)
    • 클로저가 캡처하는 모든 것은 Sendable이어야 하며, 클로저를 사용하여 Sendable이 아닌 타입을 actor 경계를 넘어 사용할 수 없음
    • 동기식 Sendable 클로저는 actor 격리될 수 없음. 왜냐, actor 외부에서 코드를 실행할 수 있기 때
  • Task.detached 작업은 함수 형식의 @Sendable 클로저를 사용함

 

자 다시 세션 초반 예제를 보자 (22:20)

값 타입 Counter를 만들고, 두 개의 클로저에서 동시에 수정하려고 했다

  • 이건 변경 가능한 지역 변수(Counter.value)에 대한 데이터 경쟁이 발생할 것이다
  • Task.detached에 대한 클로저는 Sendable이므로 오류가 날 것임

→ Sendable 함수 타입은 동시 실행이 발생할 수 있는 위치를 나타내므로 Data Race를 예방 가능

 

다음은 앞선 다른 예제이다 (22:51)

  • Task.detached의 클로저는 Sendable이므로 actor에 대해 격리되어서는 안 됨
  • 따라서, 상호작용을 할 때는 비동기식어야 함

→ Sendable 타입, 클로저는 변경 가능한 상태가 actor간에 공유되지 않고 동시에 수정할 수 없는지 확인하여

actor 격리를 유지하는 데 도움이 됨

 


Main Actor (23:20)

  • 앱을 빌드할 떄는 메인 스레드에 대해 생각해야함
  • 메인 스레드는 핵심 사용자 인터페이스 렌더링이 발생하는 곳이자 사용자 상호작용 이벤트를 처리하는 곳
  • UI에서 작동하는 작업은 일반적으로 메인 스레드에서 수행되어야 함
  • 그러나 오래 걸리는 입출력 작업이나 서버 통신을 메인 스레드에서 처리하면 UI가 정지될 수 있음
  • 따라서, 오래 걸리는 작업은 메인스레드에서 빠르게 벗어나야 함
  • 따라서 메인 스레드에서 실행해야 하는 특정 작업이 있을 때 DispatchQueue.main.async를 호출하여 사용함
  • 메인 스레드와 상호작용하는 것은 actor 상호작용과 매우 유사함
    • 이미 메인 스레드에서 실행 중이라면 UI 상태에 안전하게 접근하고 업데이트 할 수 있고
    • 메인 스레드에서 실행하지 않으면 비동기적으로 상호작용해야 함
    • → 이게 actor가 작동하는 방식

 

 

메인 스레드를 설명하는 특별한 actor를 Main Actor라 함

  • 두 가지 중요한 면에서 일반 actor와 다름
    1. 메인 액터는 Main Dispatch Queue를 통해 모든 동기화를 수행함
      • 즉, 런타임 관점에서 볼 때, 메인 액터는 DispatchQeue.main을 사용하는 것과 같음
    2. 메인 스레드에 있어야 하는 코드와 데이터가 분산되어 있음
      • SwiftUI, AppKit, UIKit 및 기타 시스템 프레임워크에 있고,
      • 사용자 뷰, 뷰 컨틀롤러, UI 연결 부분 등에 분산되어 있음
  • Swift Concurrency에서 @MainActor으로 표시하면 메인 액터에서 실행해야 함을 표시할 수 있음
    • 메인 액터 외부에서 호출하는 경우, 메비동기적으로 수행되도록 기다려야 함 (await 키워드 표시)
    • → @MainActor를 사용하면 DispatchQueue.main를 사용하는 시기에 추측할 필요 없음

 

 

Main actor types

  • 메인 액터는 타입에도 배치할 수 있으며, 이는 모든 멤버와 서브 클래스가 메인 액터에 존재하도록 함
  • 대부분 메인 스레드에서 실행하는 UI 코드에 유용함
  • 개별 메서드는 일반 actor처럼 nonisolated 키워드를 사용 가능함

마무리 (26:57)

이 세션에서는 actor가 동시 접근에서 변경 가능 상태를 보호하는 방법에 대해 설명했다

  • 액터를 사용해서 Swift 코드에서 안전한 동시 추상화를 구축해보자
  • 액터를 구현할 때와 모든 비동기 코드에서 항상 재진입을 생각하며 설계하자
  • 값 타입과 actor를 함께 사용하여 데이터 경합을 제거하자
  • UI와 상호작용은 항상 메인 액터에서 하자

자체 애플리케이션 내에서 액터를 사용하는 방법에 대한 자세한 내용은 Swift 동시성을 위한 앱 업데이트에 대한 세션을 확인하세요

Apple Inc. Swift concurrency: Update a sample app - WWDC21 - Videos - Apple Developer

액터를 포함한 Swift의 동시성 모델 구현에 대해 자세히 알아보려면 "비하인드 스토리" 세션을 확인하세요.

Apple Inc. Swift concurrency: Behind the scenes - WWDC21 - Videos - Apple Developer

 


 

정리

Data Race (데이터 경합)

정의

  • 두 개 이상의 동시 작업에서 동일한 데이터에 동시에 접근하고, 한 번 이상의 변경을 할 때 발생하는 문제
  • 값이 예측 불가능해짐

왜 문제인가?

  1. 비결정성(Nondeterminism)
    • 프로그램을 실행할 때마다 결과가 달라짐
    • 원인: OS 스케줄러가 어떤 순서로 스레드를 실행할지 예측할 수 없음 → 실행 순서에 따라 값이 달라짐
  2. 비로컬 추론 필요(Non-local reasoning)
    • 데이터 경합을 유발하는 접근을 코드 한 부분만 보고는 찾을 수 없고,
    • 프로그램의 다른 부분까지 고려해야 하므로 문제를 이해하려면 코드 전체를 파악해야 함
  3. 디버깅 어려움
    • 비결정성이므로, 동작을 재현하기 어려움

 

Actor

정의

  • 동시성 환경에서 공유 자원을 안전하게 상태(state)를 관리하기 위해 고안된 참조 타입

특징

  • Data Race를 막기 위해, 한 번에 하나의 작업만 actor 내부에 접근할 수 있게 보장함
  • 내부 상태에 접근하려면 await 필요
  • actor는 기본적으로 격리된 실행 컨텍스트를 갖음 (Actor Isolated)

 

Actor Reentrancy (액터 재진입성)

정의

  • Actor가 await 지점에서 일시 중단(suspend)되면, 그 순간 다른 Task가 해당 Actor의 메서드에 진입하여 상태를 바꿀 수 있는 성질

문제 상황

  • 액터는 한 번에 하나의 Task만 실행을 보장
  • 하지만 await 중에 다른 작업이 실행되므로 상태가 바뀌어 resume 되었을 때, 상태가 바뀌어 있을 수 있음

주의점

  • await 전후로, 상태(state)가 바뀔 수 있음을 항상 고려해서 설계해야 함

 

Actor Isolation (액터 격리)

정의

  • 액터의 상태는 그 액터 내부 코드에서만 직접 접근 가능하고, 외부에서는 await를 통해서만 간접 접근해야 하는 규칙

의미

  • 상태를 외부에서 직접 접근할 수 없게 해서 데이터 경쟁을 원천 차단
  • Swift 컴파일러가 이 규칙을 강제 → 안전성 보장

예외

  • 액터 내부에서 nonisolated 키워드 사용 → 격리를 해제할 수 있음

 

Sendable

정의

  • 여러 Task 또는 Actor 간에 안전하게 주고 받을 수 있는 값을 나타내는 프로토콜

왜 필요?

  • 동시성 환경에서는 객체를 복사 없이 공유하다가 데이터 경쟁이 발생할 수 있음
  • Sendable은 “이 값은 동시적으로 상용해도 안전하다”는 컴파일러 검증 장치

기본 규칙

  • Int, String, Bool, Array(요소가 Sendable일 때) 같은 값 타입은 자동으로 Sendable
  • Actor 타입은 변경 가능한 상태에 대한 접근을 동기화하기 때문에 Sendable
  • 참조 타입(class)는 기본적으로 Sendable이 아님
    • 모든 하위 클래스가 변경할 수 없는 데이만 보유한 경우 Sendable
    • 안전한 동시 접근 보장을 위해 내부적으로 동기화를 보장하는 경우 Sendable

 

Main Actor

정의

  • UI 업데이트 같은 메인 스레드 전용 작업을 안전하게 처리하기 위해 제공되는 액터

역할

  • UIKit/AppKit과 같은 UI 프레임워크는 오직 메인 스레드에서만 안전하게 접근 가능
  • @MainActor는 해당 함수나 속성이 메인 스레드에서만 실행됨을 보장
728x90