Context Synchronization
Context는 각자가 개별적으로 동작하므로,
동일한 Entity를 복수의 Context가 각자 처리하는 경우 데이터 무결성의 문제가 생긴다.
이러한 문제는 실제 값을 변경하게 되는 업데이트와 저장에서 더욱 두드러진다.
따라서 어떤 데이터를 적용할 건지, 옳은 데이터인지 판단할 Merge 정책이 필요하다.
Main Context와 Background Context에서 같은 Entity의 같은 데이터에 접근한 뒤,
Fetch, Update, Save를 진행해 차이를 확인하고, 동기화를 구현한다.
SyncAndMergeViewController>fetchIntoMainContext()
@IBAction func fetchIntoMainContext(_ sender: Any) {
mainContext.perform {
self.employeeInMainContext = DataManager.shared.fetchEmployee(in: self.mainContext)
self.nameLabel.text = self.employeeInMainContext?.name
self.mainContextSalaryLabel.text = self.formatter.string(for: self.employeeInMainContext?.salary)
}
}
main Context에 Employee Entity를 fetch 하고, 이를 UI에 반영한다.
일련의 동작이 main Context가 위치하는 main Thread에서 동작할 수 있도록 perform 메서드를 사용하고,
main Context가 존재하는 main Thread는 UI를 업데이트할 수 있는 thread이므로 직접 접근해 사용할 수 있다.
SyncAndMergeViewController>fetchIntoBackgroundContext()
@IBAction func fetchIntoBackgroundContext(_ sender: Any) {
backgroundContext.perform {
self.employeeInBackgroundContext = DataManager.shared.fetchEmployee(in: self.backgroundContext)
let salary = self.employeeInBackgroundContext?.salary?.decimalValue
DispatchQueue.main.async {
self.nameLabel.text = self.employeeInBackgroundContext?.name
self.backgroundContextSalaryLabel.text = self.formatter.string(for: salary)
}
}
}
backgroundContext는 backgroundThread에서 진행되고,
UI 업데이트는 mainThread에서 진행되어야 한다.
따라서 DispatchQueue로 UI 업데이트 구현을 mainThread로 지정해야 한다.
또한, Context를 포함하고 있는 Thread 간에는 작업을 위해 값을 전달할 수 없다.
따라서 backgroundThread에서 값을 변수에 저장한 뒤, 이를 전달하는 방식으로 구현한다.
SyncAndMergeViewController>updateInMainContext(), updateInBackgroundContext()
@IBAction func updateInMainContext(_ sender: Any) {
mainContext.perform {
let newSalary = NSDecimalNumber(integerLiteral: Int.random(in: 30...90) * 1000)
self.employeeInMainContext?.salary = newSalary
self.mainContextSalaryLabel.text = self.formatter.string(for: newSalary)
}
}
@IBAction func updateInBackgroundContext(_ sender: Any) {
backgroundContext.perform {
let newSalaray = NSDecimalNumber(integerLiteral: Int.random(in: 30...90) * 1000)
self.employeeInBackgroundContext?.salary = newSalaray
DispatchQueue.main.async {
self.backgroundContextSalaryLabel.text = self.formatter.string(for: newSalaray)
}
}
}
Context와 thread에 위치에 따른 특성에 주의하며 무작위 값으로 Salary를 업데이트하고,
이를 UI에 반영한다.
결과
처음에 각각 Fetch를 진행했을 때는 동일한 Salary를 출력하지만,
각각 Update를 진행하게 되면 이제부터는 모호함이 발생한다.
MainContext와 BackgroundContext 두 값 중 어느 값이 맞는 값인지 판단할 기준이 필요하게 되는 것이다.
따라서 어느 한쪽을 저장하고, 다른 쪽을 저장하려고 시도하면 동작하지 않게 된다.
CoreData는 저장소에서 데이터를 Load 할 때마다 Sanpshot을 남기고,
Context 저장 시 update 된 객체의 Snapshot과 저장소의 값을 비교해 다른 부분이 생기는 경우 Merge 정책에 따라 판단하게 되는데
이를 Optimistic Locking이라고 한다.
Merge 정책
- NSErrorMergePolicy
기본 값으로 충돌 시 오류를 발생하고, 저장을 진행하지 않는다. - NSMergeByPropertyStoreTrumpMergePolicy
저장할 Attribute와 Snapshot이 충돌할 경우 저장소의 현재 값을 저장한다. - NSMergeByObjectStroreTrumpMergePolicy
저장할 Attribute와 Snapshot이 충돌할 경우 Attribute의 값을 저장한다. - NSOverwriteMergePolicy
충돌을 무시한 채 현재 값을 저장한다. - NSRollbackMergePolicy
업데이트된 값을 버리고 저장소의 값으로 다시 대체한다.
SyncAndMergeViewController.swift > viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
mainContext.mergePolicy = NSOverwriteMergePolicy
backgroundContext.mergePolicy = NSOverwriteMergePolicy
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil, queue: OperationQueue.main) { (noti) in
guard let userInfo = noti.userInfo else { return }
guard let changedContext = noti.object as? NSManagedObjectContext else { return }
print("===========================")
}
}
기본값인 NSErrorMergePolicy에서 NSOverwriteMergePolicy로 Context들의 정책을 변경한다.
결과
마지막에 저장된 Background Context의 저장 결과가 최종 결과로 사용된다.
Context 간 동기화
mainContext에서 변경되는 값에 대해 backgroundContext가 동기화할 수 있도록 구현한다.
Context는 자신이 관리하는 객체가 변경될 때마다 Notification을 발생한다.
SyncAndMergeViewController.swift > viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
mainContext.mergePolicy = NSOverwriteMergePolicy
backgroundContext.mergePolicy = NSOverwriteMergePolicy
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil, queue: OperationQueue.main) { (noti) in
guard let userInfo = noti.userInfo else { return }
guard let changedContext = noti.object as? NSManagedObjectContext else { return }
print("===========================")
}
}
따라서 이를 감지하는 Observer를 구현해 객체가 변경 될 때 마다
다른 Context에서 이를 반영할 수 있도록 구현할 수 있다.
객체가 어떻게 변화했는지는 전달된 notification의 UserInfo 속성으로 구분하고,
어떤 내용이 변화했는지는 notification의 object 속성에 저장돼있다.
따라서 이 둘을 바인딩해 사용한다.
SyncAndMergeViewController.swift > viewDidLoad()
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil, queue: OperationQueue.main) { (noti) in
guard let userInfo = noti.userInfo else { return }
guard let changedContext = noti.object as? NSManagedObjectContext else { return }
print("===========================")
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 {
}
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 {
}
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 {
}
}
상태에 대해서는 보통 위와 같은 패턴으로 구현한다.
NSInsertedObjectsKey, NSDeletedObjectsKey, NSUpdatedObjectsKey가 각각 존재하는지 확인하고,
이들이 0개 이상인 경우가 실제 값이 변화했음을 의미하기 때문이다.
SyncAndMergeViewController.swift > viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
mainContext.mergePolicy = NSOverwriteMergePolicy
backgroundContext.mergePolicy = NSOverwriteMergePolicy
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil, queue: OperationQueue.main) { (noti) in
guard let userInfo = noti.userInfo else { return }
guard let changedContext = noti.object as? NSManagedObjectContext else { return }
print("===========================")
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 {
guard changedContext != self.backgroundContext else {
return
}
}
}
}
update 된 Context가 backgroundContext라면 작업을 진행하지 않는다.
이렇게 Context를 구분해 대상을 지정해 주지 않으면
- mainContext가 update를 실행
- 이에 반응해 backgroundContext가 update
- 이에 다시 반응해 mainContext가 update
- 반복
의 루프로 빠지게 된다.
SyncAndMergeViewController.swift > viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
mainContext.mergePolicy = NSOverwriteMergePolicy
backgroundContext.mergePolicy = NSOverwriteMergePolicy
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil, queue: OperationQueue.main) { (noti) in
guard let userInfo = noti.userInfo else { return }
guard let changedContext = noti.object as? NSManagedObjectContext else { return }
print("===========================")
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 {
guard changedContext != self.backgroundContext else {
return
}
for update in updates {
self.backgroundContext.perform {
for (key, value) in update.changedValues() {
self.employeeInBackgroundContext?.setValue(value, forKey: key)
}
}
}
}
}
}
변경사항을 열거해 변경된 부분의 key와 value를 각각 바인딩한다.
이때 mainContext의 내용을 사용해 backgroundContext를 업데이트하는 것이 목표이므로,
backgroundContext에서 perform을 사용한다.
set으로 저장된 update의 changeValues() 메서드는 변경된 attribute와 값을 dictionary 형태로 반환한다.
따라서 이들의 key와 value를 바인딩해 backgroundContext의 값을 변경한다.
SyncAndMergeViewController.swift > viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
mainContext.mergePolicy = NSOverwriteMergePolicy
backgroundContext.mergePolicy = NSOverwriteMergePolicy
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: nil, queue: OperationQueue.main) { (noti) in
guard let userInfo = noti.userInfo else { return }
guard let changedContext = noti.object as? NSManagedObjectContext else { return }
print("===========================")
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 {
guard changedContext != self.backgroundContext else {
return
}
for update in updates {
self.backgroundContext.perform {
for (key, value) in update.changedValues() {
self.employeeInBackgroundContext?.setValue(value, forKey: key)
}
let salary = self.employeeInBackgroundContext?.salary?.decimalValue
DispatchQueue.main.async {
self.backgroundContextSalaryLabel.text = self.formatter.string(for: salary)
}
}
}
}
}
}
변경된 Context를 사용해 다시 UI를 업데이트한다.
결과
MainContext가 업데이트됨에 따라 BackgroundContext가 이를 자동으로 반영한다.
refresh
SyncAndMergeViewController.swift > refreshInMainContext(), refreshInBackgroundContext()
@IBAction func refreshInMainContext(_ sender: Any) {
mainContext.perform {
self.mainContext.refresh(self.employeeInMainContext!, mergeChanges: true)
self.mainContextSalaryLabel.text = self.formatter.string(for: self.employeeInMainContext?.salary)
}
}
@IBAction func refreshInBackgroundContext(_ sender: Any) {
backgroundContext.perform {
self.backgroundContext.refresh(self.employeeInBackgroundContext!, mergeChanges: true)
let salary = self.employeeInBackgroundContext?.salary?.decimalValue
DispatchQueue.main.async {
self.mainContextSalaryLabel.text = self.formatter.string(for: salary)
}
}
}
refresh는 객체를 최신 값으로 대체한다.
최신 값은 fetch 이후에 update 된 값을 의미한다.
결과
따라서 두 Context 중 마지막으로 update 한 값이 최신 값이 된다.
만약 mainContext에서 update 하고 save를 진행해도,
이 과정 사이에 backgroundContext에서 update 된 이력이 존재한다면 backgroundContext의 값을 최신 값으로 취급한다.
'학습 노트 > iOS (2021)' 카테고리의 다른 글
201. Migration (0) | 2022.07.06 |
---|---|
200. Performance & Debugging (0) | 2022.06.30 |
198. Concurrency with Context (0) | 2022.06.09 |
197. Batch Processing with CoreData (0) | 2022.06.03 |
196. Data Validation (0) | 2022.05.30 |