Programming/Mac & iOS

[iOS] Swift Concurrency Foundation

MB Kyle K. 2025. 10. 11. 20:10

async-await
 
비동기 함수 혹은 메소드 (이하 '비동기 함수')는 다른 비동기 방법 (GCD, Operation 등)과 몇가지 다른 차이점을 보인다. return 및 thorows 등을 일반적인 함수들과 같이 지원한다. 이는 유연하고 확장 가능한 비동기 실행 방법을 제공한다. 그리고 실행 흐름을 busy wait 하지도 않고 suspend 상태로 남겨두었다가 마저 실행된다. suspend 상태로 남겨두는 곳을 suspension point로 칭하기도 하는데, 이를 await라는 키워드를 통해서 표시한다. thread는 이 suspension point를 만나면 실행의 흐름을 넘기고 다른 코드를 즉시 실행하는 동작을 수행한다. 
 
async-let
 
'async let'을 사용하면 비동기 함수의 실행을 바로 하지 않고 유보할 수 있다. 이렇게 유보된 비동기 함수는 필요한 시점에 다른 'async let'으로 선언되었던 비동기 상수와 함께 사용가능하다. 실제로 실행이 필요한 시점에서 await를 통해서 suspension point를 생성이 가능하다.
 
AsyncSequence
 
Array와 같은 collection 단위의 결과값을 계산하기 위해서 async를 사용한다면 AsyncSequence를 사용하는 것이 도움이 된다. 이를 통해 구현된 sequence는 for-in 구문에서 async-await를 사용함으로써, 각 값의 계산을 비동기로 처리 가능하도록 구현이 가능하다.
 
Tasks and Task Groups
 
task는 비동기로 실행되는 작업의 단위이다. 앞서 얘기한 'async let'을 사용할 경우에도 암묵적으로 task를 생성한다. 우리는 Task를 이용하여 명시적으로도 생성하여 사용 가능하다. 만들어진 task 들은 TaskGroup을 사용하여 관리가 가능하다. TaskGroup을 통해서 생성된 Task (chlid task)는 동일한 Task (parent task)에게 귀속된다. Task의 우선순위, 취소 등에서 paraent task와 child task는 서로 연관되어 동작하게 된다. 이를 Structed Concurrency라고 칭한다.
 
Unstructed Concurrency는 parent task를 가지지 않도록 하는 것을 말한다. 그러나 child task를 관리하거나 연관된 parent task가 없음으로 개발자가 직접 task를 신경써서 관리하여야 한다. Task 생성자를 통해서 생성하거나, 이보다 더 분별된 방법을 사용할 경우에는 Task.detached를 사용한다. 2가지 방식 모두 현재 task의 환경 (Actor isolation, 우선순위, task-local state 등)을 상속받지 않는다. 그러나 별도로 환경을 선언해주지 않을 경우는 현재 task의 환경과 똑같은 환경으로 생성하여 실행한다.
 
일반적으로 TaskGroup을 사용한 Structed Concurrency가 task 관리가 편리하여 권장된다. Apple Doc.에서도 Unstructed Concurrency, 특히 Detached Task의 사용을 권장하지 않는다.
 
Isolation
 
같은 데이터에 동시에 접근해서 수정하는 race condition을 피하기 위해서 swift concurrency에서는 보통 3가지 방법을 제시한다.
 
1. 수정 불가능한 상수 사용
2. 생성된 데이터가 항상 동일한 task에 수정되도록 구현
    - local variable을 활용한 일시적 사용
    - closure를 활용한 capturing
3. actor를 활용한 isolation 구현
 
Main Actor
 
비동기를 활용하지 않은 코드에서는 모든 코드가 Main Actor에서 돈다. 그래서 개발 시에는 이를 항상 상정하여 개발한다. Main Actor는 UI 관련된 코드에서 Isolation을 위해 사용하는 Actor로 이해하면 된다. 
 
Actors
 
actor를 class처럼 선언하여 사용 가능하다. actor는 class처럼 reference typ이다. 하지만 class와는 다르게 모든 수정 가능한 상태에 동일한 task로만 접근하도록 한다. 이는 동일한 actor 객체에 여러 task가 접근하는 환경에서 데이터의 신뢰성과 안정성을 보장한다. 
 
Global Actors
 
actor를 여러 객체로 생성하여 사용한다. MainActor는 시스템에서 제공하는 유일한 singleton 객체이다. 만약에 사용자 정의된 singletone actor를 생성해서 쓰고 싶다면 GlobalActor를 사용하면 된다.
 
Sendable Types
 
위에서 자원의 효율성 및 유연성을 위해서 다양한 비동기 방식을 소개했다. 그런데 이러한 각 비동기 동작의 영역은 서로 격리된 공간을 지닌다. 이러한 공간을 'concurrency domain'이라고 부른다. 이 공간은 서로 격리되어 있다보니 데이터 등을 공유하지 않는다. 이를 공유하도록 만들기 위한 타입이 존재하는데, 우리는 이를 'Sendable Types'라고 한다. Sendable Types으로 만들기 위해서는 Sendable 프로토콜을 선언하면 된다. 프로토콜에 따른 코드 구현은 필요하지는 않다. 하지만 Sendable에 대한 semantic requirement를 따라야 한다.
 

  • Value type일 것
  • Reference type의 경우, 수정이 불가능한 상수만 가질 것
  • Reference type이면서 변경 가능한 데이터를 가질 경우, 각 데이터에 대한 접근을 내부적으로 관리할 것
  • 함수나 클로저의 경우, @Sendable로 marking할 것