Advertisement
  1. Code
  2. Core Data

Core Data 그리고 Swift: 동시 실행(Concurrency)

Scroll to top
Read Time: 9 min
This post is part of a series called Core Data and Swift.
Core Data and Swift: Subclassing NSManagedObject
Core Data and Swift: Batch Updates

() translation by (you can also view the original English article)

만약 여러분이 작거나 간단한 어플을 개발한다면, 여러분은 아마도 background에서 Core Data operation들을 실행하는 이익을 볼 수 없다. 그러나, 만약 여러분이 여러분의 어플이 처음 런치됬을 때 main 쓰레드에서 수백 또는 수천개의 레코드를 불러온다면? 결과는 최악일 것이다. 예제에서, 여러분의 어플은 애플의 watchdog에 의해 죽임 받을 수도 있습니다.

이 기사에서, 우리는  다수의 쓰레드들에서 Core Data를 사용 그리고 우리가 몇몇 까다로운 문제의 해결책 탐험할 때 위험한것들을 보자.

1. 안정적인 쓰레드

Core Data를 가지고 작업할때, Core Data가 스레드에 안정적이지 않다는것을 기억하는것은 중요하다. Core Data는 싱글 스레드에서 실행되어진다는 것을 예상한다. 이것은 모든 Core Data 작동이 메인 쓰레드에서 실행되어져야 한다는 의미가 아니다, 사실 메인 쓰레드는 UIKit을 위한 스레드이다. 그러나 이것은 여러분이 작동들이 어느 쓰레드들에서 실행되어지는지를 염두해서 사용하는 것이 필요하다는 의미이다. 이것은 또한 여러분이 한 쓰레드가 다른 쓰레드들에서 전파되어지는 변경들을 주의할 필요가 있다는 의미이다.

다수 쓰레드들에서 Core Data를 가지고 작업하는것은 실제로 굉장히 이론적으론 간단하다. NSManagedObject, NSManagedObjectContext, 그리고 NSPersistentStoreCoordinator는 쓰레드에 안정적이지가 않다. 이 클래스들의 인스턴스들은 오직 이것들이 생성되어진 쓰레드에서만 접급되어질 수 있다. 여러분은 상상할 수 있는 것과 같이, 이것은 복잡하다.

NSManagedObject

우리는 이미 NSManagedObject가 쓰레드에 안정적이지 않다는 것을 안다, 그러나 어떻게 여러분은 다른 쓰레드로 부터 레코드를 접근 할까? NSManagedObject 인스턴스는 NSManagedObjectID 클래스의 인스턴스를 반환하는 ObjectID 프로퍼티를 가지고 있다. NSManagedObjectID 클래스는 쓰레드에 안정적이고 이 클래스의 인스턴스는 managed object context가 일치하는 managed object를 가져오는데 필요한 모든정보를 포함한다.

1
// Object ID Managed Object

2
let objectID = managedObject.objectID

다음 코드에서, 우리는 managed object context에게 objectID와 일치하는 managed object를 요청한다. objectWithID(_:) 그리고 existingObjectWithID(_:) 메소드는 지역 버전을 리턴한다. 이 로컬은 일치하는 managed object의 현재 쓰레드 이다.

1
// Fetch Managed Object

2
let managedObject = managedObjectContext.objectWithID(objectID)
3
4
// OR

5
6
do {
7
    let managedObject = try managedObjectContext.existingObjectWithID(objectID)
8
} catch {
9
    let fetchError = error as NSError
10
    print("\(fetchError), \(fetchError.userInfo)")
11
}

기본적으로 기억할 규칙은 하나의 쓰레드에서 다른 쓰레드로 부터 NSManagedObject 인스턴스를 보내지 말아야 한다는 것이다. 대신, managed object의 objectID를 보내라 그리고 쓰레드의  레코드의 로컬 버전에 대한 managed object context를 요청하라.

NSManagedObjectContext

NSManagedObjectContext 클래스는 쓰레드에 안정적이지가 않기 때문에, 우리는 Core Data와 상호작용하는 모든 쓰레드에 managed object context를 생성할 수 있다. 이 전략은 종종 쓰레드 감금(thread confinement)이라고 불린다.

일반적인 접근은 쓰레드의 사전에 managed object context를 저장하는 것이다. 어떻게 이것이 작업하는지를 보기 위해서 다음 예를 보자.

1
// Add Object to Thread Dictionary

2
let currentThread = NSThread.currentThread()
3
currentThread.threadDictionary.setObject(managedObjectContext, forKey: "managedObjectContext")

