포스트

[Swift]CoreData 정리

[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의 변경사항을 모두 되돌림

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.