JUST DO IT PROJECT

[iOS] Core Data란? 1. Core Data Model과 NSPersistentContainer 만들기 본문

개발/iOS

[iOS] Core Data란? 1. Core Data Model과 NSPersistentContainer 만들기

웨일.K 2023. 2. 10. 22:59
반응형

오늘은 코어데이터에 대해서 알아보려고 합니다. 특히 아래 내용에 대해서 알아볼 예정입니다.

  • Core Data 란 무엇인지?
  • Core Data Stack이란 무엇인지?
  • Core Data의 Model은 어떻게 만드는지?
  • Core Data Stack 즉 NSPersistentContainer를 어떻게 만드는지?
    • model, context, coordinator에 대해서는 별도의 포스팅에서 알아보겠습니다
  • UIKit과 SwiftUI 각각에서 어떻게 사용할 수 있는지?

 

 

Core Data

  • 하나의 단말 내부에 데이터를 캐싱하거나 보존하는 프레임워크
  • 또는 CloudKit을 이용해 여러 단말의 데이터를 싱크하는 프레임워크

Overview

코어데이터는 앱의 데이터를 오프라인에서도 사용할 수 있도록 저장하거나, 임시 데이터를 캐싱하는데 사용됩니다. 여러 디바이스를 iCloud로 연동하기 위 해서 Core Data는 schema를 CloudKit 컨테이너에 미러링하기도 합니다.
코어 데이터가 제공하는 기능은 아래와 같습니다.

  • 영구보존
    • 코어데이터는 저장소에 있는 정보와 객체를 매핑하고, 직접적으로 DB를 관리하지 않으면서 Swift나 Objc에서 데이터를 저장하기 쉽게 해줍니다.
  • Undo / Redo
    • 코어데이터의 undo 매니저는 변경사항을 추적하고, 각각, 그룹 또는 전체를 롤백 할 수 있습니다.
  • Background Data Tasks
    • UI를 블락할 수 있는 데이터 작업의 경우(JSON을 객체로 파싱하는 작업 등) 백그라운드에서 수행할 수 있습니다.
    • 그 후에 결과를 캐시하거나 저장해서 서버에 지나치게 자주 접근할 필요 없게 할 수 있습니다.
  • View Synchronization
    • 코어데이터는 뷰와 데이터간의 싱크를 맞추는 걸 도와줄 수 있습니다. 테이블뷰, 콜렉션 뷰에 데이터소스로 제공되는 방식으로.
  • Versioning and Migration
    • 코어데이터는 데이터모델의 버전을 관리할 수 있게 해주고, 앱이 진화할 수록 사용자 데이터를 옮길 수 있는 메커니즘을 제공합니다.

Core Data Model 생성하기

Xcode에서 처음으로 프로젝트를 만들 때, CoreData를 사용한다는 체크박스를 체크하면 자동으로 코어데이터 모델 파일을 하나 만들어줍니다. 확장자는. xcdatamodeld, 파일명은 보통 프로젝트명(Product Name)으로 생성됩니다. 파일명은 변경해도 됩니다.

만약 처음에 프로젝트를 만들 때 CoreData를 사용한다고 체크하지 않았다면 그냥 파일을 하나 만들어주면 됩니다. Model.xcdatamodeld 파일을 하나 만들어보겠습니다.

모델 파일을 생성하고 열어보면 아래와같은 화면이 뜹니다.

여기서 좌측 하단의 Add Entity 버튼을 클릭해 Item이라는 이름의 Entity를 하나 만들고 Date 타입의 timestamp라는 attributes를 생성해보겠습니다.  attributes는 DB의 schema라고 생각하시면 됩니다.

이렇게 생성한 데이터모델의 클래스는 자동으로 생성됩니다. 그래서 이렇게 모델을 추가하고나면 아래 코드처럼 코드 내에서 바로 모델을 생성할 수 있게 됩니다.

private func addItem() {
	let newItem = Item(context: viewContext)
	newItem.timestamp = Date()
}

위 코드에서 Item의 정의로 이동해보면 아래와 같이 자동생성된 클래스 파일을 볼 수 있습니다.

//
//  This file was automatically generated and should not be edited.
//
import Foundation
import CoreData

@objc(Item)
public class Item: NSManagedObject {

}

entity Item을 클릭한 후 우측 패널에서 확인해보면 이렇게 Entity와 Class가 선언되어있는걸 볼 수 있습니다. 원하신다면 클래스명을 바꿀수도 있겠죠. 

 

Core Data Stack

