[Swift] SwiftData Concurrency 사용하기

    0. 서론

    SwiftData를 비동기 처리하려고 보면
    CoreData의 await context.perform 처럼 ModelContext를 비동기처리해주는 함수가 없는 것을 확인할 수 있다.

     

    당연한 이야기 이지만 async 키워드를 붙이고 Task 블록 내에서 호출해도 메인 스레드에서 동작한다.

     

    그럼 어떻게 하면 비동기 컨텍스트에서 실행시킬 수 있을까?

     

    1.  ModelActor와 Actor

    방법을 찾는 도중 @ModelActor라는 키워드를 발견했다. ModelActor가 그래서 뭔고?

    SwiftData의 ModelContext를 비동기/동시성 환경에서 안전하게 다루기 위해 설계된 구조로,
    데이터의 격리와 thread-safe한 처리를 자동으로 보장해주는 매크로

     

    아하! ModelActor 매크로를 붙이면 백그라운드 스레드로 돌릴 수 있겠구나!

     

     

    but, 이 친구를 사용하기 위해서는,,,, Actor와 같이 사용해야 한다.

    그래서 Actor란?

    Concurrency에서 Data Race를 방지해주는 타입, actor를 사용하면 여러 스레드에서 안전하게 데이터를 공유 가능.
    • Swift 5.5에서 Swift Concurrency가 도입되면서 같이 나온 유형(class, struct, enum과 같은)
    • Class 처럼 참조 타입이지만, 서브 클래싱은 지원X
    • 공유자원을 격리(isolate) 시키고, 공유자원에 대해서 Serial 하게 접근하도록 처리.
    • 외부에서 접근할때는 await가 필요함

    사용법은 어렵지 않았다. class 대신 actor로 바꿈 된다.

    또한, @ModelActor를 붙이면 actor 내에서 명시적으로 modelContext를 초기화 시켜주지 않아도 된다.
    (외부에서 넘겨주긴 해야됨ㅎ)

    @ModelActor
    actor ModelActorExample {
        
        func create() {
            // code
        }
        
        func fetch() -> [] {
            // code
        }
    }

     

     

    2. 프로젝트 예시

    간단하게 ModelActor를 활용해볼 수 있는 프로젝트를 만들어보자.

    @ModelActor
    actor ModelActorExample {
        
        func createNote(content: String) {
            let note = Note(content: content)
            modelContext.insert(note)
            try? modelContext.save()
        }
        
        func fetchNotes() -> [Note] {
            let sortDescriptor = SortDescriptor(\Note.createdAt, order: .reverse)
            let descriptor = FetchDescriptor<Note>(sortBy: [sortDescriptor])
            
            guard let data = try? modelContext.fetch(descriptor) else { return [] }
            return data
        }
    }

     

    뷰는 간단하게 아래처럼 짜 보았다.

    안녕하세용?

     

     

    App에서 초기화 시에 ModelActoExample 인스턴스를 생성해주고 뷰에 주입을 했다.

    @main
    struct ModelActorPracticeApp: App {
        
        private let modelActor: ModelActorExample
        
        init() {
            let storage = SwiftDataStorage()
            self.modelActor = ModelActorExample(modelContainer: storage.modelContainer)
        }
        
        var body: some Scene {
            WindowGroup {
                ContentView(modelActor: modelActor)
            }
        }
    }

     

     

    뷰에서는 다음과 같이 처리해주었다.

    struct ContentView: View {
        
        @State private var notes: [Note] = []
        @State private var text: String = ""
        
        private let modelActor: ModelActorExample
        
        init(modelActor: ModelActorExample) {
            self.modelActor = modelActor
        }
        
        var body: some View {
            VStack {
                
                // MARK: - TextField & Button
                HStack {
                    TextField("", text: $text)
                        .textFieldStyle(.roundedBorder)
                    
                    Button {
                        Task {
                            await modelActor.createNote(content: text)
                            await MainActor.run { self.text = "" }
                            
                            await updateList()
                        }
                    } label: {
                        Text("저장")
                    }.buttonStyle(.borderedProminent)
                }.padding(.horizontal, 24)
                
                // MARK: - List
                List {
                    ForEach(notes) {
                        Text($0.content)
                    }
                }
            }
            .task {
                await updateList()
            }
        }
        
        private func updateList() async {
            let datas = await modelActor.fetchNotes()
            await MainActor.run { self.notes = datas }
        }
    }

     

     

     

    이렇게 완성하고 breakpoint를 찍어봤는데....

    녜...? 왜 1번쓰레드에서 동작하쥐

     

     

     

     

    3.  TroubleShooting... 해결!

    그래서 찾아보다가 레딧에서 나와 같은 고민을 하는 글을 발견했다.
    https://www.reddit.com/r/SwiftUI/comments/1fhrsn9/what_is_a_good_way_to_create_a_modelactor_ready/

     

    From the SwiftUI community on Reddit: What is a good way to create a ModelActor ready for background use?

    Explore this post and more from the SwiftUI community

    www.reddit.com

     

    But according to this video https://www.youtube.com/watch?v=VG4oCnQ0bfw it seems the ModelActor itself needs to be created in the background off the main thread.

    저 유튜브를 들어가서 보니 해당 @ModelActor를 비동기 컨텍스트 내에서 생성이 되어야한다는 내용이더라....

     

    그래서 나는 ModelActorProvider Class를 따로 만들어서 해결하였다.

    @Observable
    final class ModelActorProvider {
        
        var modelActor: ModelActorExample? = nil
        
        func createModelActor() async {
            let storage = SwiftDataStorage()
            let modelActor = ModelActorExample(modelContainer: storage.modelContainer)
            await MainActor.run { self.modelActor = modelActor }
        }
    }

     

     

    이렇게 만들어주고, App 에서 ModelActorExample이 생성되면 흰화면에서 ContentView로 바뀌게 했다.

    @main
    struct ModelActorPracticeApp: App {
        
        @State private var modelActorProvider = ModelActorProvider()
        
        var body: some Scene {
            WindowGroup {
                if let modelActor = modelActorProvider.modelActor {
                    ContentView(modelActor: modelActor)
                } else {
                    Color.white.ignoresSafeArea()
                        .task {
                            await modelActorProvider.createModelActor()
                        }
                }
            }
        }
    }

     

     

    결과는..!

    다른 스레드에서 잘 동작하는 것을 확인 할 수 있었다.

    해~~결!

     

    해당하는 프로젝트를 깃헙에도 올렸다.

     

     

    GitHub - Monfi98/ModelActor-Practice

    Contribute to Monfi98/ModelActor-Practice development by creating an account on GitHub.

    github.com

     

     

     

    4. 참고

     

    [iOS] Actor 알아보기

    Actor를 사용하면 여러 스레드에서 안전하게 데이터를 공유하고, 동시에 실행되는 코드 블록에서 Data race가 발생하지 않도록 보장할 수 있습니다.

    velog.io

     

    댓글