하루를살자

[iOS] CoreData 를 배워보자 - 5 [CoreData Concurrency] 본문

iOS

[iOS] CoreData 를 배워보자 - 5 [CoreData Concurrency]

Kai1996 2022. 10. 28. 00:45

목차 

1.0 Motivation: persistent container 의 viewContext 는 어떤 Thread 에서 동작하나? 

  1.1 Managed Object Context Concurrency Types

2.0 Heavy Duty Task Strategy

    2.1 performBackgorundTask(_:) via Persistent Container

    2.2 newbackgroundContext()

    2.3 Child context 

3.0 Performance Test

1.0 Motivation:  persistent container 의 viewContext 는 어떤 Thread 에서 동작하나? 

- 기본적으로 생성된 Persistent container 의 viewContext(NSObjectManagedContext) 는 mainQueueConcurrencyType(main queue) 에서 동작합니다.

- 따라서 만약 어떠한 Task 에 관련된 데이터가 많거나 클경우 UI 에 직접적인 영향을 주어 사용자가 앱을 사용하는데 불편함을 겪을수 있습니다. 

- 보통 이런문제는 GCD 를 이용하여 backgroundQueue 에 오래걸리는 작업을 수행하는데, CoreData 는 Thread-Safe 하지 않기 때문에 그냥 backgroundQueue 에 Task 를 수행시킬수 없습니다. 

 

1.1 Managed Object Context Concurrency Types

💭 그럼 어떻게 background task 를 수행시킵니까? 

 👉 CoreData 는 NSManagedObjectContext 를 생성할때 어떤 Queue 에서 동작할건지 미리 정해줄수 있도록 생성자에 명시할수 있습니다. 

- 크게 2가지로 분류되는데, 앞서 설명드린 mainQueueConcurrencyType privateQueueConcurrencyType 가 있습니다. 

- mainQueue = main queue 에서 작업을 수행.

- privateQueue = background queue 에서 작업을 수행

- 이렇게 생성된 Context 들은 2가지 다른 메소드(perform(_:), performAndWait(_:)) 를 통해 원하는 작업을 context 에 해당하는 Thread 에서 진행 시킬수 있습니다.

- 이 둘 메소드의 차이점은 클로저에 선언된 명령어를 어떤식으로 queue 가 처리하는지에 있습니다. 

- perform(_:) = 메소드 호출이후 바로 다음 흐름으로 넘어갑니다. Thread 가 호출이 끝날때까지 기다리지 않아도 됩니다. (비동기)

- performAndWait(_:) = 메소드 호출 이후 끝날때까지 기다립니다.  (동기)

 

2.0 Heavy Duty Task Strategy

- mainQueueConcurrencyType 을 사용하는 대표적인 예는 디폴트로 생성되는  Persistent container 의 viewContext 가 있습니다. 

- 앞서 설명했듯이 많은양의 데이터를 다루는 Task 를 여기서 진행하게된다면 UI 를 Block 하기 때문에 concurrent queue 에서 이러한 작업을 진행시켜주어야합니다. 

- 이포스트에선 아래 3가지 방법으로 Heavy lifting 작업을 수행하는 법을 알아보겠습니다. 

 

2.1 performBackgorundTask(_:) via Persistent Container

- 만약 현재 persistent container 가 존재한다면, 간단히 performBackgroundTask 를 사용해서 background queue 을 통해 작업을 처리할수 있습니다. 해당 메소드는 클로저를 통해서 context 를 넘겨주는데 이는 현재 persistent store 에 연결되어있는 private queueConcurrency Type 입니다. 

- 해당 context 를 이용해서 변경된 내용을 저장하면 mainContext 에 저장된 내용이 업데이트 되어집니다. 

- 변경사항이 mainContext 에 적용된이후, disk 에 저장하고 싶다면 따로 viewContext.save() 를 호출하여 저장해야합니다. 

func saveContext() {
	persistentContainer.performBackgroundTask { context in 
    	do{
            try context.save()
            //만약 변경된 내용을 disk 에 저장하고 싶다면 
            try persistentContainer.performAndwait {
            	try persistentContainer.viewContext.save()
            }
        }catch {
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")        
        }
    } 
}

 

2.2 newbackgroundContext() 

