ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Core Data 뜯어 보기
    Apple🍎/CoreData 2024. 1. 4. 15:06
    • Core Data가 뭔가?
    • Core Data stack은 무엇이고 왜 stack이라고 부르는가?
    • Core Data stack은 어떤 부분들로 구성되어있나?
    • 어떻게 persistent container는 작업을 간단히 만들어주나?
    • Xcode는 어떻게 CoreD Data를 위한 Stack을 만들고 이로부터 어떤걸 알 수 있는가?

    그래서 Core Data가 뭔데?

    According to Apple
    Use Core Data to save your application’s permanent data for offline use, to cache temporary data, and to add undo functionality to your app on a single device. To sync data across multiple devices in a single iCloud account, Core Data automatically mirrors your schema to a CloudKit container.

     

    Core Data를 이용하면

    • 오프라인 상태에서 데이터를 영구적으로 저장할 수 있다.
    • 임시 데이터를 캐싱할 수 있다.
    • 단일 기기에 대해 ‘undo’ 기능을 추가할 수 있다.
    • ICloud와 함께 여러 기기에 대하여 어플리케이션의 데이터을 동기화할 수 있다.

     

    흔히들 Core Data를 단순히 로컬 기기에 데이터 저장을 용이하게 해주는 도구 정도로 생각하는데 아니다.
    DB와의 동작을 추상화하여 쿼리를 직접 작성하지 않고도 데이터 저장을 해주는 도구( Sql3 wrapper ) 같은게 아니다.
    Core Data에서는 DB로부터 데이터 조회를 위해 SELECT를 하거나 데이터 변경을 위해 UPDATE를 하지 않는다. 심지어 Core Data의 저장소가 Sqlite3로 되어 있지 않는 경우도 있다.

    Core Data는 단순히 데이터 저장소가 아닌 ‘객체 그래프 관리자’다.

     

    예를들어 MVC / MVVM 에 아키텍쳐에서 XIB, Storyboards, SwiftUI가 사용자 UI를 그리는 뷰(V)에 해당한다면 Core Data는 어플리케이션에서 비지니스에 사용하는 객체들을 모델링하는 모델(M)에 해당한다.

    이거면 되나?

    func reloadSongs() {
            let album = CoreDataManager.shared.getAlbum(byID albumID : "myAlbumID")
            self.title = album.title
            self.songs = album.songs
            tableView.reloadData()
     }

    너무 간단해서 단순 의사코드처럼 보일 수 있지만 실제 Core Data를 이용하여 엘범을 조회하고 앨범의 제목과 앨범과 관련된 음악들을 업데이트 하는 과정이다.

    Core Data Stack

    뭔가 해야할일이 많을 것 같은데 코드가 너무 단순하다. 누군가 구체적인 동작들을 추상화시켜 놓아 편리하게 사용할 수 있도록 만들어 놓은 것이다. 이것이 어떻게 가능할까를 알기 위해서는 일단 Core Data Stack이라는 것에 대해 알아야한다.

    Core Data가 객체 그래프 관리자라고 했으니 Core Data가 어떤것들로 구성되어 있을지 예상해보자

    1. 객체와 객체들간의 관계를 알고 있어야 관리를 하지 않겠는가? : 엔티티와 엔티티 관계를 정의하는 모델 스키마를 가지고 있을 것이다.
    2. 어찌됐든 최종적으로 어딘가에 저장을 해야된다. : 데이터 저장소를 가지고 있다.
    3. 우리가 코드를 간단히 작성할 수 있다는 것은 누군가 대신해준다는 뜻이다. : 데이터에 쉽게 접근할 수 있게 무엇인가가 일을 자동화 해주고 있다.

    위에서 예상한 것들이 Core Data Stack을 구성하는 3가지 핵심 요소이다.

    1. Managed Data Model
    2. Persistent Store Coordinator
    3. Managed Object Context

    왜 Stack이라고 하냐?

    우리가 데이터베이스를 디자인할 때 첫번째로 고려하는 것이 무엇일까?
    어떤 엔티티들이 필요하고 이들의 관계가 어떻게 될지를 생각한다.
    따라서 데이터베이스 스키마를 설계하는 것이 첫번째 지점이 된다.
    두번째로는 설계한 데이터베이스 스키마를 기반으로하여 데이터 저장소를 생성한다.
    마지막으로 생성한 데이터 저장소와 어플리케이션간의 상호작용하는 방법을 정의하는 컨텍스트를 만든다.

    데이터베이스 스키마 설계를 기반으로 데이터 저장소를 생성하고
    데이터베이스 스키마 설계를 기반으로 생성된 데이터 저장소와 상호작용하는 방법을 나타내는 컨텍스트를 만든다. 위와 같이 Core Data는 먼저 기반이 되는 요소를 먼저 쌓은 상태에서 다음 요소가 올라오는 형태로 구성되기 때문에 “쌓는다”를 강조하기위해서 “Stack” 이라고 부르는 것이다.

     

    Core Data Stack 만들기

    Core Data 가 어떻게 구성되어 있는지 확실하게 파악하기 위해서 Core Data Stack을 구성해보자.

    import CoreData
    
    final class CoreDataStack {
    
    }

    일단 CoreDataStack 구성하기 위해서는 가장 기초가 되는 데이터베이스 스키마 설계가 필요하다.
    이것을 담당하는 것이 Managed Data Model이다.

    Managed Data Model

    데이터 스키마 설계시 테이블과 속성을 이용하는 Sqlite3와 다르게 엔티티와 속성을 사용한다.

    음악앱을 만든다고 가정해보자. 그러면 필요한 엔티티는 앨범, 플레이리스트, 음악 등이 있을 것이고 속성으로는 이름, 재생시간, 발매연도 등이 있을 것이다. 여기까지는 Sqlite3의 테이블 설계와 동일하다.
    하지만 Managed Data Model은 엔티티간의 관계 또한 나타낼 수 있다.
    예를들어 앨범과 음악엔티티는 일대다 관계(한 앨범에 여러 음악이 들어있을 수 있음)를 직접 설정할 수 있지만 Sqlite3에서는 관계를 나타내기 위해 음악테이블 필드에 앨범 ID를 추가해서 foreign key를 사용해야한다.

    • Managed Data Model 만들기

    • Stack에 Managed Data Model 추가하기
      • 위에서 만든 MyModel 기반으로해서 ManagedObjectModel 만들기
    import CoreData
    
    final class CoreDataStack {
    
        // Managed Data Model
        private lazy var managedObjectModel: NSManagedObjectModel = { 
            guard let url = Bundle.main.url(forResource: "MyModel", withExtension: "momd") else{
                fatalError("Unable to Find Data Model")
            guard let managedObjectModel = NSManagedObjectModel(contentsOf: url) else {
                fatalError("Unable to Load Data Model")
            }
            return managedObjectModel
        }( )
    }

    The Persistent Store

    • Managed Data Model을 이용해 DB스키마를 설계를 하고나서 이를 기반으로 실제로 데이터 저장할 곳을 생성해야한다.
    • 이름에서부터 알 수 있듯이 Persistent Store은 엔티티들을 저장하는 곳이다.
    • 새로운 엔티티를 저장하거나 기존의 엔티티를 수정할 때 이 데이터들은 모두 persistent store에 저장된다.
    • 저장소는 여러 종류가 존재하지만 플랫폼마다 사용가능한 저장소가 다르다.

    Store 종류 4가지

    • 애플은 4가지 타입의 Persistent Store를 제공한다.
    • 1가지는 메모리에 3가지는 디스크에 저장하는 형태이다.
    • Persistent store를 만져볼일은 Core Data Stack을 직접 다룰 때 이외에는 없지만 Core Data 동작에 대해서 알기위해서는 알아야할 필요가 있다.
    1. In Memory
      • In Memory 저장소는 영구저장소가 아니다.
      • 다른 Store 유형처럼 Core Data 객체가 동작하지만 앱이 종료된 순간 모두 사라진다.

     

    In Memory Store을 보고 “아니 저장도 못하는데 이딴걸 왜써” 라고 생각할 수 있다.
    하지만 이는 Core Data를 잘못이해한 것이다.

    Core Data는 Database가 아니다. Core Data는 객체 그래프 관리 및 저장을 위한 프레임 워크이다.

     

    Core Data의 첫번째 목표는 엔티티와 엔티티간의 관계를 정의하는 것이고 부차적으로 이 정보를 디스크 상에 저장할 수도 있는 것이다. -> Persistent Storage는 Optional Feature다.

    • In Memory 를 이용해 임시적으로 객체 그래프를 정의하여 가지고 있을 수 있게 되고 이를 복잡한 HTTP 요청 결과를 받는 그릇으로 활용할 수 있다.
    • 유닛테스트나 통합테스트 진행시에 필요한 데이터를 잠깐 띄울 때도 활용할 수 있다.
    • 테스트에 필요한 데이터들을 In Memory 상에 띄워 이미 존재하는 DB 데이터와의 충돌을 막을 수도 있다.
    final class CoreDataStack {
    
        // Persistent Store Coordinator ~ In-Memory 
        private lazy var persistentStoreCoordinator: NSPresistentStoreCoordinator = {
        let persistentStoreCoordinator =  
            NSPersistentStoreCoordinator(mangedObjectModel: self.managedObjectModel)
        do {
            try persistentStoreCoordinator.addPresistentStore
                    (ofType: NSInMemoryStoreType,
                    configurationName: nil,
                    at: nil,
                    options: nil)
            } catch {
                fatalError("Unable to Add In Memory Store")
            }
            return persistentStoreCoordinator
        }( )
    }

    persistentStoreCoordinator 객체의 역할은 앞서서 만든 DB 스키마(managedObjectModel)를 선택한 type의 Store와 연결해주는 것이다.
    Coordinator는 단지 Store에 DB Scheme을 깔아주지 직접 데이터를 저장하지는 않는다. 그렇기에 이름도 저장소가 아니라 코디네이터이다.

     

     

     2. SQLite

    • SQLite는 CoreData에서 일반적으로 가장 많이쓰이는 Store type이다.
    • 메모리 사용을 가장 효율적으로 할 수 있는 Store type이다.

    SQLite의 경우에는 CoreData가 전체 객체그래프를 메모리위에 들고 있지 않는다. CoreData는 단지 Sql 쿼리를 대신 작성해주고 쿼리 요청 결과를 분석하는 역할을 수행하면서 요청시 필요한 정보 일부만 메모리에 들고 있다.

    final class CoreDataStack {
    
        // Persistent Store Coordinator ~ SQLite
        private lazy var persistentStoreCoordinator: NSPresistentStoreCoordinator = {
        let persistentStoreCoordinator =  
            NSPersistentStoreCoordinator(mangedObjectModel: self.managedObjectModel)
        let fileManager = FileManager.default
        let storeName = "MyStorage.sqlite"
        let documentsDirectoryURL =
             fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let persistentStoreURL = documentsDirectoryURL.appendingPathComponent(storeName)
        do {
            let options = [
                NSMigratePersistentStoresAutomaticallyOption : true,
                NSInferMappingModelAutomaticallyOption: true
            ]
            try persistentStoreCoordinator.addPersistentStore
            (ofType: NSSQLiteStoreType, configurationName: nil,
            at: persistentStoreURL, options: options)
            } catch {
                fatalError("Unable to Add Persistent Store")
            }
            return persistentStoreCoordinator
        }( )
    }
    • 그냥 메모리에 띄웠다가 앱 종료시 데이터가 같이 사라지는 In-Memory type 과 다르게 SQLite는 앱이 꺼져도 데이터가 보존될 수 있도록 어딘가 보존해야한다. 따라서 SQLite 파일이 디스크상에 저장될 경로에 대한 설정이 더 필요하다.
    • SQLite DB는 코디네이터를 거쳐 전달받은 DB Scheme를 기반으로 설정된다.

    3. Binary 와 XML

    NSIncrementalStore을 상소하여 Custom Store type도 만들어 사용할 수도 있다.

    The Context

    • 지금까지 Stack에 맨 밑에 Model을 깔고 그 위에 Store을 선택해서 올렸으니 마지막으로 추가할 것은 “Managed Object Context”이다.
    • Managed Object Context는 Stack의 가장 상단이 위치하여 CoreData의 Scratch Pad의 역할을 한다.
    • Context는 DB와 관련이 있는 객체들(엔티티)들을 다루는 공간으로 Create, Fetch, Modify등이 이루어진다.

    Scratch Pad란 실제 동작을 실행하기전에 임시로 필요한 작업들을 하는 공간이다.
    어플리케이션과 DB간의 상호작용(Create, Read, Update, Delete)을 직접하지 않고 상호작용에 필요한 작업들을 Scratch Pad에 미리 다해두고 DB에는 이 결과를 반영하는 방식으로 작동한다.

    Context라고 부르는 이유
    Context는 “Environment” (환경)이라는 의미로 쓰인다.
    즉 엔티티에 대한 작업을 수행하기 위한 “환경”이라는 뜻으로 Context라고 부른다.

    • Context에 한 작업들은 “Save”(DB에 반영하기) 전까지는 작업을 이루어진 특정 Context에만 종속되어 있다.
    final Class CoreDataStack {
        // Managed Object Context 
        lazy var managedObjectContext: NSManagedObjectContext = {
            let managedObjectContext =     
                ManagedObjectContext(cocurrencyType: .mainQueueConcurrencyType)
            managedObjectContext.persistentStoreCoordinator = 
                self.persistentStoreCoordinator
            return managedObjectContext
        }( )
    }
    • Managed Object Context를 생성하기 위해서 두가지 중요한 설정이 필요하다.
    • 첫번째는 Concurrency Type (동시성 유형)을 main과 private 중 선택하는 것이다.
    • 두번째는 앞서 설정한 Persistent Store를 Context에 연결하는 것이다.

     

    Core Data Container

    • Core Data를 사용할 때 Core Data Stack을 일일이 설정하지 않고 어느정도는 바로 쓸 수 있게 만들어준게 바로 Core Data Container다.
    • Core Data Container는 Core Data Stack의 구성요소들(모델, 코디네이터, 컨텍스트)를 최소한의 구성으로 미리 세팅해서 들고 있다.
    let container = NSPersistentContainer(name: "MyStore")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in } ...
    • CoreDataStack을 구성하는데 필요한 ManagedDataModel, NSPersistentStoreCoordinator, ManagedObjectContext를 건들 필요없이 위의 두줄로 Core Data 사용을 위한 준비가 끝난다.

    Context에 접근하기

    • Context는 엔티티에 대한 작업을 수행하기 위한 환경이라고 했다. 즉 DB에 엔티티들을 저장, 수정 등을 하려면 Context를 거쳐야한다.
    • Core Data Contaienr를 이용해 설정을 하는 경우 다음 방법을 통해 Context에 접근이 가능하다.
    let context = persistentContaienr.viewContext

    왜 viewContext라고 하는가????
    Context는 다중 스레드 환경에서 데이터를 처리하는데 사용된다.
    모든 Queue에는 각자의 Context가 존재한다. -> MainQueue에도 Context가 존재한다.
    MainQueue는 주로 UI(View)를 관리하기 때문에 MainQueue에 속하는 Context를 viewContext라고 한다.

    • 백그라운드 쓰레드에서 사용할 Context를 다음과 같이 만들 수 있다.
    let privateContext = presistentContainer.newBackgroundContext( )

    Container를 사용해도 세부설정이 가능하다.

    • Container에서도 Stack의 Data Model과 Store type은 직접 설정해 줄 수 있다.
    • Container에서 별도로 설정하지 않으면 알아서 동일한 이름의 Data Model과 SQLite File을 사용한다.
    let container = NSPersistentContainer(name: "MyStore")
    • 다음 처럼 optional parameter를 이용하면 직접 설정할 수 도 있다.
        var objectModel: NSManagedObjectModel {
            guard let url = Bundle.main.url(forResource: "MyModel", withExtension: "momd") else {
                fatalError("Unable to Find Data Model")
            }
            guard let managedObjectModel = NSManagedObjectModel(contentsOf: url) else {
                fatalError("Unable to Load Data Model")
            }
            return managedObjectModel
        }
        func persistentContainer() {
            let container = NSPersistentContainer(name: "mysqlFile", managedObjectModel: objectModel)
        }
    
    • 다음과 같이 NSPersistentStoreDescription을 이용하면 store type등 다양한 설정을 커스텀할 수 있다. 예) migration type, read - only, timeout duration, SQLite에서 PRAGMA 구문 실행
    let container = NSPersistentContainer(name: "mysqlFile")
    let storeType = NSPersistentStoreDescription()
    storeType.type = NSInMemoryStoreType
    container.persistentStoreDescriptions = [storeType]

     

    애플아 컨테이너 하나 줘바라. 

    • 프로젝트 시작시 Core Data를 체크하면 애플이 선택한 interface에 맞게 컨테이너를 세팅해준다
    • NSPersistnetContainer를 세팅하고 앱 내에서 싱글톤으로 들고 있게 설정해 준다.

     

    Core Data with UIKit

    • UIKit 기반 어플리케이션은 XCode 마법사가 persistent Container를 저장하기 위해 App Delegate를 사용한다.
    • 앞서 봤던 두줄로 하는 컨테이너 설정을 해준다.
        // MARK: - Core Data stack
        lazy var persistentContainer: NSPersistentContainer = {
            /*
             The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
            */
            let container = NSPersistentContainer(name: "MyCoreDataApp2")
            container.loadPersistentStores(completionHandler: { (storeDescription, error) in
                if let error = error as NSError? {
                    // Replace this implementation with code to handle the error appropriately.
                    // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                    /*
                     Typical reasons for an error here include:
                     * The parent directory does not exist, cannot be created, or disallows writing.
                     * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                     * The device is out of space.
                     * The store could not be migrated to the current model version.
                     Check the error message to determine what the actual problem was.
                     */
                    fatalError("Unresolved error \(error), \(error.userInfo)")
                }
            })
            return container
        }()
    

    Core Data with SwiftUI

    • SwiftUI가 UIKit과 다른점
    1. App Delegate가 없다. App과 View Protocol을 conform하는 struct들로 구성되어 있다. “app” struct는 바꿀 수 없다.
    2. SwiftUI의 Preview에서는 persistent store에 저장되어 있는 데이터를 사용할 수 없다.
    struct PersistenceController {
        static let shared = PersistenceController()
        static var preview: PersistenceController = {
            let result = PersistenceController(inMemory: true)
            let viewContext = result.container.viewContext
            for _ in 0..<10 {
                let newItem = Item(context: viewContext)
                newItem.timestamp = Date()
            }
    ..
    }
    • PersistenceController를 보면 두개의 static class instance가 존재하는데
    • shared는 app에 사용할 꺼
    • preview는View개발시 Preview에서 사용할 꺼다. (In-Memory 방식)

     

    Core Data 큰그림

     


     

     

     

     

    Unleash Core Data: Fetching Data, Migrating, and Maintaining Persistent Stores

    Create apps with rich capabilities to receive, process, and intelligently store data that work across multiple devices in the Apple ecosystem. This book will show you how to organize your … - Selection from Unleash Core Data: Fetching Data, Migrating, a

    www.oreilly.com

     

     

     

     

    댓글

Designed by Tistory.