컴퓨터/iOS

[iOS] CoreData 사용해보기

limorbear 2024. 12. 24. 15:52

CoreData?

CoreData는 디바이스의 디스크에 데이터를 영속화하기 위한 목적으로 제공되는 프레임워크이다. 파일 기반 RDB인 SQLite를 기반으로 되어있다고 한다. 실제로 사용해보면서 ORM에 가깝다는 생각이 많이 들었다.

프로젝트에 CoreData 추가하기

처음 프로젝트를 만들 때 Use Core Data에 체크하거나, 이미 존재하는 프로젝트에 모델을 추가해줄 수도 있다. 관련해서는 문서를 참고한다.

전자의 방법을 택한 경우, AppDelegate에 다음과 같은 보일러플레이트 코드가 추가된다. NSPersistentContainer를 프로퍼티로 추가하고 있다.

AppDelegate 객체는 애플리케이션의 루트 객체이기 때문에, NSPersistentContainer 객체를 코드 어디서든 싱글톤으로 접근할 수 있게된다.

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    // 생략
    
    // 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: "coredata_study")
        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
    }()

    // MARK: - Core Data Saving support

    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.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)")
            }
        }
    }
}

 

데이터 모델링

상술했듯이 CoreData는 SQLite 기반이기 때문에 사전에 저장하고자 하는 엔티티의 스키마를 정의해두어야 한다. Xcode에서 .xcdatamodeld 파일을 연다.

좌측 하단의 Add Entity 버튼을 눌러 새 엔티티를 만들고, 적절하게 이름을 준다.

이 엔티티를 선택한 상태에서 우측 하단의 Add Attribute 버튼을 눌러 엔티티를 구성하는 여러 속성을 정의한다.

이에 관해서는 이 문서에서 자세히 설명하고 있다.

AppDelegate에 추가된 보일러플레이트 코드를 그대로 활용해서 접근

위에서 보았듯이 AppDelegate의 `persistentContainer` 프로퍼티를 참조하여 `NSPersistentContainer` 객체에 접근할 수 있다. 이  NSPersistentContainer 객체로부터 여러 작업을 시작할 수 있다.

NSPersistentContainer?

CoreData 프레임워크는 여러 클래스로 이뤄진다. NSPersistentContainer는 각종 클래스를 관리하고 제공해주는 클래스이다. 다른 클래스들은 다음과 같다.

  • NSManagedObjectModel 객체는 앱에서 사용되는 엔티티에 대한 명세(프로퍼티, 다른 엔티티와의 릴레이션) 담는 모델 파일(.xcdatamodeld)을 표현한다. 해당 파일을 로드하고, 그 내용을 다룰 수 있는 각종 프로퍼티와 메서드를 제공한다.
  • NSManagedObjectContext 객체는 엔티티들(NSManagedObject)이 조작되는 공간이자 그 변화를 추적한다.
  • NSPersistentStoreCoordinator 객체는 실제 내부 스토어로부터 엔티티들을 읽거나 저장한다.

저장하고 불러오기

아래 예제는 엔티티를 저장하고 불러오는 작업을 간단하게 수행한다. Person이라는 엔티티에 대한 쓰기/읽기 작업이다. 이 엔티티에는 name(String) 어트리뷰트가 정의되어 있다.

func save(name: String) {
  
  guard let appDelegate =
    UIApplication.shared.delegate as? AppDelegate else {
    return
  }
  
  let managedContext =
    appDelegate.persistentContainer.viewContext
  
  let entity =
    NSEntityDescription.entity(forEntityName: "Person",
                               in: managedContext)!
  
  let person = NSManagedObject(entity: entity,
                               insertInto: managedContext)
  
  person.setValue(name, forKeyPath: "name")
  
  do {
    try managedContext.save()
    people.append(person)
  } catch let error as NSError {
    print("Could not save. \(error), \(error.userInfo)")
  }
}
func fetchAll() {
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
        return
    }
    
    let managedContext = appDelegate.persistentContainer.viewContext
    
    let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person")
    
    do {
        people = try managedContext.fetch(fetchRequest)
    } catch let error as NSError {
        print("Could not fetch. \(error), \(error.userInfo)")
    }
}

 

위의 코드는 Kodeco 튜토리얼에서 가져왔다.

반복되는 패턴은 다음과 같다.

  1. AppDelegate로부터 NSPersistentContainer 객체 얻기
  2. NSPersistentContainer 객체로부터 NSManagedObjectContext 객체 얻기
  3. 컨텍스트로부터 작업 시작

위 코드를 보면 각 엔티티는 NSManagedObject 객체로 다루게 된다. NSManagedObject는 모든 엔티티를 표현할 수 있는 타입인데, 일반적인 클래스나 구조체처럼 내부의 프로퍼티를 직접 읽고 쓸 수는 없고 KVO(Key-Value Observing) 방식으로 간접적으로 다루도록 되어있다. 엔티티에 대한 정의는 NSManagedObject 객체 내부의 `entity: NSEntityDescription` 프로퍼티에 포함되어 있다.

