본문 바로가기

MOBILE/ios

[iOS] core data를 이용하여 데이터를 영구 저장하기

core data

: application에서 모델 계층의 객체를 관리하는 데 사용하는 프레임워크

 

ios에서 데이터를 영구 저장하는 방법은 다양한데, 그 중에서도 core data는 애플이 코코아 개발환경을 통해 제공하는 In-Memory 방식의 데이터 관리 프레임워크이다. In-Memory 방식이므로 모든 데이터를 다루는 작업은 메모리에서 동작한다. (In-Memory 방식은 뒤에서 설명)

즉 읽고 쓰는 데이터들은 모두 메모리에 로드한 다음에 메모리에서 읽거나 쓰기 작업을 수행하고 그 결과를 저장소에 반영하는 것이다.

한마디로 영구 저장소에 직접 데이터를 쓰거나 읽는 일이 없다는 점이 SQLite와 다른 점이다. 그냥 개발자와 영구 저장소를 이어주는 프레임워크이다.

 

core data의 데이터 저장 구조

크게 4가지 요소로 이루어진다.

- 데이터 모델 파일

- 엔터티

- 어트리뷰트

- 릴레이션

 

데이터베이스로 따지면 데이터 모델 파일은 db파일, 엔터티는 테이블, 어트리뷰트는 칼럼, 릴레이션은 조인과 비슷하다.

하지만 core data는 데이터베이스와는 다르다.

core data는 영구 저장소로 바이너리 파일을 사용할 수도 있고, 아예 영구 저장소를 사용하지 않고 in-memory 방식으로만 사용하는 것도 가능하다. (물론 이 경우에 데이터는 휘발된다.)

 

core data의 내부 구조

core data의 내부 구조는 다음과 같다.

 


관리 객체 (Managed Object)

관리 객체는 core data에서 하나의 레코드를 저장하기 위한 인스턴스이다. core data는 모든 레코드를 객체로 다루기 때문에 테이블의 행 하나가 관리 객체 하나가 된다. 

관리 객체는 NSManagedObject 클래스의 인스턴스이며, 이러한 관리 객체는 모두 관리 객체 컨텍스트에 속하여 관리된다.

 

 


관리 객체 컨텍스트 (Managed Object Context)

관리 객체 컨텍스트는 크게 두 가지의 역할을 한다.

 

- 관리 객체를 담아서 CRUD 등의 연산을 수행한다.

즉 데이터를 읽고 쓰는 모든 작업은 이 관리 객체 컨텍스트를 통해서 처리된다.

core data는 이 관리 객체 컨텍스트를 통해서 각각의 관리 객체에 대한 CRUD 연산을 진행한다.

 

- 영구 저장소와 영구 저장소 코디네이터에 대한 관리자이다.

따라서 core data에 대한 참조를 획득하면 영구 저장소에 대한 접근 또한 간접적으로 얻게 된다.


영구 저장소 코디네이터

컨텍스트와 직접 데이터를 주고 받으며 다양한 영구 저장소들의 접근을 조정하고 실제로 입출력을 처리한다.

컨텍스트가 데이터를 코디네이터에 요청하면 영구 저장소 코디네이터는 영구 저장소에서 데이터를 찾아 컨텍스트 객체에 전달하여 메모리에 로드한다. 이때 데이터는 관리 객체 인스턴스가 된다.


관리 객체 모델 (Managed Object Model)

엔터티의 구조를 정의하는 객체이다. 즉 관리 객체의 데이터 구조에 대한 정보이다.

관리 객체가 실제 인스턴스라면 관리 객체 모델은 클래스 또는 인터페이스인 것이다.


영구 객체 저장소 (Persistant Object Store)

core data에서 데이터가 저장되는 공간이다. 이때 영구 저장소 타입은 4가지이다.

 

인메모리 저장소 타입 (NSInmemoryStoreType)

: 메모리 기반의 저장소이다. 즉 영구 저장소를 사용하지 않으므로 데이터는 보존되지 않고 휘발된다.

 

플랫 바이너리 저장소 타입 (NSBinaryStoreType)

: 데이터를 바이너리 파일 형식으로 저장한다.

데이터 조회 성능이 좋지만 데이터의 크기가 클수록 초기 로딩시간이 늘어난다.

 

XML 저장소 타입 (NSXMLStoreType)

: 데이터를 XML 파일 형식으로 저장한다.

이때 데이터는 모두 저장되거나, 저장되지 않는 원자성을 가진다.

처리속도가 느리지만 코드를 외부에서 열어서 확인이 가능하다.

 

SQLIte 데이터베이스 (NSSQLiteStoreType)

: 데이터 전부가 아닌 일부만 메모리에 로드하는 방식이다.

ios에서 core data가 기본적으로 채택하는 방식이다.

 

 

In-Memory DB

core data는 In-Memory 방식으로 동작하기 때문에 모든 데이터가 메모리에 로드 되고 난 다음에 사용가능하다고 말했다.

즉 데이터에 대한 작업은 메모리에 있는 데이터를 대상으로 하기 때문에 영구 저장소에는 데이터에 대한 작업이 이루어지지 않는다.

따라서 영구 저장소에 반영하는 메소드를 호출하여 동기화를 맞추어야 한다. 이를 commit 이라고 한다.

In-memory 방식은 처리속도가 빠르고 성능이 향상된다는 것이다. 매번 I/O 작업을 하는 것이 아니기 때문이다. 

 

영구 저장소와 메모리 사이의 동기화를 맞추는 작업은 mirroring이라고 한다. 이때 영구 저장소의 종류가 SQLite 인 경우 차등 저장 메커니즘을 활용하게 된다. 즉 데이터 저장 이후에 변경된 부분만 commit하기 때문에 가볍고 처리 속도가 빠르다.

 