너무 많이 오래전은 아니지만 오래 전에, 애플은 이 방식을 추천했다. 비록 이것은 좋은 작업이지만, 애플이 요즘에 추천하는 더 좋은 옵션이 있다. 우리는 잠시 후에 이 옵션을 볼 것이다.

NSPersistentStoreCoordinator

persistent store coordinator는 어떻게? 당신은 모든 쓰레드에 대한 분리된 persistent store coordinator를 생성할 필요가 있다. 반면 이것은 가능하고 애플의 전략중 추천하는것을 사용하자. 이것은 필수적이진 않다.

NSPersistentStoreCoordinator 클래스는 만약 managed object context들이 다른 쓰레드들에 생성되어진다면, 다수의 managed object context들을 지원하기 위해 설계되어졌다.  NSManagedObjectContext 클래스는 persistent store coordinator를 접근할때 동안, persistent store coordinator를 잠근다. 이것은 만약 managed object context들이 다른 쓰레드에 살고 있다면  다수의 managed object context들이 같은 persistent store coordinator를 사용하게 하는 것을 가능하게 한다.  이것은 다수의 쓰레드로 된 Core Data가 좀 더 많은 관리적이고 복잡성이 적게 만든다.

2. 동시실행(Concurrency) 전략들

지금까지, 만약 여러분이 다수 쓰레드들에서 Core Data 연산들을 수행한다면, 다수 managed object context들이 필요하는 것을 배웠다. 경고, managed object context들은 서로 존재하는 것을 알 수 없다. 하나의 managed object context에 있는 managed object에 만들어진 변경들은 자동적으로 다른 managed object context들에게 전파되어지지 않는다. 이 문제를 어떻게 해결할까?

애플이 추천하는 두 가지 대중적인 전략들이 있다, 알림들(notifications) 그리고 부모-자식(parent-child) managed object context들 각 전략을 보자 그리고 이 전자와 후자를 살펴보자.

우리가 예제에서 이야기할 시나리오는 background에서 작업을 수행하고 operation의 background 쓰레드에 있는 Core Data에 접근하는 NSOperation 서브클래스이다. 이 예제는 여러분에게 다른점들 그리고 각 전략의 장점을 보여줄 것이다.

전략 1: 알림들(Notifications)

이 시리즈에서 전에, 나는 여러분에게 NSFetchedResultsController 클래스를 소개했고 여러분은 managed object context가 3개의 알림종류들을 보내는 것을 배웠다.

  • NSManagedObjectContextObjectsDidChangeNotification:이 알림은 managed object context의 managed object가 변경됬을때, 보내진다.
  • NSManagedObjectContextWillSaveNotification: 이 알림은 managed object context가 저장 연산을 수행하기 전에 보내진다.
  • NSManagedObjectContextDidSaveNotification: 이 알림은 managed object context가 저장 연산을 수행한 후에 보내진다.

managed object context가 managed object context의 변경들을 persistent store에 저장할때, 다른 managed object context들은 persistent store coordinator를 통해 managed object context에 변경들에 관한것을 알고 싶어 한다. 이것은 managed object context에 변경을 다른 managed object context에 삽입 또는 변경을 병합하는 작업은 굉장히 쉽다. 코드로 이야기해보자.

우리는 background에 있는 작업들을 수행하는 동시에 발생하지 않는 operation을 생성하고 Core Data에 접근하는 것이 필요하다. 이것은 NSOperation 클래스의 구현이다.