코어데이터 모델을 만들고나면, 여러 클래스들을 이용해서 이 코어데이터 모델을 관리할 수 있습니다. 이런 모든 클래스들을 뭉뚱그려서 코어데이터 스택이라고 부릅니다.

  • NSManagedObjectModel
    • 앱내의 타입, 프로퍼티, 관계를 묘사하는 모델 파일입니다.
    • 말그대로 모델입니다.
  • NSManagedObjectContext
    • Context입니다. 우리말로 맥락이니까, 다시말해서 데이터 모델의 현황판이라고 보면 되겠습니다.
    • 타입들의 변경사항을 추적합니다. 무엇이 변경된 상태인지.
  • NSPersistentStoreCoordinator
    • 영구저장소의 관리자입니다.
    • 직접 영구저장소에 접근하는게 아니라, 이 관리자 친구를 통해서 저장소에 CRUD를 합니다.
  • NSPersistentContainer
    • 위의 모델, 컨텍스트, 관리자를 들고 있는 하나의 클래스입니다.
    • Core Data Stack의 생성과 관리를 좀더 간단하게 해줄 수 있는 클래스로, iOS 10부터 사용되고 있습니다.

Persistent Container 초기화하기

  • 보통은 앱의 시작시점에 코어데이터를 초기화합니다.
  • Persistent Container를 lazy하게 생성해서 실제로 인스턴스화되는것은 앱에서 처음으로 사용되는 시점까지 미뤄둡니다.
  • UIKit을 사용해 프로젝트를 만들 때 코어데이터를 사용한다고 체크하면 App Delegate에 자동으로 셋업코드가 들어갑니다.
  • SwiftUI를 사용해 프로젝트를 만들 때에는 Persistence라는 Swift 파일이 별도로 생성됩니다.

UIKit

  1. NSPersistentContainer 타입의 lazy 변수 persistentContainer를 하나 선언합니다.
  2. 데이터모델의 파일이름을 생성자에 넣어서 컨테이너의 instance를 생성합니다.
  3. 영구저장소를 로딩합니다. 만약에 저장소가 없다면 위에 입력한 이름으로 생성합니다.
class AppDelegate: UIResponder, UIApplicationDelegate {

    ...

    lazy var persistentContainer: NSPersistentContainer = {        
        let container = NSPersistentContainer(name: "DataModel")
        container.loadPersistentStores { description, error in
            if let error = error {
                fatalError("Unable to load persistent stores: \(error)")
            }
        }
        return container
    }()

    ...
}

SwiftUI

  1. PersistenceController라는 structure를 선언하고, 이를 싱글톤 객체를 하나 만듭니다.
  2. 이 구조체의 초기화 시점에 데이터모델의 파일이름을 넣어서 컨테이너를 생성합니다.
  3. 초기화 시점에 영구저장소를 로딩합니다. 역시 저장소가 없다면 생성하게 될 것입니다.
  4. 컨테이너의 viewContext automaticallyMergesChangesFromParent = true설정해줍니다.
import CoreData

struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "JSCoreData")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        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)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

한번 생성되고나면 컨테이너는 모델과 컨텍스트, 저장소 관리자에 대한 참조(reference)를 들고 있습니다. 앞으로 앱에서는 이 컨테이너를 통해서 각각의 모델, 컨텍스트, 저장소 관리자에 접근하게 될겁니다.

ViewController에 Persistent container 참조를 넘겨주기

  • ViewController에서 CoreData를 import하고, 컨테이너의 레퍼런스를 받아서 볼 수 있게 변수를 하나 만들어줍니다.
import UIKit
import CoreData

class ViewController: UIViewController {

    var container: NSPersistentContainer!

    override func viewDidLoad() {
        super.viewDidLoad()
        guard container != nil else {
            fatalError("This view needs a persistent container.")
        }
        // The persistent container is available.
    }
}
  • AppDelegate로 다시 넘어가서, application(_:didFinishLaunchingWithOptions:) 함수에서 위에서 lazy var로 선언해준 persistentContainer를 루트뷰의 container 프로퍼티에 넣어줍니다.
class AppDelegate: UIResponder, UIApplicationDelegate {

    ...

    lazy var persistentContainer: NSPersistentContainer = {        
        let container = NSPersistentContainer(name: "DataModel")
        container.loadPersistentStores { description, error in
            if let error = error {
                fatalError("Unable to load persistent stores: \(error)")
            }
        }
        return container
    }()

    ...

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {        
        if let rootVC = window?.rootViewController as? ViewController {
            rootVC.container = persistentContainer
        }        
        return true
    }

