본문 바로가기
iOS/Concurrency

@MainActor, @globalActor 조금 더 이해하기

by 지금갑시다 2022. 12. 23.

@MainActor, @globalActor 는 결국 actor의 종류이다.

 

우선 actor를 왜 쓸까? 비동기적인 상황에서 concurrency를 보장해주기 위해서 사용하는 것이다.

 

@MainActor의 경우, 진행되는 동작을 Main thread로 보내고,  thread safe하게 동작함을 보장한다.

(DispatchQueue.main의 역할과 비슷)

 

@MainActor와 동일한 이유로, @globalActor 역시, 해당 globalActor를 사용하게 되면 background에서 진행되는 동작들의 concurrency를 보장하고 해당  @globalActor를 전역변수로 만들어 @MainActor를 사용할 때와 동일하게 어떤 상황에서도 사용하기 쉽게 만드는 것이다.

 

 'MainActor.run'이 사용하기 편리한 이유는 static으로 선언되어 있어 전역변수처럼 쉽게 이용이 가능하기 때문이다.

MainActor run은 static 함수이다
MainActor의 사용예시

 

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

실제 동일한 @globalActor로 실행되고 있는 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