오늘은 직접 코어데이터를 앱에 적용해보면서 코어데이터가 어떻게 작동하는지 알아보도록 하자.
보면서 따라하는 용도보다는 각 과정을 왜 그렇게 하는건지, 각 객체의 역할은 무엇인지가 궁금해 공부한 내용이다보니 중간중간 내용이 많을 예정이다!
(Raywenderlich의 Core Data with SwiftUI Tutorial: Getting Started를 바탕으로 글을 작성하였다.)
Core Data를 사용하기 위해 제일 먼저 해야할 일은 객체들의 구조를 정의하기 위해, 즉 객체 그래프를 추가하기 위해서 data model 파일(.xcdatamodeld
파일)을 만드는 것이다.
새 프로젝트를 만들 때 추가하는 방법과 이미 진행중인 프로젝트에 추가하는 방법 2가지가 있다.
.xcdatamodeld
파일 추가하기프로젝트를 만들 때, Use Core Data
체크박스에 체크를 해주면 자동으로 데이터 모델 파일을 추가해준다.
프로젝트 내비게이터에서 바로 확인할 수 있다.
새로운 프로젝트를 생성할 때 체크를 하면 코어데이터에 필요한 코드들을 자동으로 생성해주기 때문에 더욱 편리하다.
새로운 파일을 만들어 Core Data 섹션에 있는 Data Model 파일을 추가해준다. 그러면 끝!
데이터 모델은 앱의 객체와 그 객체들이 어떻게 서로 관련되어 있는지를 나타내는 객체 그래프에 대한 정보를 담고 있는 것이다.
객체 그래프에 대한 정보를 우리는 .xcdatamodeld
파일에 Entity와 Attributes, Relationships, Fetched Properties를 추가해 알려줄 것이다.
Entity
와 Attributes
, Relationships
, Fetched Properties
는 쉽게 말하면 모델 계층의 객체(class)와 프로퍼티라고 볼 수 있다.
예를 들어 코어데이터를 사용하지 않던 앱에서 아래와 같은 모델 타입이 있었다면
struct Movie {
let genre: String
let releaseDate: Date
let title: String
}
코어데이터를 사용하는 앱에서는 위 타입은 제거하고, 아래와 같이 data model editor에서 Entity와 Attributes를 추가해 사용해준다.
즉, 우리의 모델 계층의 객체 그래프를 데이터모델 파일에 추가해주는 과정이라고 보면된다.
이 객체들이 UI의 source of data가 되는 것!
그런데 밑에 있는 Relationships와 Fetched Properties는 무엇일까?
Attributes와 Relationships, Fetched Properties는 모두 Entity의 프로퍼티(코어데이터 프레임워크가 관리하는 객체의 값)다. 어떤 차이가 있을까?
Family
와 Person
Entity 사이의 관계를 Relationships에서 정의해줘야 한다.Family
가 Person
을 배열로 갖고 있으므로 Relationships를 하나 추가해주고 이름과 Destination을 정의해준다.Person
을 배열로 여러개 가질 거기 때문에 Data Model Inspector에서 Type을 To Many로 바꿔준다. 끝!Data Model Inspectector를 보면 Codegen
이라는 설정칸이 있다.
코어데이터와 관련한 코드를 자동으로 생성할지, 수동으로 만들어줄지를 Codegen
을 통해 결정할 수 있다.
Manul/None
Editor-Create NSManagedObject Subclass을 하면 xcode will generate the class files for what you’ve defined in the core data model.
하나의 Entity에 대해 CoreDataClass파일과 CoreDataProperties파일을 만든다.
NSManagedObject
의 서브클래스다. 코어데이터가 라이프사이클과 persistence를 다루기 때문에 managed object다.fetchRequest()
라는 메소드도 있음.Class Definition
기본 설정인 Class Definition은 위의 두 파일을 자동으로 만들어 알아서 관리하기 때문에 개발자가 해당 파일을 볼 수 없어 커스텀 코드를 추가할 수 없다.
Category/Extension
CoreDataClass 파일만 만들어주고 CoreDataProperties 파일은 Xcode가 자동으로 관리해준다.
이후 Core Data Persistent Container에 대한 참조를 얻는다. persistent container로부터 managed object context를 얻는다. managed object context를 통해서 객체를 생성하고 코어데이터에 저장할 수 있다.
data model을 만든 뒤에는 앱의 모델 레이어를 협력해서 도와주는 클래스들을 추가해줘야한다.
이러한 클래스들을 Core Data Stack이라고 한다.
앱의 모델 레이어를 협력해서 서포트하는 객체들로 Core Data를 사용하기 위해서는 Core Data Stack이 반드시 필요하다!
Core Data Stack은 아래와 같이 구성되어 있는데 각각이 하는 역할을 이해하는 것이 중요하다.
NSManagedObjectModel
Entities라고 불리는 모델 객체와 다른 Entity들과의 관계를 정의한다.
데이터 모델을 로드하고 Core Data Stack에 노출한다.
FaveFlicks 앱에서는 FaveFlicks.xcdatamodeld 안에서 Movie
entity를 managed object model의 부분으로 정의할 것이다. NSManagedObjectModel 클래스를 코드의 managed object model에 접근하는데 사용할거다.
NSManagedObjectContext
데이터베이스에 있는 객체를 보고 접근하게 해주는 window
Core Data Stack의 핵심. managed object context로 Core Data Stack과 소통하기 때문에 이 객체를 개발자가 가장 많이 사용한다. managed object model이나 persistent store coordinator는 직접 사용하는 경우는 많지 않다.
앱의 타입의 인스턴스에 생긴 변화를 트랙한다. 항목을 생성, 수정, 삭제 또는 검색할 수 있는 in-memory 스크래치패드인 NSManagedObjectContext. 일반적으로 Core Data와 상호 작용할 때 managed object context로 작업한다.
NSPersistentStoreCoordinator
Core Data Stack의 접착제같은 존재다. managed obejct model과 managed object context에 대한 참조를 유지한다.
managed object model을 통해서 데이터 모델을 이해하고, persistent store를 관리한다.
실제 데이터베이스를 관리해 store로부터 앱의 타입의 인스턴스들을 저장하고 fetch한다.
⇒ 복잡해보이지만 NSPersistentContainer
덕분에 아주 쉽게 Core Data Stack을 추가할 수 있다.
NSPersistentContainer
NSPersistentContainer
클래스가 NSManagedObjectModel
, NSManagedObjectContext
, NSPersistentStoreCoordinator
를 프로퍼티로 가지고 있다. managed object model, persistent store coordinator, managed object context의 생성을 핸들링해서 Core Data Stack의 생성과 관리를 간단하게 해준다.Core Data는 내부적으로 SQLite를 사용하는 Object-Oriented Database다.
Object-Oriented Database라고 하는 이유는, 데이터베이스에 직접 CRUD를 명령하는 일반적인 데이터베이스와 달리 코어 데이터는 데이터베이스와 직접 소통하지 않는다.
객체를 만들고, 앱의 객체와 상호작용하면 코어데이터가 알아서 관리해주는 개념인 것.
여기서 코어 데이터의 데이터베이스는 NSPersistentContainer
, 그 위에서 데이터베이스에 접근하게 해주는 window가 NSManagedObjectContext
다.
데이터를 persistent store에 저장하거나 가져올 때 이 context를 반드시 거쳐야 한다.
그럼 이제 본격적으로 Core Data Stack을 추가해보자.
먼저, persistent container를 만들어줄거다. 위에서 말했던 것처럼 Core Data Stack은 복잡해보이지만, persistent container가 캡슐화를 하고 있어 persistent container만 만들어주면 모두 관리해줄 수 있다.
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "FaveFlicks")
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
}
FaveFlicks
라는 이름의 contatiner를 만들어준다..xcdatamodeld
의 파일 이름과 container의 이름이 같아야 한다.⚠️ UIKit 앱과 다른 점
container
를 lazy 변수로 만든다.끝이다! Core Data Stack을 set up하는 건 이렇게만 하면 된다.
// PersistenceController
func saveContext() {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
context.hasChanges
를 사용해 화가 있을때만 저장한다.struct CoreDataPracticeApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
context를 Environment를 통해 모든 뷰에 전해준다.
// ContentView
@Environment(\.managedObjectContext) var managedObjectContext
managedObjectContext에 @Environment를 통해 접근한다.
// ContentView
func saveContext() {
do {
try managedObjectContext.save()
} catch {
print("Error saving managed object context: \(error)")
}
}
Entity를 생성하고 업데이트, 삭제할 때 우리는 이 작업을 모두 managed object context를 통해서 수행한다.
변경 사항이 생긴 걸 디스크에 쓰려면 컨텍스트를 저장해야 하므로 항상 managedObjectContext.save()
를 실행해줘야 한다.
@FetchRequest(
entity: Movie.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Movie.title, ascending: true)
],
predicate: NSPredicate(format: "genre contains 'Action'")
) var movies: FetchedResults<Movie>
데이터를 fetch해서 화면에 보여주기 위해서는 FetchRequest
를 통해 persistent store에서 fetch해온다.
@FetchRequest를 통해 데이터를 fetch해오는 경우, 해당 데이터에 변경이 생길 때마다 계속해서 fetch해오기 때문에 데이터와 UI 사이의 싱크를 맞추는데 최고의 도구다!
어떤 Entity를 fetch해올지, 어떤 순서로 정렬할지 등의 정보를 프로퍼티 래퍼 내부에서 정해준다.
위 코드의 경우에는 코어데이터에 저장되어있는 모든 Movie
를 가져올텐데 만약 객체를 filter하고 싶거나 특정 entity만 가져오고 싶은 경우엔 프로퍼티 래퍼 안에서 predicate 매개변수를 추가해 사용한다. 특정 연도의 '영화'만 가져오거나 특정 장르와 일치하는 것과 같이 결과를 제한하기 위해 predicate로 fetched request를 구성한다.
predicate의 경우, 문법이 정해져있기 때문에 어떤 정보를 filter해서 가져올지 정해진 문법에 따라 format
을 작성해줘야 한다.
// ContentView
func addMovie(title: String, genre: String, releaseDate: Date) {
let newMovie = Movie(context: managedObjectContext)
newMovie.title = title
newMovie.genre = genre
newMovie.releaseDate = releaseDate
saveContext()
}
func deleteMovie(at offsets: IndexSet) {
offsets.forEach { index in
let movie = self.movies[index]
self.managedObjectContext.delete(movie)
}
saveContext()
}
추가하고 삭제하는 메소드도 구현해준다!
코어 데이터를 적용해보는 방법을 알아보았다.
처음 공부하기 시작할 때 러닝 커브가 있다는 이야기를 들었는데
하루만에 뚝딱 쓸 줄 알았던 글이 며칠이 걸렸다..ㅎ
정말 러닝커브가 있구나 하하
앱에서 직접 써보면서 더 공부해봐야지!
참고
https://www.raywenderlich.com/9335365-core-data-with-swiftui-tutorial-getting-started
https://cocoacasts.com/what-are-core-data-entities-and-attributes
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/index.html#//apple_ref/doc/uid/TP40001075-CH2-SW1
https://cocoacasts.com/what-is-the-core-data-stack