특별한 Actor - Main Actor
Main Thread & Main Actor
우리의 메인 스레드는 UI 렌더링 및 사용자 이벤트가 처리되는 곳이다.
왜 메인 스레드에서 UI 렌더링 해야 하는지는 지난 번에 다뤄보았다.
모든 작업을 메인 스레드에서 처리해버리면 UI 렌더링에 문제가 생겨 버벅일 수 있다.
우리는 메인 스레드에서 실행해야 할 오래 걸리는 작업이 있으면 GCD Async를 통해 백그라운드로 보냈다. 그리고 그 코드는 메인 스레드에서 동작할 거다.
메인 스레드와 상호작용하는 것이 actor와 상호작용하는 것과 유사하다.
메인 스레드에서 실행중이면 UI State에 안전하게 접근하여 업데이트, 메인 스레드가 아니라면 비동기로 스레드와 상호작용하는 원리 말이다.
메인 스레드를 나타내는 특별한 Global Actor를 우리는 Main Actor
라 한다.
actor 코드는 백그라운드 스레드에서 실행되지만, Main Actor로 격리된 코드는 무조건 메인 스레드에서 실행된다. 즉, 사용자 인터페이스에 대한 작업은 메인 액터
에서 실행해야 한다.
Main Actor가 아무리 특별하더라도 Actor이기에 한 번에 한 작업만 실행한다.
결국 오래 걸리는 작업을 Main Actor에서 수행한다면 메인 액터 Blocking에 의해 UI가 응답하지 않을 수 있다.
따라서 일반 actor 또는 detached Task에 넣음으로써 백그라운드 스레드로 이동시켜야 한다.
Main DispatchQueue
메인 액터는 메인 GCD를 통해 모든 동기화를 수행한다.
actor의 executor 관점에서 main actor의 executor는 메인 GCD에 해당한다.
DispatchQueue.main.async {}
await MainActor.run {}
따라서 Main Actor는 DispatchQueue.main 을 사용하여 교체할 수 있고, 반대로 DispatchQueue.main.async 작업은 MainActor.run으로 대체할 수 있다.
static func run<T>(
resultType: T. Type = T. self,
body: @MainActor () throws -> T
) async rethrows -> T where T: Sendable
MainActor.run은 await와 함께 호출한다.
메인 스레드에서 동작할 수 있을 때까지 기다려야 할 수도 있기 때문에 await를 통해 Suspend 시키고 해당 스레드에서 다른 코드가 실행될 수 있도록 하는 것이다.
따라서 메인 스레드에서 한 꺼번에 많은 작업이 이뤄지길 원하는 경우 아래처럼 묶어서 호출해야 일시 중단 없이 실행가능하다.
await MainActor.run {
// UI 관련 코드 1
// UI 관련 코드 2
}
@MainActor
해당 어트리뷰트를 사용하면 Main Actor로 격리를 할 수 있다.
actor MyActor {
let id = UUID().uuidString
// property 에 붙으면, 해당 property 는 main thread 에서만 접근 가능
@MainActor var myProperty: String
init(_ myProperty: String) {
self.myProperty = myProperty
}
// method 에 붙으면, 해당 메서드는 main thread 에서만 호출 가능
@MainActor func changeMyProperty(to newValue: String) {
self.myProperty = newValue
}
func changePropertyToName() {
Task { @MainActor in
// block 안에 들어있는 코드도 main actor 에서 실행
myProperty = "naljin"
}
}
}
프로퍼티, 메소드, 클로저와 같은 곳에 사용 가능하고 Main Actor에 격리되지 않은 맥락에서 호출하는 경우, Main Actor로의 전환과 기다림이 있을 수 있기에 await를 통해 호출해야 한다.
왜? Main Actor도 결국 Actor니까 해당 작업은 actor-isolated가 적용된다.

실제로 changeMyProperty
메소드가 MainActor에 격리되어 해당 액터의 격리된 메소드(= changePropertyToCity
)는 메인 액터 함수 호출 시도 시, async로 자동 표기되어 await로 호출해야 한다.
액터 내의 메소드가 호출하려는 메소드는 메인액터에 격리가 되어 있기 때문에 await를 호출한다.
반면, 액터 내의 메소드가 MainActor라면 await로 호출할 필요 없다. 같은 액터에 격리된 것이니까,
위 예제처럼 프로퍼티, 메소드, 블록이 아니라 아래처럼 클래스, 구조체, 열거형과 같은 타입에 @MainActor가 붙으면 해당 타입 내부의 모든 내용은 Main Actor에 격리되고, 메인 스레드에서 동작한다.

