-
Notifications
You must be signed in to change notification settings - Fork 1
[기술 논의] DIContainer 사용 시 Actor Isolation 문제 해결하기
UIKit을 사용하면서 actor를 기반으로 한 DIContainer를 구현하던 중, 아래와 같은 에러를 마주했다.
Call to actor-isolated instance method 'register(_:object:)' in a synchronous main actor-isolated context
홀리몰리 쮓
왜 이런 지 알아보도록 하자
actor로 DIContainer를 구현하고, register와 resolve 메서드를 사용하려 했더니 위와 같은 에러가 발생했다. 이는 Actor Isolation과 관련된 문제이다.
- SceneDelegate에서는 문제 없음: @MainActor를 사용하여 메인 스레드에서 DIContainer를 사용할 수 있었다.
- 뷰모델 생성자에서 문제 발생: 뷰모델은 메인 액터의 흐름 안에 있지 않기 때문에, 생성자에서 resolve를 호출할 때 에러가 발생했다.
public final class HomeViewModel: ViewModelType {
private var fetchUserHouseUseCase: FetchUserHouseUseCase
public init() throws {
Task {
self.fetchUserHouseUseCase = try await DIContainer.shared.resolve(FetchUserHouseUseCase.self)
}
throw MHError.DIContainerResolveFailure(key: "FetchUserHouseUseCase")
}
}
위 코드에서 self를 사용하려면 클래스의 1단계 초기화가 끝나야 하는데, Task 안에서 self를 사용하려니 문제가 발생했다. 결국 뷰모델은 메인 액터 흐름이 아니기 때문에 이 방법은 패스했다.
뷰컨트롤러는 @MainActor에서 동작하므로, DIContainer에서 뷰모델을 꺼내올 수 있을 것이라 생각했다. 그러나 뷰컨트롤러의 생성자는 throws를 지원하지 않기 때문에, resolve 실패 시 에러 처리가 어려웠다.
따라서 생성자 내에서 do-catch를 사용해보았다.
public final class HomeViewController: UIViewController {
private let viewModel: HomeViewModel
public init() {
do {
let viewModel = try DIContainer.shared.resolve(HomeViewModel.self)
self.viewModel = viewModel
} catch {
MHLogger.error("HomeViewModel 의존성을 해결할 수 없습니다: \(error)")
}
super.init(nibName: nil, bundle: nil)
}
}
뷰컨트롤러에서 fatalError를 사용하지 않고 에러를 처리할 방법이 마땅치 않았다.
또한 do-catch에 있기 때문에 프로퍼티가 확실히 초기화된다는 보장이 없으므로 패스했다.
결국 뷰모델에서 UseCase를 옵셔널로 선언하고, 필요한 시점에 DIContainer에서 가져오도록 했다. 하지만 이 방법은 의존성이 명확하지 않고, 옵셔널 처리가 번거로웠다.
Swift 6.0에서 도입된 nonisolated를 사용하려 했으나, 현재 프로젝트 환경에 맞지 않아 포기했다.
차라리 이럴 거면 Swift 5 버전 쓰지,, 라는 마인드 ?
actor 내부에서 비동기적으로 resolve를 호출하려 했으나, 생성자에서 await를 사용할 수 없고, 프로토콜에 Sendable 제약이 추가되어 복잡해졌다.
DIContainer를 @MainActor에 종속 시켜버리는 문제점 발생
DIContainer는 싱글톤이고, 여러 곳에서 동시에 접근 가능한 개체여야 하는데 이걸 MainActor에 종속시키면 의미가 없다고 봄
→ 정현: DIContainer는 MainActor와 함께 동작해야 한다, actor로 만들면 viewModel 꺼내와지는게 백그라운드에서 돌아가니까 앱이 전반적으로 느려질 거다
- 뷰모델 내에서 useCase 프로퍼티가 옵셔널 없이 사용될 수 있음
- 뷰모델까지 DIContainer에 넣어줘야 함
vs
뷰모델 내에서 useCase를 사용하는 메소드마다 아래 코드와 같이 사용
Task {
useCase = await DIContainer.shared.resolve(AUseCase.self)
await useCase?.deleteBookCover(id)
}
멘토님께서 컨벤션이라 하셔서 shared에만 @MainActor를 붙이기로 했다..^ㅇ^