이렇게 영구 저장소에 반영하기 위해 사용하는 method는 save() 이다.

 

 

How to use: 데이터 가져오기

entity를 정의하면 core data는 자동으로 NSManagedObject 클래스를 상속한 데이터 모델 클래스를 생성한다. 이는 프로젝트 파일 탐색기에서 보이지는 않는다. 하지만 이러한 모델 클래스는 다른 방식으로도 생성 가능하다.

 

- NSManagedObject 클래스를 상속하여 클래스를 새로 정의 (이때 entity의 Codegen 속성은 Manual/None 이어야 함)

- xcode의 editor에서 create NSManagedObject Subclass 를 이용한다.

- core data가 자동으로 생성하는 것을 사용한다. (Codegen의 속성이 Class Definition)

 

일단 core data로 데이터를 다루기 위해서는 관리 객체 컨텍스트에 대한 참조가 필요하다. 이 관리 객체 컨텍스트는 AppDelegate 객체의 NSPersistentContainer 객체의 viewContext 속성을 통해 참조 가능하다. 이 속성의 반환값이 NSManagedObjectContext 객체이다.

즉 다음과 같은 코드로 참조를 얻는다.

let appDel = UIApplication.shared.delegate as! AppDelegate
let context = appDel.persistentContainer.viewContext

 

이렇게 참조한 관리 객체 context에서 데이터를 가져올 때는 fetch() 메소드를 사용한다.

 

 

fetch() 메소드의 첫번째 파라미터는 NSFetchRequest 라고 하는 요청 객체이다.

 

NSFetchRequest

: core data 에서 데이터를 가져올 때의 요청 사항을 정의한 객체

어디서 데이터를 가져올 것인지에 대한 entity 정보, 어떤 데이터를 가져올 것인지에 대한 검색 조건, 어떤 순서로 가져올 것인지에 대한 정렬 조건 등 다양한 요청 사항을 속성을 통해 정의할 수 있다.

이 객체는 entity 이름을 생성자 파라미터에 넣어 초기화 할 수 있다.

 

이 객체를 fetch() 메소드의 파라미터로 넣어 전달하면 데이터를 가져올 수 있다.

다음과 같은 형식으로 진행된다.

let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext 

let fetchRq = NSFetchRequest<NSManagedObject>(entityName: "entityname")
        
let sort = NSSortDescriptor(key: "sortKey", ascending: false) 
fetchRq.sortDescriptors = [sort] // 배열 속성이므로 배열로 담아서 전달. 즉 정렬 기준이 여러개가 가능하다는 뜻
        
let result = try! context.fetch(fetchRq)
// result는 [NSManagedObject] 타입이 됨

 

이때 요청 객체의 sortDescriptor에는 정렬 기준을 설정할 수 있다. NSSortDescriptor 객체를 배열로 담아서 넘기게 된다.

 

 

How to use: 데이터 추가하기

core data에 데이터를 추가할 때도 관리 객체 context가 필요하다.

이때 중요한 점은, 관리 객체는 생성과 동시에 관리 객체 컨텍스트에 속해야 한다는 것이다.

 

따라서 다음과 같은 구문으로 관리 객체 컨텍스트에 관리 객체를 추가할 수 있다.

이때 NSEntityDescription은 core data model 파일에서 생성한 entity 그 자체에 대한 클래스이다.

 

let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext 

let data = NSEntityDescription.insertNewObject(forEntityName: "entityName", into: context)

 

 

이렇게 관리 객체 컨텍스트에 바로 관리 객체를 생성하여 추가하게 되면 반환되는 data는 NSManagedObject 즉 새로 생성된 관리 객체이다. 

 

이제 이 관리 객체에 setValue() 메소드로 key-value 형태의 값을 넣을 수 있다.

그 후 이를 영구 저장소에 반영하는 save() 메소드를 꼭 사용하여 동기화를 맞춰야 한다.

 

let appDel = UIApplication.shared.delegate as! AppDelegate
let context = appDel.persistentContainer.viewContext
        
let data = NSEntityDescription.insertNewObject(forEntityName: "entityName", into: context)
data.setValue("아무", forKey: "key1")
data.setValue("내용", forKey: "key2")
data.setValue("넣고 싶은 것", forKey: "key2")

do {
	try context.save()
} 
catch {
	context.rollback()
}

 

만약 영구 저장소와 메모리의 동기화에 실패했을 경우 변경 내역을 원래대로 되돌려야 한다.

이때 사용하는 것이 rollback() 메소드이다.

 

 

How to use: 데이터 삭제하기

데이터 삭제는 또한 in-memory 방식을 따른다.

데이터를 메모리에 가져와서 메모리의 데이터를 삭제하고 이를 다시 영구 저장소에 반영하는 방식이다.

 

이때 사용하는 메소드는 delete() 이다. 파라미터로는 삭제하고자 하는 NSManagedObject 객체를 전달한다.

 

 

다음과 같은 방식으로 진행된다.

 

let appDel = UIApplication.shared.delegate as! AppDelegate
let context = appDel.persistentContainer.viewContext
        
context.delete(object)

do {
	try context.save()
} 
catch {
	context.rollback()
}

 

여기서도 메모리의 삭제된 데이터를 영구 저장소에 반영하는 것이기 때문에 save() 메소드를 호출하여 동기화를 맞춘다.

 

 

core data를 잘 활용하자 xcode에서 제일 처음 프로젝트를 만들 때 같이 반입할 수도 있고 수동으로 반입할 수도 있다.

이때 수동으로 반입하게 되면 AppDelegate 파일에 추가 코드를 작성해야 한다. 이는 나중에 다시