@MainActor, @globalActor 는 결국 actor의 종류이다.
우선 actor를 왜 쓸까? 비동기적인 상황에서 concurrency를 보장해주기 위해서 사용하는 것이다.
@MainActor의 경우, 진행되는 동작을 Main thread로 보내고, thread safe하게 동작함을 보장한다.
(DispatchQueue.main의 역할과 비슷)
@MainActor와 동일한 이유로, @globalActor 역시, 해당 globalActor를 사용하게 되면 background에서 진행되는 동작들의 concurrency를 보장하고 해당 @globalActor를 전역변수로 만들어 @MainActor를 사용할 때와 동일하게 어떤 상황에서도 사용하기 쉽게 만드는 것이다.
'MainActor.run'이 사용하기 편리한 이유는 static으로 선언되어 있어 전역변수처럼 쉽게 이용이 가능하기 때문이다.


그러나, @globalActor로 선언한 actor의 경우에는 @MainActor가 main thread에서 실행됨과는 다르게 동일한 actor라고 동일한 thread에서 동작이 진행되진 않는다. 다만 동시다발적으로 일어나는 동작들의 시작과 끝을 데이터가 간섭될 위험없이 진행됨을 보장할 뿐이다.

사실 @MainActor도 @globalActor의 한 종류이다.

MainActor는 실제로 선언자체가 @globalActor로 이루어져 있으며, GlobalActor프로토콜을 채택하고 있고, 때문에 GlobalActor의 성격을 가지며, Main thread에서 실행이 된다는 점을 같이 가지고 있는 것 이다.
사실 globalActor를 공부하며 든 생각은 actor 그 자체로 이미 thread safe하게 실행이 되는건데, globalActor랑 역할이 너무 겹치지 않나? 라는 생각이었다.
하지만 필요한 부분은 항상 더 있다.
# @globalActor 사용 예시
예시를 위해 actor와 이를 이용해 @globalActor를 선언해보자
// @globalActor는 class로도, struct로도 선언할 수 있다.
@globalActor final class MyFirstGlobalActor1 {
static var shared = MyNewDataManager()
}
@globalActor struct MyFirstGlobalActor {
static var shared = MyNewDataManager()
}
actor MyNewDataManager {
func getDataFromDataBase() -> [String] {
return ["One", "Two", "Three"]
}
}
위와 동일하게 MyNewDataManager actor를 만들고, MyFirstGlobalActor를 @globalActor로 선언해놓고 이를 사용해 보자.
class GlobalActorViewModel: ObservableObject {
@Published var dataArray: [String] = []
let globalActorManager = MyFirstGlobalActor.shared
@MyFirstGlobalActor func getDataFromGlobal() {
// 여기 안이 만약 HEAVY COMPLEX task 라면 Main에서 진행되게 하면 안된다.
// MainActor혹은 Main thread에서 해당 massive한 task를 가지게 할 수 없다.
Task {
let data = await globalActorManager.getDataFromDataBase()
print("\(Thread.current)에서 진행중")
await MainActor.run(body: {
// UI와 관련된 코드는 MainActor로 보내어 Main thread에서 실행시켜야 함
self.dataArray = data
})
}
}
}
globalActor를 사용하는 ViewModel을 만들고, 이 안에 우리의 shared된 actor를 넣어주고, @MyFirstGlobalActor를 따르는 함수도 만들어 준다.
여기서 왜 shared된 actor를 사용해야 하는지 이유가 나온다. 만약 shared된 객체를 쓰지 않고, actor 그 자체로 써버린다면, actor에서 일어나는 동작들이 actor가 초기화 될 때마다 새로운 actor로 생성되기에, 전역변수처럼 쓰이는 단 하나의 shared된 actor가 필요한 것이다.
getDataFromGlobal() 안의 주석에 쓰여 있듯이, GlobalActor의 사용이유가 나오는데, 예를 들어 'Task{ }' 바로 위에서 굉장히 무겁고 massive한 코드가 Main thread에서 진행되면, 화면이 나오지 않고 해당 작업이 완료될 때까지, 계~~속 기다리는 현상이 일어나는데 이는 사용자로 하여금 좋은 경험이 아니기에, 해당 getDataFromGlobal()함수는 실행될때 background인 actor로 실행되게 만들어 주는 것이다.
동일하게 @MyFirstGlobalActor가 아니라 그 자리에 @MainActor가 쓰여 있다면 getDataFromGlobal()함수는 실행이 Main actor에서 실행되는 것이다.
그렇게 되면, getDataFromGlobal()함수가 MainActor로 실행됨을 보장하기에, 'self.dataArray = data'는 굳이 'MainActor.run'을 써줄 필요가 없게 되는 것이다. (이미 MainActor로 실행되고 있음)
class GlobalActorViewModel: ObservableObject {
@Published var dataArray: [String] = []
let globalActorManager = MyFirstGlobalActor.shared
@MainActor func getDataFromGlobal() {
Task {
let data = await globalActorManager.getDataFromDataBase()
print("\(Thread.current)에서 진행중")
self.dataArray = data
}
}
}
위와 같은 기능이 있기에, actor와는 확실히 구별되는 GlobalActor의 사용이유라고 할 수 있을 것 같다.
# 추가로!
위에서 dataArray는 그 변화마다 화면에 보여줄 데이터가 달라질 것이기에 MainActor에서 실행시켰는데, 개발을 하다보면 이를 까먹는 경우가 있을 수 있고, 안전하게 해주기 위해서
class GlobalActorViewModel: ObservableObject {
@MainActor @Published var dataArray: [String] = []
let globalActorManager = MyFirstGlobalActor.shared
@MyFirstGlobalActor func getDataFromGlobal() {
Task {
let data = await globalActorManager.getDataFromDataBase()
await MainActor.run(body: {
self.dataArray = data
})
}
}
}
dataArray를 선언할때, @MainActor를 같이 선언해주곤 한다. 이렇게 선언해둔다면, getDataFromGlobal()에서 'self.dataArray = data'를 해줄때 'MainActor.run'을 생략해주게 되면, 에러가 발생하여 더욱 안전하게 코딩을 할 수 있게 된다.


끗!!!!!!
'iOS > Concurrency' 카테고리의 다른 글
AsyncPublisher (구독을 하지 않고 데이터의 변화를 확인) (0) | 2022.12.26 |
---|---|
Sendable 프로토콜에 대해! (0) | 2022.12.20 |
class대신 actor가 요긴한 상황! (0) | 2022.12.19 |
Async Await (0) | 2022.12.03 |