1
import UIKit
2
import CoreData
3
4
class Operation: NSOperation {
5
6
    let mainManagedObjectContext: NSManagedObjectContext
7
    var privateManagedObjectContext: NSManagedObjectContext!
8
    
9
    init(managedObjectContext: NSManagedObjectContext) {
10
        mainManagedObjectContext = managedObjectContext
11
        
12
        super.init()
13
    }
14
    
15
    override func main() {
16
        // Initialize Managed Object Context

17
        privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
18
        
19
        // Configure Managed Object Context

20
        privateManagedObjectContext.persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator
21
        
22
        // Add Observer

23
        let notificationCenter = NSNotificationCenter.defaultCenter()
24
        notificationCenter.addObserver(self, selector: "managedObjectContextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: privateManagedObjectContext)
25
        
26
        // Do Some Work

27
        // ...

28
        
29
        if privateManagedObjectContext.hasChanges {
30
            do {
31
                try privateManagedObjectContext.save()
32
            } catch {
33
                // Error Handling

34
                // ...

35
            }
36
        }
37
    }
38
39
}

분명하게 말해야할 필요가 있는 자세한 몇몇 중요한 부분이 있다. 우리는 private managed object context를 초기화하고 이것의 persistent store coordinator 프로퍼티에 mainManagedObjectContext 객체를 사용하여 설정했다. 이것은 정말 괜찮다, 우리는 mainManagedObjectContext를 접근할 수 없기 때문에, 우리는 오직 어플의 mainManagedObjectContext가 참조하는 persistent store coordinator에게 요청한다. 우리는 쓰레드 감금(thread confinement) 규칙을 위반하지 않았다.

main() 메소드는 operation이 실행하고 있는 background 쓰레드에서 실행되기 때문에, operation의 main()method에 있는 private managed object context를 초기화하는것은 필수적이다. 우리는 operation의 init(managedObjectContext:) 메소드에 있는 managed object context를 초기화 할 수 없을까? 답은 no 이다. 이 operation의 init(managedObjectContext:)메소드는 Operation 인스턴스가 초기화되어지는 쓰레드에서 실행된다. 아마 이 쓰레드는 대부분 main 쓰레드일 것이다. 이것은 private managed object context의 의도를 무모화 시킬 수 있다.

operation의 main() 메소드에서, 우리는 private managed object context로 부터 보내진NSManagedObjectContextDidSaveNotification 알림의 옵저버인 Operation 인스턴스를 추가했다.

우리는 operation가 생성되는 작업 그리고 private managed object context의 변경들을 저장하는 작업을 했다. 이 private managed object context는 NSManagedObjectContextDidSaveNotification 알림을 발솨할 것이다. managedObjectContextDidSave(_:) 메소드에서 무슨일이 발생되는지 보자.

1
// MARK: -

2
// MARK: Notification Handling

3
func managedObjectContextDidSave(notification: NSNotification) {
4
    dispatch_async(dispatch_get_main_queue()) { () -> Void in
5
        self.mainManagedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
6
    }
7
}

여러분이 볼 수 있는것 과 같이, 이것의 구현은 짧고 간단하다. 우리는 main managed object context에 알림객체를 보내mergeChangesFromContextDidSaveNotification(_:)를 호출했다. 내가 전에 언급했던것 처럼, 알림은 변경, 삽입, 업데이트, 삭제를 포함한다

main 쓰레드에서 이 메소드를 호출하는 것은 중요하다. 왜 우리는 이 호출을 main 쓰레드의 queue에 전파했을까 이것은 쉽고 그리고 명백하게 만들기 위해서, 여러분은 managed object context의 queue에서 이용하는 변경들을 병합하는 것을 보장하기 위해서,  performBlock(_:) 또는 performBlockAndWait(_:)를 사용할 수 있다. 우리는 이 기사에서 나중에 이 메소들에 관해서 이야기할 것이다.

1
// MARK: -

2
// MARK: Notification Handling

3
func managedObjectContextDidSave(notification: NSNotification) {
4
    mainManagedObjectContext.performBlock { () -> Void in
5
        self.mainManagedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
6
    }
7
}

사용할 Operation 클래스를 집어 놓는것은 인스턴스를 초기화하는것 처럼 간단하다. managed object context를 보내고, operation를 operation queue에 추가하면 된다.

1
// Initialize Import Operation

2
let operation = Operation(managedObjectContext: managedObjectContext)
3
4
// Add to Operation Queue

5
operationQueue.addOperation(operation)

전략 2: 부모/자식 Managed Object Context들

iOS 6때 부터, 좀 더 우화하고 더 좋은 전략이 있다. Operation 클래스 그리고 부모/자식 managed object context들을 다시 방문해보자. 부모/자식 managed object context 개념은 간단하지만 강력하다. 나는 이것을 가지고 작업하는 방법을 설명한다.

자식 managed object context는 부모 managed object context에 의존적이다. 자식 managed object context는 persistent store coordinator에 접근할 수 없다. 자식 managed object context가 저장되어질 때마다, 변경들은 parent managed object context에 푸시되어진다. 수작업으로 변경들을 메인 또는 부모 managed object context에 병합시키기 위해서, 알림들을 사용할 필요가 없다.

또 다른 이익은 성능이다. 자식 managed object context는 persistent store coordinator에 접근할 수 없기 때문에, 자식 managed object context가 저장될때, 변경들은 persistent store coordinator에 푸시되어지지 않는다. 대신에, 변경들은 부모 managed object context에 푸시되어진다, 이것을 만드는 것은 더럽다. 이 변경들은 자동적으로 persistent store coordinator에 전달되어지지 않는다.

Using ParentChild Managed Object ContextsUsing ParentChild Managed Object ContextsUsing ParentChild Managed Object Contexts

Managed object context는 중첩될 수 있다. 자식 managed object context는 또 다른 자식 managed object context를 가질 수 있다. 같은 규칙들을 적용하자. 그러나, 부모 managed object context에 푸시되어진 변경들은 밑에 있는 자식 managed object context들에는 푸시되어지지 않는다는 것을 기억하는것은 중요하다. 만약 자식 A가 이것의 변경을 이것의 부모에 푸시한다면, 자식 B는 이 변경들을 알 수 없다.

자식 managed object context를 생성하는 것은 지금까지 우리가 본 것으로 부터 약간 어렵다. 우리는 init(concurrencyType:) 호출하는 것으로 부터 자식 managed object context를 초기화 했다. 동시실행(concurrency) 초기자가 받아들이는 종류는 managed object context의 쓰레드 모델을 정의한다. 각 동시실행 종류를 보자.

