[Swift]CoreData 정리
CoreData란?
CoreData는 Apple에서 제공하는 객체 그래프(Object Graph) 및 영속성(Persistence) 관리 프레임워크입니다. SQLite를 기반으로 로컬 데이터베이스로도 사용할 수 있지만, 단순히 데이터베이스 기능뿐만 아니라 객체 간의 관계 관리, 상태 추적, 변경 감지, iCould 연동, Undo/Redo, Lazy Loading 등 다양한 기능을 제공합니다.
객체 그래프란?
객체 그래프란 참조를 통해 서로 연결되어 있는 객체들의 구조를 말합니다. 아래 코드 예시를 보면 Person과 Dog는 서로 참조를 가지고 있고, 이를 도식화하면 그래프 형태로 나타낼 수 있습니다. CoreData는 이 객체들 사이의 관계를 정의하고, 이를 저장소에 관계형 데이터처럼 변환하여 저장합니다.
1
2
3
4
5
6
7
8
9
class Person {
var name: String
var dog: Dog?
}
class Dog {
var name: String
weak var owner: Person?
}
CoreData의 저장 구조
CoreData의 저장 구조는 아래와 같은 계층 구조를 가집니다.
1
2
3
4
5
6
7
NSManagedObject // DB에 저장될 객체
⬇️
NSManagedObjectContext // 변경 사항을 추적하는 작업 공간
⬇️
NSPersistentStoreCoordinator // 저장소와 Context를 연결
⬇️
저장소 (SQLite, CloudKit, In-Memory 등)
NSManagedObject: CoreData를 통해 자동으로 생성 및 관리되는 객체로, 속성 및 관계를 포함하며 DataBase의 Scheme 역할을 합니다.
NSManagedObjectContext: 트랜잭션 단위의 작업 공간으로, 변경사항을 추적하고 save() 를 통해 저장할 수 있습니다.
NSPersistentStoreCoordinator: 실제 저장소와 Context를 연결하는 역할을 합니다.
NSPersistentContainer는 CoreData Stack을 구성하는데 필요한 객체들을 담는 역할을 합니다. CoreData를 설정할 때 주로 사용됩니다.
NSManagedObject 생성
MyModel.xcdatamodeld 라는 CoreData Model을 만든 다음,
Entites 에 저장하고 싶은 객체를 모델링하면
NSManagedObject가 자동으로 생성되고, 코드에서 참조할 수 있게 됩니다.
멀티스레드 환경에서 CoreData 다루기
CoreData에서 이뤄지는 트랜잭션들은 NSManagedObjectContext를 통해 이뤄지는데, Context는 기본적으로 메인스레드에서 실행됩니다.
따라서 대량의 데이터 처리나 저장 작업을 백그라운드에서 처리하지 않으면 메인스레드가 버벅이게 되어 UX에 영향을 줄 수 있습니다.
NSManagedObjectContextConcurrencyType
이를 위해 CoreData는 Context를 초기화할 때 NSManagedObjectContextConcurrencyType 옵션을 제공합니다. 옵션으로 .mainQueue 또는 .privateQueue 를 사용할 수 있고, .mainQueue 는 메인 스레드에서 .privateQueue는 백그라운드 스레드에서 실행되므로 UI와 관련된 작업만 mainQueue에서 수행하는게 좋습니다.
또는 newBackgroundContext() 메소드를 통해 내부적으로 .privateQueueConcurrencyType으로 설정된 Context를 생성할 수도 있습니다.
1
let backgroundContext = persistentContainer.newBackgroundContext()
.mainQueueConcurrencyType으로 설정된 viewContext도 생성할 수 있습니다.
1
let viewContext = persistentContainer.viewContext
perform, performAndWait
Context에서의 작업은 perform 또는 performAndWait을 통해 안전하게 실행할 수 있습니다. perform은 비동기, performAndWait은 동기 방식으로 동작합니다.
큐에 작업이 등록되고 순차적으로 실행되어 데이터 레이스 문제를 방지할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
// 비동기
backgroundContext.perform {
let entity = SomeEntity(context: backgroundContext)
entity.title = "Test"
try? backgroundContext.save()
}
// 동기
backgroundContext.performAndWait {
let entity = SomeEntity(context: backgroundContext)
entity.title = "Test"
try? backgroundContext.save()
}
MergePolicy
멀티스레드 환경에서 서로 다른 NSManagedObjectContext가 동일한 객체를 수정하고 저장하려고 하면 충돌(Conflict)이 발생합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func saveAsync() {
let viewContext = container.viewContext // 메인 스레드
let bgContext = container.newBackgroundContext() // 백그라운드 스레드
// 동일한 객체 참조
let object1 = try? viewContext.existingObject(with: id) as? SomeEntity
let object2 = try? bgContext.existingObject(with: id) as? SomeEntity
// 수정
object1.title = "title1"
object2.title = "title2"
try? viewContext.save() // 저장됨
bgContext.perform {
// bgContext의 스냅샷과 db의 객체 상태가 다름을 감지
// NSMergeConflict 발생
// mergePolicy가 .error(기본값)이므로 저장 실패
try? bgContext.save()
}
}
이러한 문제를 해결하기 위한 정책이 MergePolicy입니다.
MergePolicy를 설정하지 않으면 기본값은 .error로 충돌이 발생했을 때 저장에 실패합니다.
MergePolicy 종류는 아래와 같습니다.
.error: 충돌 시 저장 실패 .mergeByPropertyObjectTrump: 현재 Context 값으로 덮어쓰기 (충돌된 속성만) .mergeByPropertyStoreTrump: DB 값이 우선 .overwrite: 현재 Context 값으로 덮어쓰기 (객체 전부) .rollback: 현재 Context의 변경사항을 모두 되돌림