우리가 자주 사용하는 UI와 관련한 클래스들은 모두 @MainActor
가 붙어 있기에 viewDidLoad
와 같은 메소드도 메인 스레드에서 동작하는 것이다.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
Main Actor 클래스도, 다른 actor처럼 데이터가 격리되어 있으므로 해당 참조 또한 Sendable하다.
Main Actor 안에서 Task 생성

Task를 생성하는 시점에 도달하면 Swift는 원래 scope와 동일한 actor에서 실행되도록 스케줄링한다.
위 코드에서는 Main Actor
일 것이다.

컨트롤은 호출자에게 즉시 반환하고, Task는 추후에 메인 스레드에서 실행될 것이다.
MainActor로 격리된 뷰컨에서 Task를 생성해도, Task는 주변 컨텍스트를 이어가기 때문에 Task 안의 코드는 메인 스레드에서 동작한다.
Task 안에 await 호출이 있다면?

download 메소드는 async인데 결국 이 메소드도 메인 스레드에서 동작할까 ?
→ 이건 async 메소드가 어디에 격리되어 있느냐에 따라 다르다.
우선 이 상황을 그림으로 다시 말해보자.

메인 액터에서 Task를 사용하면 해당 작업도 메인 액터에 격리된다.
그러나 await를 만나면 메인 스레드 제어권을 놓게 된다.

그리고 시스템에 의해 download 함수가 진짜 실행될때 격리된 장소에서 동작하는데, 위 메소드는 struct의 메소드였다.

그럼 어디에도 격리되어 있지 않음.
그렇게 때문에 스레드 풀의 임의의 스레드에서 Task 작업을 할 수 있다.
스레드 관점에서의 MainActor와 다른 actor 전환

메인 스레드는 cooperative thread pool과 격리되어 있다.
즉, 컨텍스트 스위치를 해야한다는 것

cooperative thread pool 끼리는 Actor 호핑(Hopping)을 하여 전환이 빠르지만 메인 액터가 끼면 이야기가 다르다.
메인 액터에 격리된 updateArticles
메소드 안에서 각 루프는 두 번의 Context Switch를 한다.
하나는 Main Actor → Database Actor로, (database.loadArticle(with:)
)
하나는 Database Actor → Main Actor로 말이다. (updateUI(for:)
)

루프가 많이 반복되면 계속해서 컨텍스트 스위칭을 하게 되어 오버헤드가 발생할 것이다.

그래서 Loop에서 매번 호출해주는 각 actor 작업을 한 번에 처리할 수 있게끔하여 성능을 개선시킨다.

그럼 위와 같이 컨텍스트 스위칭이 줄어든다.
이 핵심은 메인 스레드와 cooperative thread pool 간의 전환은 컨텍스트 스위칭을 사용하기 때문이다.
레퍼런스
https://developer.apple.com/news/?id=o140tv24
https://developer.apple.com/kr/videos/play/wwdc2021/10133/
https://developer.apple.com/videos/play/wwdc2021/10254
https://developer.apple.com/videos/play/wwdc2022/110350
https://developer.apple.com/videos/play/wwdc2022/110356
https://developer.apple.com/videos/play/wwdc2022/110351
https://developer.apple.com/videos/play/wwdc2023/10170
https://developer.apple.com/documentation/Swift/Copyable
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0306-actors.md
'📱 iOS > Swift Concurrency' 카테고리의 다른 글
[문제해결] @Sendable과 isolated 실전 응용: AVCaptureDevice.requestAccess(for:) (0) | 2025.03.15 |
---|---|
[Swift Concurrency 10편] Actor는 한글 키보드로 'ㅁㅊ색' 이란 걸 아시나요? (0) | 2025.02.26 |
[Swift Concurrency 9편] Sendable 프로토콜 (0) | 2025.02.26 |
[Swift Concurrency 8편] Task와 구조화된 동시성(= Structed Concurrency) (0) | 2025.02.26 |
[Swift Concurrency 7편] 비동기 호출에서의 스레드 제어권 (0) | 2025.02.26 |