현재 Clean Architecture + MVI(Presentation Layer) 패턴을 적용하여 코드를 작성하던 중, Container 역할을 수행하는 Store의 스레드 안전성에 대하여 생각해볼 여지가 생겼다.
protocol Store: AnyObject, ObservableObject {
associatedtype Intent
associatedtype Action
associatedtype State
var state: State { get set }
func action(intent: Intent) -> AsyncStream<Action>
func reduce(state: State, action: Action) -> State
}
extension Store {
@MainActor
func send(intent: Intent) async {
let actions = action(intent: intent)
for await action in actions {
state = reduce(state: state, action: action)
}
}
}
@MainActor로 고정해도 되는지 고민Store의 역할: 상태 변경을 안전하게 메인 스레드에서 일관되게 수행하는 것
-> 네트워크/비동기 IO는 @MainActor 내부에서 실행해도 안전하다.
단, CPU-heavy/동기 루프/큰 파싱/대규모 컬렉션 변환과 같은 작업들은 반드시 도메인 계층 또는 백그라운드 Task로 옮겨야 한다.
의도
예시 코드
@MainActor
final class ExampleStore: Store {
private var task: Task<Void, Never>?
func action(intent: Intent) -> AsyncStream<Action> {
AsyncStream { continuation in
task?.cancel() // 🔹 기존 작업 취소
let newTask = Task {
do {
let model = try await useCase.execute()
continuation.yield(.didLoad(model))
} catch is CancellationError {
// 취소는 조용히 무시
} catch {
continuation.yield(.failed(error))
}
continuation.finish()
}
task = newTask
continuation.onTermination = { _ in
newTask.cancel()
}
}
}
deinit {
task?.cancel()
}
}
@Published로 쓰고 있음.Publishing changes from background threads is not allowed …