Skip to content

[기술 논의] DIContainer 사용 시 Actor Isolation 문제 해결하기

00me edited this page Nov 28, 2024 · 1 revision

문제 상황

UIKit을 사용하면서 actor를 기반으로 한 DIContainer를 구현하던 중, 아래와 같은 에러를 마주했다.

Call to actor-isolated instance method 'register(_:object:)' in a synchronous main actor-isolated context

홀리몰리 쮓

왜 이런 지 알아보도록 하자


문제 해결

시도한 방법들

actor로 DIContainer를 구현하고, register와 resolve 메서드를 사용하려 했더니 위와 같은 에러가 발생했다. 이는 Actor Isolation과 관련된 문제이다.

1. final class @MainActor 적용하기

  • 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를 사용하려니 문제가 발생했다. 결국 뷰모델은 메인 액터 흐름이 아니기 때문에 이 방법은 패스했다.

2. 뷰모델을 DIContainer에 저장하고, 뷰컨트롤러에서 꺼내오기

뷰컨트롤러는 @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에 있기 때문에 프로퍼티가 확실히 초기화된다는 보장이 없으므로 패스했다.

3. 뷰모델에서 UseCase 프로퍼티를 옵셔널로 사용하기

결국 뷰모델에서 UseCase를 옵셔널로 선언하고, 필요한 시점에 DIContainer에서 가져오도록 했다. 하지만 이 방법은 의존성이 명확하지 않고, 옵셔널 처리가 번거로웠다.

4. nonisolated(unsafe) 사용하기

Swift 6.0에서 도입된 nonisolated를 사용하려 했으나, 현재 프로젝트 환경에 맞지 않아 포기했다.

차라리 이럴 거면 Swift 5 버전 쓰지,, 라는 마인드 ?

5. actor Taskawait 사용하기

actor 내부에서 비동기적으로 resolve를 호출하려 했으나, 생성자에서 await를 사용할 수 없고, 프로토콜에 Sendable 제약이 추가되어 복잡해졌다.

정리

final class + @MainActor로 사용 && 뷰컨 viewDidLoad에서 뷰모델 Resolve

DIContainer를 @MainActor에 종속 시켜버리는 문제점 발생

DIContainer는 싱글톤이고, 여러 곳에서 동시에 접근 가능한 개체여야 하는데 이걸 MainActor에 종속시키면 의미가 없다고 봄

→ 정현: DIContainer는 MainActor와 함께 동작해야 한다, actor로 만들면 viewModel 꺼내와지는게 백그라운드에서 돌아가니까 앱이 전반적으로 느려질 거다

장점

  • 뷰모델 내에서 useCase 프로퍼티가 옵셔널 없이 사용될 수 있음

단점

  • 뷰모델까지 DIContainer에 넣어줘야 함

vs

actor + 뷰모델의 useCase 프로퍼티 옵셔널로 하고 useCase 사용마다 Resolve

뷰모델 내에서 useCase를 사용하는 메소드마다 아래 코드와 같이 사용

Task {
  useCase = await DIContainer.shared.resolve(AUseCase.self)
  await useCase?.deleteBookCover(id)
}

결론

멘토님께서 컨벤션이라 하셔서 shared에만 @MainActor를 붙이기로 했다..^ㅇ^

Clone this wiki locally