- 이 메소드 는 존재하는 persistent container 를 사용하여 같은 persistentStoreCoordinator 를 가르키는 새로운 context 를 생성 시킵니다. 

- 이때 생성되는 context 를 backgroundManagedObjectContext 라고 불른다고 치고, parent context 를 살펴보면 nil 이 찍히는것을 알수 있습니다. (viewContext 의 parent 또한 nil)  

2개의 다른 Context 가 하나의 PersistentStore Coordinator 를 가르키고있다.

 

- 아래와 같이 perform 메소드를 사용해서 비동기적으로 데이터를 private queue 에서 처리할수 있습니다. 

//create new background context 
var backgroundContext = persistentContainer.newbackgroundcontext() 

backgroundContext.perform { 
    do {
        try backgroundManagedObejctContext.save()
        //변경된 사항을 disk 에 저장하고 싶다면
        try persistentContainer.viewContext.performAndWait {
          try persistentContainer.viewContext.save()
        }
      } catch {
        let nserror = error as NSError
        fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
      } 
}

 

 2.3 Child context 

- 또하나의 방법은 하나의 context 에 private queue 로되어 있는 parent 혹은 child 를 만들어서 작업을 대신 처리하게 하는 것 입니다.

- 이방법은 persistentContainer 를 사용하는지, 아니면 커스텀한 coredata Stack 을 사용하느냐에 따라서 parent 가 heavy lifting 작업을 수행할수도, child context 가 작업을 처리할수도 있게 설계할수 있습니다. (각각의 상황에 맞추어서 설계하는게 중요!) 

 

- 예제는 persistentContainer 를 사용해서 childContext 를 priavte queue type 으로 생성해 주겠습니다. 

- 새로운 context 를 viewcontext 의 parent 가 아닌 childContext 로 설정한이유는 child context 는 parent context 의 persistentStore Coordinator 를 사용하게 되는데, persistentContainer 의 viewContext 는 readOnly 이기 때문에 새로운 persistentStore Coordinator 를 지정할수없게 됩니다. (만약 강제로 하게 된다면 아래와 같은 에러를 받게됩니다) 

let saveManagedObejctContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
persistentContainer.viewContext.parent = saveManagedObejctContext

//Error! 
//*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
//reason: 'Context already has a coordinator;  cannot replace.'

- 아래 코드는 새로운 context 를 private Queue 타입으로 만들고, persistentContainer 를 parent 로 지정하여 데이터를 저장하는 로직입니다. 

lazy var backgroundManagedObejctContext: NSManagedObjectContext = {
    let saveManagedObejctContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    saveManagedObejctContext.parent = persistentContainer.viewContext
    saveManagedObejctContext.automaticallyMergesChangesFromParent = true //Parent context 에서 바뀐 상태를 자동으로 머지함.
    return saveManagedObejctContext
 }()
 
 backgroundManagedObejctContext.perform {
      do {
        try backgroundManagedObejctContext.save()
        try persistentContainer.viewContext.performAndWait {
          try persistentContainer.viewContext.save()
        }
      } catch {
        let nserror = error as NSError
        fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
      }
}

 

3.0 Performance Test

- 아래 예제는 이미지를 선택하고 뒤로 가기를 누를때 CoreData 의 Save() 를 호출합니다. 이때 Save() 함수가 걸리는 시간을 main queue 를 사용했을때와 private queue 를 사용했을때의 차이를 측정해 보겠습니다.

- MainQueue

- PrivateQueue 

 

- private queue 를 사용해서 저장하는데 거의 100배 빠른 응답을 주었습니다. 아무리 ms 가 걸린다고 해도 데이터 양이 방대해지면 private queue 와 main queue 의 사용 용도를 분명히 나누어서 프로젝트를 진행해야겠습니다.

 

 

참조 

https://www.kodeco.com/7586-multiple-managed-object-contexts-with-core-data-tutorial

https://ali-akhtar.medium.com/mastering-in-coredata-part-14-multithreading-concurrency-strategy-parent-child-context-305d986f1ac3

https://cocoacasts.com/more-core-data-and-concurrency

https://stackoverflow.com/questions/40892147/update-nsfetchedresultscontroller-using-performbackgroundtask

Comments