    ...
}
  • 다른 ViewController에도 이 컨테이너를 넘겨주려면 루트 VC에서 했던것처럼 container 변수를 하나 만들고, 이전 VC의 prepare(for:sender:) 함수에서 컨테이너를 세팅해주면 됩니다.
class ViewController: UIViewController {

    ...

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let nextVC = segue.destination as? NextViewController {
            nextVC.container = container
        }
    }
}

Persistent Container를 상속받기 

UIKit

  • NSPersistentContainer는 subclassed 해서 사용하길 바라는 의도가 들어있습니다.
  • 그 subclass는 코어데이터 관련 로직들을 넣어서 사용하면 됩니다. 예를들어 데이터의 subset을 반환하거나, 디스크에 데이터를 저장하거나 하는로직이요.
  • 아래는 saveContext라는 함수를 추가해서 context에 변경사항이 있는 경우에만 저장하도록 해서 성능을 높이는 예제입니다.
import CoreData

class PersistentContainer: NSPersistentContainer {

    func saveContext(backgroundContext: NSManagedObjectContext? = nil) {
        let context = backgroundContext ?? viewContext
        guard context.hasChanges else { return }
        do {
            try context.save()
        } catch let error as NSError {
            print("Error: \(error), \(error.userInfo)")
        }
    }    
}

SwiftUI

  • PersistenceController의 싱글톤 객체를 가져와서, ContentView()의 environment 변수로 넘겨줄 수 있습니다.
import SwiftUI

@main
struct CoreDataApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}
  • ContentView에서는 @Environment라는 property wrapper를 이용해서 환경변수인 managedObjectContext를 꺼내 쓸 수 있게 되겠지요.
  • 아래는 사용 예제입니다. 이 예제에 대해서는 다음 포스팅에서 자세히 한번 다뤄보겠습니다.
import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        Text("Item at \(item.timestamp!, formatter: itemFormatter)")
                    } label: {
                        Text(item.timestamp!, formatter: itemFormatter)
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()

            do {
                try viewContext.save()
            } catch {
                // 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.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                // 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.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

private let itemFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    formatter.timeStyle = .medium
    return formatter
}()

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

요약

  1. Core Data 란 무엇인지?
    • 코어데이터는 애플에서 데이터베이스 사용을 편리하게 해주는 프레임워크입니다. 앱에서 DB를 직접 접근해서 쓰지 않아도 되도록 해줍니다.
  2. Core Data Stack이란 무엇인지?
    • 앱 내에서 데이터를 다루기 위해 필요한 세가지 클래스와, 이 세가지를 포함하는 컨테이너클래스를 뭉뚱그려 부르는 표현입니다.
    • 세가지 클래스는 각각 아래와 같습니다.
      • 모델(데이터 생김새) - NSManagedObjectModel
      • 컨텍스트(데이터 현황) - NSManagedObjectContext
      • 저장소 관리자( DB에 접근해 CRUD를 대신해줌) - NSPersistentStoreCoordinator
    • 위 세 클래스를 포함하는 컨테이너 클래스는 아래와 같습니다.
      • NSPersistentContainer
  3. Core Data의 Model은 어떻게 만드는지?
    • 모델은 New File > Data model로 만들 수 있습니다.
    • Entity와 attributes를 추가하면 자동으로 클래스가 생성됩니다.
  4. Core Data Stack 즉 NSPersistentContainer를 어떻게 생성하는지?
    1. 앱 시작 시점에 초기화해줍니다. 컨테이너를 만들 때 생성자에 모델 이름을 넘겨줍니다.
    2. 컨테이너 초기화 시점에 이 이름으로 된 영구저장소를 로딩합니다. (없으면 생성합니다.)
  5. UIKit과 SwiftUI 각각에서 어떻게 사용할 수 있는지?
    1. UIKit
      1. 보통 AppDelegate에 lazy 변수로 persistentContainer를 선언하고 생성합니다.
      2. 루트뷰컨트롤러에 container property를 선언합니다.
      3. App Delegate에서 루트뷰컨트롤러의 container에 위에서 생성한 persistentContainer를 넣어줍니다.
      4. 다른 VC에서 사용하고싶은 경우 동일하게 container property를 선언하고 상위 VC의 prepare함수에서 넣어줍니다.
    2. SwiftUI
      1. 별도의 싱글톤 객체를 만들고 persistentContainer를 선언합니다.
      2. View의 environment 변수로 싱글톤 객체의 persistentContainer.viewContext를 전달해서 사용합니다.

참고

https://developer.apple.com/documentation/coredata/setting_up_a_core_data_stack

반응형