boostcamp-2020 / Project18-B-iOS-BoostRunClub

사용자의 러닝을 측정 및 기록하여 저장하고, 활동 내역을 정리하여 보여주는 iOS 애플리케이션 입니다. 🏃‍♀️🏃‍♂️🏃
MIT License
55 stars 10 forks source link

CoreData 조사 #75

Closed seoulboy closed 3 years ago

seoulboy commented 3 years ago

의논거리 🤔

seoulboy commented 3 years ago
Core Data는 데이터베이스다.
Core Data는 데이터베이스가 아니다.
Core Data는 SQLite다.

???

언제나 그렇듯 처음에 보면 언뜻 이해가 잘 가지 않는, 서론에 적혀있는 추상적인 정의를 읽어보았다.

Core Data is an object graph management and persistence framework in the mac OS and iOS SDKs.

That means Core Data can store and retrieve data, but it isn't a relational database like MySQL or SQLite. Although it can use the SQLite as the data store behind the scenes, you don't think about Core Data in terms of tables and rows and primary keys.

그러니까 Core Data는 데이터를 저장하고 가져올 수 있는데, MySQL 이나 SQLite 같은 데이터베이스는 아니다. SQLite를 data store로 사용할 수 있는데, Core Data는 테이블과 열과 프라이머리 키로 이루어진 구조는 아니다.

... 라는데.

그러니까 Core Data는 데이터를 저장하고 가져올 수 있는 저장공간이면서 table, row, primary key 이러한 개념이 아닌 다른 개념으로 구성되어있고, SQLite로 조작이 가능한 친구라는 것 같다.

계속해서 읽어보자.

먼저 managed object model 이 필요하다. Data Model Editor을 통해 만들 수 있다.

Data Model 파일인 .xcdatamodel은 File > New File > Data Model 검색 -> Create 를 통해 파일을 생성할 수 있다.

👆 Xcode의 Data Model editor. Managed Object Model을 생성하고 수정할 수 있다.

Add Entity 를 통해 생성하고, + 버튼을 통해 attributes를 생성할 수 있다. Attributes는 기본적인 타입 설정이 가능하다. Custom type을 적용하고 싶으면 transformable 이라는 것을 사용해야한다. - transformable 에 대한 얘기는 잠시 미뤄두겠다.

Person 이라는 entity를 생성하고, name 이라는 String 타입의 attribute 를 추가해주었다.

데이터 저장하기

뷰컨에 var people = [String]() <- 배열의 내용에 따라 cell의 내용이 채워지는 테이블 뷰가 있다고 가정하자. 각각의 배열의 요소에는 사람들의 이름이 담겨있다.

배열에 이름을 추가할 때마다 테이블 뷰에 이름이 표시되고 있다. 그러나 앱을 완전히 종료하고 다시 열면 데이터가 사라져있다.

코어데이터를 사용해서 저장하는 법을 간략하게 알아보자

우선 배열을 다음과 같이 NSManagedObject 변경시켜줘야한다.

var people = [NSManagedObject]()

NSManagedObject는 CoreData에 저장된 하나의 오브젝트이다. 이것이 있어야만 CRUD 작업을 할 수 있다. NSManagedObject 는 어떤 entity도 될 수 있다.

NSManagedObject에는 value(forKeyPath: ) 라는 메서드가 있는데, 이 메서드를 통해 attribute의 값을 가져올 수 있다.

extension ViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView,
                 numberOfRowsInSection section: Int) -> Int {
    return people.count
  }

  func tableView(_ tableView: UITableView,
                 cellForRowAt indexPath: IndexPath)
                 -> UITableViewCell {

    let person = people[indexPath.row]
    let cell =
      tableView.dequeueReusableCell(withIdentifier: "Cell",
                                    for: indexPath)
    cell.textLabel?.text =
      person.value(forKeyPath: "name") as? String // Person entity의 "name" attribute에 저장된 값에 접근하는 부분
    return cell
  }
}

NSManagedObject 는 위에 Data Model에서 입력한 Person entity의 name attribute에 대해 모르기 때문에, Key-value Coding (KVC)을 통해 String 으로 직접 입력해주어야 한다. - 이는 Managed Object 를 SubClass해서 클래스에 속성에 접근하듯이 사용할 수도 있다. - Managed Object Subclass 에 대한 얘기도 잠시 미뤄두겠다.

자 이제 드디어 코어데이터에 저장하는 로직이 담겨있는 부분이다.

아래 함수는 이름을 입력한 후에 매개변수로 입력한 이름이 문자열로 받으면서 호출이된다는 상상을 해보자.