NSManagedObject 하위 클래스 Codegen

NSManagedObject는 기본적으로 너무 불편하다. 다행이도 Xcode에서는 특정 엔티티를 표현하는 NSManagedObject의 하위 타입을 코드젠 방식으로 생성하는 방법을 제공하고 있다. 이렇게 만든 하위 타입에는 내부의 프로퍼티를 엔티티의 어트리뷰트로 바인딩해, 일반적인 클래스와 구조체에서 프로퍼티를 다루듯이 엔티티의 어트리뷰트를 조작하거나 참조할 수 있다.

 

데이터 모델 편집기에서 엔티티를 선택하고, 우측 인스펙터의 Codegen을 Manual/None으로 설정한다. Class Definition인 경우 모든 것들을 자동으로 해준다. 다만 여기서는 실제로 생성해주는 코드가 어떤 것인지 보려고 Manual/None로 지정해본다.이 경우 어트리뷰트가 수정되었을 때 직접 수정하거나 매번 메뉴에서 Codegen을 해줘야 한다.

 

상단 메뉴 - Editor - Create NSManagedObject Subclass...를 눌러 Codegen 한다.

 

지정한 경로에 엔티티+CoreDataClass.swift와 엔티티+CoreDataProperties.swift가 생성되었다. 전자는 비어있는 클래스 정의이며, 후자는 어트리뷰트와 바인딩될 프로퍼티들이 익스텐션으로 정의되어 있다.

//  Person+CoreDataProperties.swift

extension Person {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Person> {
        return NSFetchRequest<Person>(entityName: "Person")
    }

    @NSManaged public var name: String?
    @NSManaged public var phone: String?
    @NSManaged public var birth: Date?

}

 

 

엔티티의 각 어트리뷰트가 프로퍼티로 잘 바인딩된 것을 볼 수 있다.  나중에 엔티티의 어트리뷰트를 수정하게 되면 코드젠을 한 번 더 해주면 된다. 알아서 잘 추가 된다.

 

CoreData는 내부적으로 SQLite를 사용한다고 했는데 그 컬럼 타입과 Swift의 타입 체계는 차이가 있다. 따라서 엔티티의 어트리뷰트 타입이 하위 클래스 프로퍼티로 바인딩될 때에도, 타입이 1:1로 대응될 수는 없으며 다음과 같이 바인딩된다.

  • String maps to String?
  • Integer 16 maps to Int16
  • Integer 32 maps to Int32
  • Integer 64 maps to Int64
  • Float maps to Float
  • Double maps to Double
  • Boolean maps to Bool
  • Decimal maps to NSDecimalNumber?
  • Date maps to Date?
  • URI maps to URL?
  • UUID maps to UUID?
  • Binary data maps to Data?
  • Transformable maps to NSObject?

참고로 데이터 모델 인스펙터에서 Codegen을 Category/Extension으로 해두면 엔티티+CoreDataProperties.swift를 자동으로 관리하고 갱신해준다. Category/Extension로 지정하고, 파일 네비게이터에서 엔티티+CoreDataProperties.swift를 삭제하면 된다.

각 설정마다 장단점이 있어보인다. 만약에 하위타입 자체를 비즈니스 로직을 포함한 모델 객체로 사용하고자 한다면 Manual/None이나 Category/Extension으로 해두고 직접 구현하면 된다. 또한 명시적으로 엔티티에 대한 명세가 코드베이스에 남아서 버전 관리에 포함하고자 한다면 자동 생성보다는 Manual/None이 좋을 것 같다.

관련해서는 이 문서를 참고.

 

저장하고 불러오기 (개선)

그럼 위의 예제 코드를 아래처럼 고쳐볼 수 있다.

func save(name: String) {
  
  guard let appDelegate =
    UIApplication.shared.delegate as? AppDelegate else {
    return
  }
  
  let managedContext =
    appDelegate.persistentContainer.viewContext
  
  // Here!
  let person = NSEntityDescription.insertNewObject(forEntityName: "Person", into: managedContext) as! Person
  person.name = name
  
  do {
    try managedContext.save()
    people.append(person)
  } catch let error as NSError {
    print("Could not save. \(error), \(error.userInfo)")
  }
}
func fetchAll() {
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
        return
    }
    
    let managedContext = appDelegate.persistentContainer.viewContext
    
    // Here
    // let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person")
    let fetchRequest = Person.fetchRequest()
    
    do {
        people = try managedContext.fetch(fetchRequest)
    } catch let error as NSError {
        print("Could not fetch. \(error), \(error.userInfo)")
    }
}

 

참고자료

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

https://www.kodeco.com/books/core-data-by-tutorials/v8.0/chapters/1-your-first-core-data-app