Swift Concurrency는 async/await와 Task 같은 개념과 함께 글로벌 액터(global actor) 라는 개념을 도입했어요.
가장 흔히 쓰이는 글로벌 액터는 @MainActor이지만, 직접 커스텀 글로벌 액터를 정의해서 사용하는 것도 가능합니다.
많은 개발자에게 글로벌 액터가 언제, 어떻게 사용되는지는 여전히 다소 생소할 수 있는데, 이 글에서는 글로벌 액터의 개념과 사용 방법, 주의점 등을 정리해놨습니다.
Global Actor는 말 그대로 “전역적으로 사용할 수 있는 액터”입니다.
일반 액터(actor)가 특정 인스턴스의 상태 접근을 병렬성(동시성)으로부터 격리(isolation)하는 것처럼, 글로벌 액터는 전역 수준에서의 데이터나 기능 접근을 격리하는 역할을 해요.
예를 들어 @MainActor는 UI 업데이트처럼 항상 메인 스레드에서 실행되어야 할 코드에 붙여서, 해당 코드가 메인 스레드 격리 도메인(main-thread actor executor) 안에서 안전하게 실행되도록 보장합니다.
@MainActor
func updateUI() {
// 메인 스레드에서 안전하게 실행됨
}
@MainActor를 속성(property)이나 타입(type)에 붙이는 것도 가능합니다:
@MainActor
var titles: [String] = []
@MainActor
final class ContentViewModel {
var titles: [String] = []
// ...
}
이런 식으로 붙이면, 그 속성이나 타입의 접근은 모두 @MainActor 격리 도메인 내에서만 허용돼요.
하지만 글로벌 액터는 메인 스레드에만 한정되지 않아요 — 직접 글로벌 액터를 정의해서 앱의 특정 기능 영역을 격리할 수도 있습니다.
예를 들어, 이미지 처리(image processing) 작업이 많은 앱이 있다고 가정해볼게요.
이미지 처리 기능은 동시에 여러 작업이 돌면 안 되고, 이미지 처리 관련 코드는 하나의 동기화된 흐름으로 실행되고 싶을 수 있어요.
이럴 때 커스텀 글로벌 액터를 정의할 수 있습니다:
@globalActor
actor ImageProcessing {
static let shared = ImageProcessing()
}
@globalActor attribute 를 붙이면 이 액터가 Global Actor로 간주됩니다.GlobalActor 프로토콜을 준수해야 하고, static let shared 인스턴스를 제공하면 됩니다. 이렇게 정의하면, 이후 코드에서 다음처럼 사용할 수 있어요:
@ImageProcessing
final class ImageCache {
var images: [URL: Data] = [:]
// 이미지 캐싱 로직 등…
}
@ImageProcessing
func applyFilter(_ inputImage: UIImage) -> UIImage {
// 이미지 필터 적용 …
}
이제 @ImageProcessing으로 표시된 클래스나 함수는 ImageProcessing 글로벌 액터 격리 도메인에서 실행됩니다. 이렇게 하면 이미지 처리 관련 상태 접근을 중앙에서 안전하게 관리할 수 있어요.
그런데 위 예제에서 문제점이 하나 있어요 — 바로 누군가가 글로벌 액터 인스턴스를 직접 생성해버릴 수 있다는 것:
// 가능하긴 하지만, 이건 의도하지 않은 사용 방식이에요.
ImageProcessing()
이렇게 하면 새로운 액터 인스턴스가 생성되면서, 서로 다른 실행자(executor)가 만들어질 수 있고, 결국에는 동시성 안전성이 깨지는 문제가 생기죠.
이걸 막으려면 글로벌 액터의 init를 private으로 선언하는 게 좋습니다:
@globalActor
actor ImageProcessing {
public static let shared = ImageProcessing()
private init() { }
}
이렇게 하면 외부에서는 ImageProcessing()과 같이 생성할 수 없게 되어, 오직 shared 인스턴스를 통해서만 접근하게 돼요. 이렇게 해서 글로벌 액터의 중복 인스턴스 생성을 방지할 수 있습니다.