func save(name: String) {

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

  // 2
  let managedContext =
    appDelegate.persistentContainer.viewContext

  // 3
  let entity =
    NSEntityDescription.entity(forEntityName: "Person",
                               in: managedContext)!
  // 4
  let person = NSManagedObject(entity: entity,
                               insertInto: managedContext)

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

  // 6
  do {
    try managedContext.save()
    people.append(person)
  } catch let error as NSError {
    print("Could not save. \(error), \(error.userInfo)")
  }
}
  1. AppDelegate 에 접근한다 - NSPersistentContainer 라는 친구가 그곳에 있는데, 그 친구를 통해서 managedContext를 가져올 수 있다. 코어 데이터에서 하는 대부분의 동작, 저장 및 불러오기는 이 context를 통해 이뤄지기 때문에 사용한다.
  2. persistentContainer.viewContext viewContext를 변수에 담는다. 이는 다음 몇줄의 코드에서 여러번 사용된다
  3. NSEntityDescription 은 Data Model에 있는 entity와 코드를 연결하는 연결고리이다. entity(forEntityName:, in:) 메서드를 사용해서 생성과 동시에 context에 넣어준다.
    • 저장 및 불러오기는 두 단계로 나눠진다. 1단계는 변경 사항을 혹은 수행할 작업을 context에 넣어주는 것이고, 2단계는 context에 입력된 작업을 commit 하는 것이다. 지금은 Person entity를 context에서 사용하고자 하는 차원에서 준비하는 단계이다.
  4. NSManagedObject(entity:, insertInto:) Person 이라는 managed Object를 생성하고 context에 추가해준다. - Entity는 클래스 정의이고 Managed Object는 해당 클래스의 인스턴스라고 생각하면 된다.
  5. 위에서 생성한 managed object의 name attribute에 값을 설정한다. 값은 입력받은 매개변수 name을 할당한다.
  6. 위에서 말한 저장 단계 중 1단계를 마치고 2단계를 실행하는 구문인데, context의 메서드는 throw할 수 있기때문에 do catch 문안에서 실행한다.
    • context에 변경사항을 저장하기 위해 save() 를 호출한다. 그리고 people 배열을 갱신해준다.

이렇게하면 CoreData에 값을 저장할 수 있다.

데이터 불러오기

저장한 값을 불러오는 방법도 빠르게 살펴보자. 저장한 데이터를 불러오는 방법은 방금 전 확인한 저장 방법과 유사하면서 조금 더 간단하다.

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

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

  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. 위에서 말했던 것처럼 코어데이터의 작업은 context를 통해 이뤄지므로 AppDelegate에 만들어준 persistentContainer 접근해서 context를 가져온다.
    • 이 작업은 파일의 여러곳에서 사용할 수 있으므로 생성자 안에서 혹은 viewDidLoad() 안에서 해주어도 좋다
  2. NSFetchRequest 제네릭 타입 클래스를 사용한다. 반환 타입은 NSManagedObject 이어야하므로 NSManagedObject 를 명시해주고, (entityName:) 메서드에는 불러올 entity의 이름을 넣어준다.
  3. 저장할때와 마찬가지로 불러오는 작업도 throw할 수 있기 때문에 do catch 문 안에서 실행한다.
    • context.fetch() 메서드에 위에서 생성한 fetch request를 담아서 실행한다. - 이렇게 하면 CoreData에 저장 되어있는 Person 들이 [NSManagedObject] 반환된다.

Notes

Debug log

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+entityForName: nil is not a legal NSManagedObjectContext parameter searching for entity name 'Person''

-> NSPersistentContainer 생성자에 들어가는 name의 매개변수와 xcdatamodel 파일명이 동일한지 확인한다.

Thread 1: "[<Person 0x600001d400a0> setValue:forUndefinedKey:]: the entity Person is not key value coding-compliant for the key \"name\"."

-> xcdatamodel 파일의 Person entity 의 attribute 명이 name과 동일한지 확인한다.

    func save(name: String) {

        // get reference to AppDelegate
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }

        // get reference to managedContext
        let managedContext = appDelegate.persistentContainer.viewContext

        // get link to entity and add it to context
        let entity = NSEntityDescription.entity(forEntityName: "Person", in: managedContext)!

        // add object to context
        let person = NSManagedObject(entity: entity, insertInto: managedContext)

        // set the name attribute value
        person.setValue(name, forKeyPath: "name")

        // commit changes  by calling context.save()
        do {
            try managedContext.save()
            people.append(person)
        } catch let error as NSError {
            print("\(error) \(error.userInfo)")
        }
    }

-> let person = NSManagedObject(...) 다음 줄에 있는 person.setValue() 메서드를 잊지말고 호출해주어서 value를 업데이트해주자.