Core Data ? CloudKit ? 그거 누가 쓰는데 ? 현업에서 아무도 안쓴다고 하던데 ?
아마 많은 분들이 들어보셨을 이야기라고 생각합니다. 그리고 일부 맞는 이야기이기도 합니다. 내부 DB로는 Realm을 사용하거나 fmdb 혹은 SQLite.swift 와 같은 SQL wrapper 를 많이 사용하기도 하고 동기화가 필요한 경우에는 Firebase를 활용해서 기능을 확장 하기도 합니다.
하지만 1st-party 프레임워크를 사용하면서 누릴 수 있는 공식지원이라는 안정적인 개발환경이나 간단한 코드로 iOS는 물론이고 iPadOS, MacOS, watchOS 까지 폭넓은 애플 생태계에서 동시에 개발이 가능하다는 장점은 다른 어떤 프레임워크에서도 찾을 수 없는 독보적인 기능입니다. 특히 생산성앱 분야에서는 인디 개발자는 물론 이미 많이 성장한 회사들도 동기화 기능으로 iCloud를 사용하는 경우가 많이 있습니다. 대표적으로 마크다운 기반의 노트앱 bear 이나, iPad 필기앱 최강자 goodnotes 역시 iCloud와 Core Data를 적극적으로 활용하고 있습니다.
우리가 기획하는 모든 앱들의 백엔드를 CloudKit이나 Core Data가 대체할 수 있다는 이야기는 절대 아닙니다. 당장 아래에서 설명할 때도 적합하지 않은 경우들에 대해서 이야기 할 예정이고요. 하지만 반대로 이 기술이 힘을 발휘하는 상황도 확실히 있기 때문에, 오늘 잠시 시간을 투자해서 이 기술의 특징이나 장점들을 알아둔다면 우리가 해결할 수 있는 문제의 폭이 더 넓어질 것입니다.
그럼 이제 하나씩 주제를 들어가보도록 할까요?
이번 글은 Overview 이기 때문에 빠르게 개념만 짚고 넘어가보려고 합니다 🙂
Persist or cache data on a single device, or sync data to multiple devices with CloudKit.
주로 데이터베이스처럼 쓰고 있기는 하지만, 사실 Core Data는 Obejct Graph를 관리하기 위한 애플의 1st party 프레임워크입니다. Object Graph라는 이름처럼 Object사이의 관계를 관리할 필요가 있는 다양한 곳에서 다양한 모습으로 사용이 가능합니다.
위에 인용한 Core Data 공식 문서의 한줄 설명에도 나와 있는것처럼 Core Data는 한 기기에서의 캐시를 관리하는데도 사용이되고, sqlite를 사용해서 데이터베이스처럼 사용하기도 합니다. 그리고 오늘의 주제인 CloudKit을 활용해서 여러개의 기기에서의 데이터 동기화에도 사용할 수 있습니다.
Core Data는 SQLite라고 들었는데…
맞기도 하고, 틀리기도 합니다. Core Data가 제일 많이 사용되는 사례가 디바이스 내부에 앱의 데이터를 저장하는 것이고, Core Data에서 디바이스에 Object Graph를 저장하기 위해서 사용하는 방법중 하나가 SQLite이기 때문입니다. 저장을 하는 방법을 sqlite를 사용하고, 용어들도 비슷한 부분이 많기 때문에 RDB 특히 SQL에 익숙하신 분들은 Core Data에 쉽게 적응하실 수 있을것입니다. 하지만 처음에도 설명한 것 처럼 Core Data의 주된 기능은 Object Graph를 관리하는 일이고, 그 기능을 구현하기 위해서 sqlite를 사용하는 것이기 때문에 SQL의 기능을 쉽게 사용하는 것이 목적이라면 Core Data가 적절한 선택이 아닐 수도 있습니다.
한가지 예로, 대량의 데이터를 다룰 때 퍼포먼스 이슈가 생길 수 있습니다. Core Data는 Object Graph를 메모리에 올려놓고 사용합니다. Object를 지우거나 수정하는 등의 작업을 하려고 하면 우선 메모리에 불러와야 합니다. 만약 수천개의 Object를 한번에 지우거나 하는 등의 작업을 빈번하게 해야 한다면 우선 그 모든 Object를 메모리에 먼저 올려야 하기 때문에 퍼포먼스 이슈가 생길 수 있습니다.
우리 앱이 여러가지 관계들이 표현되어야 하는 Object들을 다루고 있다면 Core Data를 고려해볼 수 있습니다. 특히 아래에서 다룰 CloudKit과 함께 사용 되면 다양한 분야에서 개발자에게 꽤나 강력한 기능들을 제공해줍니다. 하지만 단순하게 SQL의 wrapper가 필요한 경우라면 시작에 언급되었던 fmdb 혹은 SQLite.swift 가 더 좋은 옵션이 될 수 있습니다. 특히 Core Data에는 JOIN 테이블을 만드는 쿼리가 존재하지 않기 때문에 이를 고려한 데이터 모델링이 필요한 경우에는 다른 옵션들을 선택해야 합니다.
Core Data framework 의 Core Data Stack은 우리 App에서 Model 레이어를 담당하기 위해 설계 되었습니다. 즉, MVVM, MVC, MVP 등 Model 레이어를 포함하는 여러가지 디자인 패턴에서 Model의 역할을 담당할 수 있다는 이야기입니다. Core Data Stack은 아래의 그림과 같이 도식화될 수 있습니다.
이제 각각의 객체들이 어떤 역할을 하고, 어떻게 Core Data Stack이 동작하는지 알아보도록 합시다
NSManagedObjectModel
은 앱에서 사용되는 object type(Entity), 가질 수 있는 속성(Attribute), 관계(Relation)를 나타내는데 사용됩니다. 이 개념 자체는 SQL의 스키마와 비슷해 보이지만 Core Data Stack에서 sqlite를 사용하는 경우에만 스키마를 사용합니다. 반복해서 이야기 하지만 Core Data는 데이터베이스가 아니기 때문에 sqlite 이외에도 많은 종류의 영구적 저장소를 사용합니다.
Store Coordinator는 NSManagedObjectModel
과 실제 데이터가 읽고 써지는 NSPersistentStore
의 연결고리 역할을 합니다. NSManagedObjectModel
을 이용해서 Entity를 만들고, 이미 존재한다면 NSPersistentStore
로부터 데이터를 로드합니다.
NSPersistentStoreCoordinator
에서는 실제 데이터에 대한 입출력이 이뤄지게 되며, NSManagedObjectContext
의 요청에 대한 응답 등의 작업을 수행합니다.
참고로, Core Data에서 NSPersistentStore
는 기본적으로 4가지 유형의 Store를 제공합니다.
NSXMLStoreType
과 동일한 atomic 저장소로 바이너리 파일로 지원됩니다. 실제로 코드를 작성하게 되면 제일 많이 마주치게 되는 객체입니다. 앱의 타입들에 대한 변화들을 추적하여 관리 중인 데이터들에 대한 Seamless한 접근을 지원합니다. Context를 통해서 NSPersistentStoreCoordinator
에게 데이터의 CRUD를 요청할 수 있습니다. NSManagedContext
는 개발하면서 제일 많이 사용되는 클래스이기 때문에 간단한 예시 코드와 함께 몇가지 정보들을 남겨둡니다.
// from WWDC19 - Making Apps with Core Data
// (https://developer.apple.com/wwdc19/230)
context.perform {
let post = Post(context: context)
post.title = "Hello World"
try? context.save()
}
NSManagedObject
는 연결되어 있는 context 없이는 존재할 수 없습니다.context.save()
를 통해서 저장 해야지 변경사항이 저장됩니다.// from WWDC19 - Making Apps with Core Data
// (https://developer.apple.com/wwdc19/230)
let rawPostsData: Data = // Server response ...
if let postDicts: try? JSONSerialization.jsonObject(with: rawPostsData) as? [[String : Any]] {
context.perform {
let insertRequest = NSBatchInsertRequest(entity: Post.entity(), objects: postDicts)
let insertResult = try? context.excute(insertRequest) as! NSBatchInsertRequest
let success = insertResult.result as! Bool
}
}
NSPersistentContainer
는 iOS10 부터 지원하며, 위에서 다룬 모든 클래스들을 모두 묶어서 관리(오케스트레이션)하는 클래스입니다. 이 클래스를 통해서 각각의 클래스를 초기화 시키는 것보다 Core Data를 훨씬 편하게 사용할 수 있게 해줍니다.
Store structured app and user data in iCloud containers that all users of your app can share
설명에도 나와 있는 것처럼 CloudKit은 우리 앱을 사용하는 사용자들이 각종 데이터를 저장할 수 있도록 고안되었습니다. 따라서 Core Data와의 연동 이외에도 단순한 파일의 저장이나, Core Data 연동 없이 iCloud container에서 구조화된 데이터를 가져오는 것도 가능합니다. 하지만 이번에는 Core Data와의 연동에만 집중해서 공부해볼 예정입니다. Core Data 연동 이외의 것이 궁금하시다면 공식문서를 참조해주세요.
CloudKit과 Core Data는 데이터를 다루는 방식이 꽤나 유사합니다. 그렇기 때문에 Core Data의 개념이 CloudKit에서 어떻게 사용되는지 정리를 한다면 앞으로 학습에 도움이 될 것입니다.
Image from “Sync a Core Data store with CloudKit public database“ in WWDC20
CloudKit을 활용해서 Core Data의 기능을 확장하는 경우는 크게 3가지로 나눌 수 있습니다.
이 3가지가 현재 CloudKit에서 제공하는 3가지 형태의 데이터베이스입니다. 이 세가지에 대해서 각각 어떤 개념이 있고 어떻게 구현이 되는지 알아보도록 하겠습니다.
만약 우리가 Core Data Stack을 사용해서 앱을 만들었다면 대부분의 경우 여러분들의 앱은 이미 CloudKit의 Private Database를 통한 기기간 동기화의 준비를 마친 상태일 것입니다.
상기한 바와 같이 CloudKit과 Core Data는 데이터를 다루는 방식이 유사하기 때문에 매우 간단한 절차를 통해서 기능을 추가할 수 있습니다.
Core Data Stack의 Container를 NSPersistentCloudKitContainer
로 교체하기
// Core Data만 사용하는 경우 Core Data Stack을 만드는 코드
container = NSPersistentContainer(name: "CoreData")
// Class 이름에 CloudKit만 추가해주면 1단계 완료!
container = NSPersistentCloudKitContainer(name: "CoreData")
프로젝트의 Capability 추가하기
끝! 이제 서로 다른 기기에서 앱을 실행하면 서로 동기화가 시작됩니다.
이렇게 엄청 간단하기 모든 애플 기기와의 동기화 작업이 끝났습니다! 바로 다음으로 넘어가보죠
여기서부터는 조금 까다로운 상황들이 발생합니다. Public Database 의 대표적인 Use case들은 다음과 같습니다.
이러한 데이터들은 필요에 따라 앱의 모든 사용자들이 로그인 여부와 상관없이 보여지고 싶은 경우도 있고, 권한에 대한 설정들도 조금 복잡해집니다.
그래서 CloudKit에서 Database를 다루는 방식이 .private 과 .public 이 서로 다릅니다.
그리고 이 차이는 데이터를 읽고 쓰는 권한의 영역에서는 더 복잡해집니다
위의 이미지는 Private Database에서의 로그인 여부에 따른 권한에 대한 도표입니다. 굉장히 심플하죠?
반면 Public Database의 경우는 훨씬 복잡합니다. 특히 Modify 부분의 수정 및 삭제는 Public이라는 특성 때문에 단순하게 접근하기 매우 힘듭니다. 특히 하나의 Entity가 Public 과 Private 모두에서 관리되는 중이라면 더 복잡해지게 됩니다.
한가지 예시로, 이 화면에서는 Post들의 목록이 표시됩니다. 그리고 이 Post들은 Private / Public 모두에서 관리하고 있어서 저 화면만으로는 어떤 Post가 어떤 데이터베이스에서 관리되고 있는지 알 수 없기 때문에 Public Database에서 관리되고 있는 레코드를 Edit 버튼으로 수정하려고 할 때 문제가 발생할 수 있습니다.
이렇듯 Public Database를 사용할 때는 그 구조적인 차이로 인해서 Private Database를 사용하는 것에 비해서 여러가지 제약사항들을 가지게 됩니다.
특히 1번의 제약사항 때문에 CloudKit 도입의 가능성이 있다면 CoreData를 설계할 단계에서부터 오브젝트를 잘 정의해야 합니다.
이제 실제 구현을 해보도록 하겠습니다. 가상의 앱을 통해서 한번 실습을 해보죠.
💡 SmallTalk 앱은 시덥지 않은 일들을 기록하는 어플입니다. 이 시덥지 않은 기록들은 iCloud를 통해 내가 소유한 모든 기기에서
굳이동기화가 됩니다. 이제 우리는 새로운 탭에 공지사항 기능을 추가하려고 합니다. 놀랍게도 이 어플의 공지사항은 iCloud 계정을 가진 모두가 작성할 수 있습니다
아래 작업은 이 리포지토리의 cloudkit-public-start 브랜치 로 직접 실습해보실 수 있습니다.
- 등록비를 지불한 개발자 계정에서만 동작 확인이 가능합니다
- 예시는 예시일 뿐 모든 상황에 동일하게 동작하지 않습니다. 단계별로 따라하기 보다는 각각의 단계가 어떤 작업을 하는지 아는 것이 훨씬 더 중요합니다.
우선 Public Database에서 관리할 새로운 오브젝트를 만들어야 합니다. - Notice **Entity를 추가했습니다.
CloudKit은 기본 설정으로 모든 오브젝트들이 하나의 Database에서 관리되도록 하고 있습니다. 지금 시나리오는 새로운 오브젝트를 만들고 그 오브젝트는 Public으로만 관리가 되어야 하기 때문에 새로운 Configuration을 만들어줘야 합니다. 같은 화면 CONFIGURATIONS Section에서 작업을 합니다.
PersistentController
의 초기화 함수를 수정합니다 recordName
, modifiedTimeStamp
이 두가지에 대해서 Queryable을 추가하고, modifiedTimeStamp에는 Sortable을 추가한 index를 생성해야 합니다.Making Apps with Core Data - WWDC19 - Videos - Apple Developer
What's new in CloudKit - WWDC21 - Videos - Apple Developer
Sync a Core Data store with the CloudKit public database - WWDC20 - Videos - Apple Developer
Build apps that share data through CloudKit and Core Data - WWDC21 - Videos - Apple Developer