CoreData 훑어보기

·

5 min read

앱 개발을 하다보면 데이터를 서버에 저장하지 않고 로컬 데이터베이스에 저장해야 할 필요가 있다. 이 때 사용할 수 있는 로컬 저장소로는 User Defaults, CorData, Realm등이 있는데 CoreData의 컨셉과 간단한 사용법에 대해 알아보고자 한다.

Core Data란

  • 애플리케이션에서 모델 계층 개체를 관리하는 데 사용하는 프레임워크로, 객체의 라이프 사이클이나 영속성 관리를 위한 기능을 제공하는 Object Graph Manager

    Object Graph? 서로 연결된 객체들의 연관성을 나타낸 그래프. Core Data는 저장할 정보들을 객체의 형태로 저장하고 관리함. Core Data는 이러한 객체간의 관계를 설정할 수 있으며, 연결된 객체들은 영속적으로 동기화되는 특성을 갖는다.

  • 데이터를 테이블 단위가 아닌 객체로 취급(ORM 방식)
  • Core Data는 on-disk 지속성을 제공함
    • 앱을 종료하거나 기기를 종료해도 데이터에 접근 가능
    • 앱이 foreground나 background에 있는 경우에만 데이터를 저장함
  • 관리 객체 모델은 Entity, Attributes, Relationships로 구성됨
    • Entity: Core Data의 클래스 정의
    • Attributes: Entity의 정보를 나타내는 속성
    • Relationships: 여러 Entity 간을 연결하는 링크


Core Data Stack

Core Data Stack은 애플리케이션 객체와 외부 데이터 저장소 사이를 중재하는 다층 구조로 이루어진 프레임워크로 모델 레이어를 공동으로 지원하는 클래스를 제공한다.

  1. 관리객체 모델(NSManagedObjectModel)
    • .xcdatamodeld 파일을 프로그래밍적으로 표현한 형태로 Entity의 구조를 정의하는 객체.
    • 스키마의 Entity를 설명하는 NSEntityDescription을 포함하고 있다.
  2. 관리객체 컨텍스트(NSManagedObjectContext)
    • Core Data에서 객체를 메모리에 불러오는 역할을 한다.
    • 모든 관리객체는 관리객체 컨텍스트에 등록해야 하고 컨텍스트를 사용해서 object graph에 객체를 추가/삭제/수정한다.
    • context는 관리 객체로 작업하기 위한 in-memory 스크래치 패드이다. (스크래치 패드; 작업을 수행하는 동안 발생하는 중간 결과를 임시 보관하는데 사용하는 임시 기억 장소)
    • context를 이용해서 Core Data 개체에 관한 모든 일을 할 수 있어 가장 많이 상호작용하는 객체이다.
    • save() 를 통해 context를 호출하기 전까지는 변경사항이 디스크의 기본 데이터에 영향을 주지 않는다.
  3. 영구 저장소 코디네이터(NSPersistentStoreCoordinator)
    • 관리객체 컨텍스트와 영구 저장소 간의 브릿지 역할을 한다.
    • 코디네이터가 없는 컨텍스트는 영구 저장소에 접근할 수 없다.
    • 코디네이터는 작업을 직렬화해 진행하므로, 동시적으로 읽고 쓰려면 여러 코디네이터를 사용해야 한다. 그리고 여러 스레드가 코디네이터와 직접 작동하는 경우 명시적으로 잠그고 해제해야 한다.

      Core Data가 제공하는 영구 저장소 유형

      • SQLite Store : SQLite 데이터베이스에 의해 지원됨. Core Data가 즉시 지원하는 유일한 유형으로 가볍고 효율적인 메모리 공간을 제공함. 대부분의 iOS 프로젝트에서 가장 적합함. Xcode의 Core Data 템플릿은 기본적으로 이 저장소 유형을 사용함
      • XML Store : XML 파일로 지원되므로 모든 저장소 유형 중 사람이 가장 읽기 쉬움. 읽고 쓰는 작업 수행 전에 영구 저장소를 완전히 역직렬화한 후 메모리에 로드해야 하므로 공간 차지가 클 수 있음. OS X 에서만 사용 가능함
      • Binary Store : 바이너리 데이터 파일로 지원됨. 전체 바이너리 파일을 메모리에 로드해야 작업을 수행할 수 있음. 실제 애플리케이션에서는 이러한 유형의 영구 저장소를 거의 찾을 수 없음.
      • In Memory Store : 메모리 내 영구 저장소 유형이지만 어떤 면에서는 실제로 영구적이지 않음. 앱을 종료하거나 휴대폰을 끄면 인메모리 저장소에 저장된 데이터가 허공으로 사라짐. 단위 테스트 및 일부 유형의 캐시 작업에 도움이 됨.
  4. 영구 컨테이너(NSPersistentContainer)

    • Core Data Stack을 캡슐화해서 관리하는데, 스택의 네가지 요소를 각각 구성하는 대신 컨테이너를 통해 한번에 관리되도록 한다.

      class CoreDataStack {
      private let modelName: String
      
      init(modelName: String) {
        self.modelName = modelName
      }
      
      private lazy var storeContainer: NSPersistentContainer = {
      
        let container = NSPersistentContainer(name: self.modelName)
        container.loadPersistentStores { _, error in
          if let error = error as NSError? {
            print("Unresolved error \(error), \(error.userInfo)")
          }
        }
        return container
      }()
      }
      