  • MainQueueConcurrencyType: managed object context는 오직 main 쓰레드로 부터 접근할 수 있다. 만약 여러분이 다른 쓰레드에서 managed object context를 접근한다면, 예외가 던져질 것이다.
  • PrivateQueueConcurrencyType: PrivateQueueConcurrencyType의 동시실행 종류를 가지고 managed object context를 생성할 때, managed object context는 private queue와 연관되어진다. 그리고 이것은 private queue로 부터 오직 접근되어질 수 있다.
  • ConfinementConcurrencyType: 이것은 우리가 이전에 배웠던 쓰레드 감금(thread confinement) 개념와 일치하는 동시실행 종류이다. 만약 init()을 사용하여 여러분이 managed object context를 생성한다면, managed object context의 동시실행 종류는 ConfinementConcurrencyType이다. 애플은 이 동시실행  종류를 iOS9에서 없앴다. 이것은 또한 init()은 iOS9에서 없애졌다는것을 의미한다.

애플이 부모/자식 managed object context들을 도입했을 때, Core Data framework에 추가되어진 두가지 중요한 메소드 performBlock(_:) 그리고 performBlockAndWiat(_:) 가 있다. 이 두 메소드는 여러분의 삶을 더 쉽게 만들어 줄 것입니다. 여러분이 managed object context에 있는 performBlock(_:)를 호출하고 실행할 코드의 블록을 전달할때, Core Data는 알맞은 쓰레드에서 블록이 실행되는지 확인합니다. PrivateQueueConcurrencyType 동시실행 타입에서, 이것은 managed object context의 private queue에서 블록이 실행되어지는것을 의미합니다.

performBlock(_:) 그리고 performBlockAndWait(_:) 사이 다른점은 간단하다. performBlock(_:) 메소드는 현재 쓰레드에서 블록되지 않는다. 이것은 블록을 받아드린다, 적절한 queue에서 실행을 위해 계획을 짜고 다음 문장의 실행을 가지고 계속한다.

performBlockAndWait(_:) 메소드는 블로킹 된다. performBlockAndWait(_:)가 불려진것으로 부터 쓰레드는 다음 문장을 실행하기 전에 끝내기 위해 메소드에 보낸 블록을 위해 기달린다. 중첩된 장점은 실행된 performBlockAndWait(_:)에 호출하는것이다.

이 기사 끝에, 나는 부모/자식 managed object context들을 이용하는 Operation 클래스를 리팩토링하는 것을 원합니다. 여러분은 빠르게 이것이 굉장히 우리가 생성한 NSOperation 서브 클래스가 간단해졌다는 것을 알 수 있습니다. main() 메소드는 약간 좋게 변경되었다. 밑에서 이것의 변경된 구현을 보자.

1
override func main() {
2
    // Initialize Managed Object Context

3
    privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
4
    
5
    // Configure Managed Object Context

6
    privateManagedObjectContext.parentContext = mainManagedObjectContext
7
    
8
    // Do Some Work

9
    // ...

10
    
11
    if privateManagedObjectContext.hasChanges {
12
        do {
13
            try privateManagedObjectContext.save()
14
        } catch {
15
            // Error Handling

16
            // ...

17
        }
18
    }
19
}

이게 다이다. main managed object context는 private managed object context의 부모이다. 우리는 private managed object context의 persistentStoreCoordinator 프로퍼티를 설정하지 않았다는것 그리고 우리는 NSManagedObjectContextDidSaveNotification 알림을 위해 operation을 옵저버로 추가하지 않았다는 것을 알 수 있다. private managed object context가 저장되어질때, 변경들은 자동적으로 이것의 부모 managed object context에 푸시되어진다. Core Data는 적절한 쓰레드에 이것이 발생하도록 보장한다. persistent store coordinator에게 변경들을 푸시하는 것은 main object context(=부모 managed object context)에 달려있다.

결론

동시실행은 이해하거나 구현하는 것은 쉽지않다, 그러나 이것은 여러분이 Core Data operation들을 background 쓰레드에서 수행할 필요한 상황에서는 피할 수 없다.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.