-
Core Data Context 이해와 활용 - 심화Apple🍎/CoreData 2024. 1. 15. 16:54
그냥 Store에 저장하지 뭐하러 Context에다가 임시로 저장을 하냐?
데이터를 저장하거나 변경할 때 Store에 바로 하지 않고 Context에 먼저 임시로 해본 뒤에 save를 호출해야 해당 내용들이 Store에 반영된다.
뭐하러 Context를 Scratch Pad로 사용을 할까? 그냥 바로 Store에다가 꽂아버리면 안되는 이유가 뭘까?- Core Data는 그냥 데이터만 저장하는 게 아니다. 앱의 로직과 맞물려서 돌아가다가 의도한 로직에 맞지 않으면 해당 내용은 Store에 반영하면 안된다. 따라서 앱이 돌아가면서 데이터의 변경이 일어날 때 이것들을 Context에 임시로 저장해 놓았다가 모든 로직에 오류가 없는 것을 확인한 이후에서야 Store에 반영한다.
Multiple Main Contexts
- 하나의 Main Context는 기본으로 있고 필요에 따라 Context를 추가로 만들어서 같은 Store에 연결해서 사용할 수 있다.
- 예를 들어 2개이상의 Context에서 같은 엔티티의 값에 대해 동시에 변경을 하려하면 데이터 충돌이 일어날 수 있다.
- Core Data는 위와 같은 상황에 대해 디폴트로 Optimistic Locking을 채택한다.
Context 동작 방식 Remind
- Context는 SandBox(일정 지역에서 일어난 변화는 해당 지역에만 적용된다.)의 형태로 Store로부터 고립되어 있다.
- 다시말해서 save를 호출하기 전까지는 Context 내부의 변화는 Context 내부에만 한정된다.
Optimistic Locking
- 하나의 문서 파일을 두개의 워드 창에 띄워 놓고 편집을 하는 상황을 가정해보자.
- 이런 상황이면 하나의 엔티티를 여러 곳에서 접근하여 변경할 수 있어 데이터 충돌이 일어날 수 있다. Core Data에서는 이를 어떻게 해결 할까?
- 각 Context는 Persistent Store로부터 엔티티를 fetch해올때 스냅샷을 떠놓는다.
- 유저가 엔티티를 수정하고 이를 저장하기 위해 save를 호출하면
- Context는 변경된 엔티티를 persistent store에 저장하기 전에 먼저
- 한번더 Persistent Store로부터 해당 엔티티를 조회해서 가져와
- 이전에 스냅샷과 비교한다.
- 만약 조회한 엔티티가 이전 스냅샷과 동일한 경우, 엔티티의 변경이 일어나지 않았음을 의미하므로 정상적으로 변경한 엔티티를 저장한다.
- 만약에 변경된 엔티티를 저장하기 전 persistent store로부터 한번더 가져온 엔티티가 이전에 떠 놓은 스냅샷과 다른 경우
- 해당 엔티티를 편집하는 동안 다른 곳에서 이미 해당 엔티티를 변경 시킨 것이다.
- 따라서 이 경우 데이터간의 충돌이 일어날 수 있기 때문에 save를 취소한다.
Context 내부에서의 엔티티 변화를 persistent store에 반영하는 과정을 Merge(병합)이라고 하며 위와 같이 하나의 엔티티에 대해 여러 Context에서 변경을 시도하는 경우 Merge Conflict Error가 발생한다.
Merge Conflict Errors
// 기본 main context 가져오기 let context = delegate.persistentContainer.viewContext // main context 하나 더 만들기 let context2 = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) // 기본 main Context하고 같은 persistent store에 연결하기 context2.persistentStoreCoordinator = delegate.persistentContainer.persistentStoreCoordinator // NSFetchRequest에 Predicate 설정하기 let predicate = NSPredicate(format: "name = %@", "Speak to me") let fetchRequest = NSFetchRequest<Song>(entityName: "Song") fetchRequest.predicate = predicate do { // 각 context에서 똑같은 엔티티 fetch해서 가져오기 let songFromContext1 = try context.fetch(fetchRequest).first! let songFromContext2 = try context2.fetch(fetchRequest).first! // 각 context에서 fetch해온 엔티티의 값 변경하고 songFromContext1.duration = 60.0 songFromContext2.duration = 40.0 // 각 context에서 save 호출해서 변경된 값 persistent store에 반영 시도 try context.save() try context2.save() } catch let error { NSLog((error as NSError).userInfo.debugDescription) }
위와 같이 하나의 엔티티에 대해 서로 다른 context가 동시에 값의 변경을 시도하게 되면 다음과 같이 Merge Conflict Error가 발생한다.
["NSExceptionOmitCallstacks": 1, "conflictList": <__NSArrayM 0x600002850db0>( NSMergeConflict (0x600003356800) for NSManagedObject (0x60000057c640) with objectID '0xfa2faad45a7af888 <x-coredata://E8CEB6C1-4881-497F-92D9-4CC2F1AE64B8/Song/p1>' with oldVersion = 10 and newVersion = 11 and old object snapshot = { album = "<null>"; composer = "<null>"; duration = 40; imageURL = "<null>"; lyrics = "<null>"; name = "Speak to me"; } and new cached row = { album = "<null>"; composer = "<null>"; duration = 60; imageURL = "<null>"; lyrics = "<null>"; name = "Speak to me"; } ) ]
userInfo
를 통해서 엔티티의 스냅샷과 현재 디스크에 저장되어 있는 엔티티가 서로 일치하지 않음을 알 수 있다.Merge Conflict Policy
- 낙관적 락의 경우, 데이터간의 충돌이 발생했을 때 개발자에게 충돌을 보고하고 어떤 버전의 엔티티를 저장할지 선택하게 한다.
- 충돌 때마다 버전을 수동으로 선택하기 보다 어떤 엔티티를 저장할 것인지 미리 지정해놓을 수 있다.
NSManagedContext
에mergePolicy
를 이용해 충돌시 선택할 버전을 지정해 놓을 수 있다.
NSErrorMergePolicy
NSErrorMergePolicy
는 충돌 해결을 위한 정책이다.- 스냅샷과 persistent store의 버전 사이에 충돌이 발생시 디폴트는 error를 던지는 것이다.
mergeByPropertyStoreTrump
: 현재 저장하려는 변경된 엔티티를 우선시한다.- persistent store에 있는 엔티티의 값과 다른 경우, 새로 저장하는 엔티티의 값으로 덮어쓰기 한다.
- 완전한 덮어쓰기가 아니라 충돌하는 값에 대해서만 덮어쓰기
context.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
mergeByPropertyObjectTrump
: persistent store에 저장되어 있는 버전을 우선시한다.- 위의 경우와 반대로 persistent store에 저장된 값을 그대로 유지하는 것을 선택한다.
context.mergePolicy = NSMergePolicy. mergeByPropertyObjectTrump
overwrite
: 현재 저장하려는 변경된 엔티티로 덮어쓰기- persistent store에 있는 엔티티 위에다가 아예 새로운 엔티티로 덮어써버린다.
context.mergePolicy = NSMergePolicy.overwrite
rollback
: 현재 저장하려는 변경사항 모두 취소하기- context에서 이루어진 모든 변경사항 버리기
context.mergePolicy = NSMergePolicy.rollback
Nested Managed Object Contexts
Child Contexts
- 일반적으로 context에서 일어난 변경사항을 반영하기 위해 save를 호출하면 context 내 변경사항들은 persistent store로 push 된다.
- Child Contexts에서 save를 호출하면 Child Contexts에서 일어난 변경사항들은 각자의 Parent context로 push 된다.
- Child Context는 Persistent Store와 직접 연결되어 있지 않으며 모든 변경사항은 Parent context를 통해야한다.
Child Contexts 만들기
- Child Context는 일반 Context와 다르지 않다. -> Context를 생성하는 방법은 동일하다.
- Child Context도 또다른 Contexts들의 Parent가 될 수도 있다.
// 기본 main context를 parent 로 받고 let parent = delegate.persistentContainer.viewContext // 새로운 context를 만든다음 let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) // persistentStoreCoordinator에 연결하는 대신 // parent Context 지정해주기 childContext.parent = parent
Parent와 Child Contexts 싱크 맞추기
- Parent와 Child Contexts들은 양방향으로 변화를 반영할 수 있다.
- Child Context에서
save
: Child Context에서 일어난 변화를 Parent Context에 반영 NSManagedContext
는automaticallyMergesChangesFromParent
속성을 가지고 있어 해당 속성을 true로 설정할 경우 Parent Context에서 일어난 변화가 Child Context들에게 자동으로 반영된다.
// 기본 Main Context를 parent로 지정 let parentContext = delegate.persistentContainer.viewContext // 새로운 Context 2개 만든 다음, automaticallyMergesChangesFromParent 속성 true let childContext1 = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) childContext1.parent = parentContext childContext1.automaticallyMergesChangesFromParent = true let childContext2 = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) childContext2.parent = parentContext childContext2.automaticallyMergesChangesFromParent = true // Parent Context에 새로운 엔티티 생성해서 넣은 뒤에 엔티티 값 변경 let songInMainContext = NSEntityDescription.insertNewObject(forEntityName: "Song", into: parentContext) as! Song songInMainContext.name = "Breathe" // Parent Context의 변경 내용 자동으로 Child Contexts들에 반영되어 // Parent Context에서 새로 생성한 엔티티 Child Contexts들에서도 꺼내 볼 수 있음 let childSong1 = childContext1.object(with: songInMainContext.objectID) as! Song let childSong2 = childContext2.object(with: songInMainContext.objectID) as! Song // Child Context에서 엔티티의 값 변경 후 save하면 // 해당 변경 내용 Parent Context에 반영 childSong1.name = "On the Run" try childContext1.save() // refreshAllObjects 호출해 Parent Context에 변경사항 업데이트해주기 childContext2.refreshAllObjects()
'Apple🍎 > CoreData' 카테고리의 다른 글
Core Data Context 이해와 활용 - 기본 (0) 2024.01.11 데이터 모델 개념을 기초로 엔티티와 클래스 관계 알아보기 (0) 2024.01.09 Core Data 뜯어 보기 (0) 2024.01.04 엔티티와 컨텍스트 (0) 2024.01.02