Core Data 사용법 맛보기

기본 설정

Core Data Stack이 생성될 때 NSPersistentStoreCoordinator가 가장 먼저 인스턴스화 된다. 하지만 인스턴스화 하기 위해서는 데이터 스키마가 어떻게 생겼는지 알아야 하므로 NSManagedObjectModel이 필요하다. 그 이후 NSManagedObjectContext가 초기화되고, 이 관리객체 컨텍스트는 영구 저장소 코디네이터에 대한 참조를 유지한다.

// MARK: - Core Data stack 
private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
    let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)

    let fileManager = FileManager.default
    let storeName = "\(self.modelName).sqlite"

    let documentsDirectoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]

    let persistentStoreURL = documentsDirectoryURL.appendingPathComponent(storeName)
}()

Core Data Stack의 설정이 완료되면 애플리케이션은 프레임워크를 통해 영구 저장소와 상호작용 할 준비를 마친다. 주로 관리객체 컨텍스트를 통해 영구 저장소 코디네이터와 상호작용 한다.

저장하기

func save(name: String) {

  guard let appDelegate =
    UIApplication.shared.delegate as? AppDelegate else {
    return
  }

  // 1
  let managedContext =
    appDelegate.persistentContainer.viewContext
  // 2
  let entity =
    NSEntityDescription.entity(forEntityName: "Person",
                               in: managedContext)!
  // 3
  let person = NSManagedObject(entity: entity,
                               insertInto: managedContext)

  person.setValue(name, forKeyPath: "name")

  // 4
  do {
    try managedContext.save()
    people.append(person)
  } catch let error as NSError {
    print("Could not save. \(error), \(error.userInfo)")
  }
}
  1. 영구 컨테이너를 통해 관리객체 컨텍스트 참조를 가져온다.
  2. 저장할 Entity 인스턴스를 생성한다
  3. 새 관리 객체를 만들어 관리 객체 컨텍스트에 삽입한다.
  4. 관리객체 컨텍스트를 호출해 save()로 변경사항을 커밋하고 디스크에 저장한다.

가져오기

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)

  guard let appDelegate =
    UIApplication.shared.delegate as? AppDelegate else {
      return
  }

  //1
  let managedContext =
    appDelegate.persistentContainer.viewContext

  //2
  let fetchRequest =
    NSFetchRequest<NSManagedObject>(entityName: "Person")

  //3
  do {
    people = try managedContext.fetch(fetchRequest)
  } catch let error as NSError {
    print("Could not fetch. \(error), \(error.userInfo)")
  }
}
  1. 영구 컨테이너를 통해 관리객체 컨텍스트 참조를 가져온다.
  2. 가져올 Entity 이름을 지정하고 NSFetchRequest를 통해 데이터를 가지고 올 요청 객체 인스턴스를 생성한다.
  3. 컨텍스트를 통해 fetch 작업을 수행하면, 컨텍스트는 기준을 충족하는 관리 객체 배열을 반환한다.


사용 시 주의사항

  1. 다중 스레드 환경에서 작동하도록 설계되었으나, 모든 객체가 thread-safe 한 것은 아니다.
    • 데이터 처리는 CPU 집약적이어서 UI가 응답하지 않을 수 있으므로 사용자와 관련 없는 작업은 메인 큐에서 사용하지 않도록 한다.
    • 데이터가 손상되고 앱이 종료될 수 있으므로 큐 간에 NSManagedObject 인스턴스를 전달하지 않도록 한다.
      • 큐 간에 객체 참조를 전달해야 하는 경우 NSManagedObjectID를 사용한다.
  2. 완전한 in-memory 형태로 사용이 가능해 명시적으로 저장 명령(save)을 내릴 때까지 디스크에 저장하지 않는다.


사용 여부 결정시 고려할 사항

  1. ORM은 SQL문을 직접 작성하는 것보다 더 많은 쿼리문이 필요하므로 더 많은 CPU 사이클과 디스크를 소비한다.
  2. 객체간 관계 설정, 매핑하는 과정에 러닝커브가 있다.
  3. Xcode가 지원하는 모델 편집기를 이용할 수 있다.
  4. CloudKit을 이용해 기기간 연동이 가능하다.


References