CoreData는 앱을 통한 CRUD가 가능하도록 하는 프레임워크다. 앱에서 기기의 디스크에 데이터를 쓰고(Create), 읽고(Read), 수정하고(Update), 삭제(Delete)하도록 하는 것이다.
CoreData의 데이터 모델은 xcdatamodeld 파일로 생성된다. 이 파일은 프로젝트에서 Data Model 템플릿을 선택하여 생성할 수 있다.

Data Model 파일을 생성하고 나면 이런 화면이 나온다.

여기서 Add Entity 버튼을 눌러 Entity를 추가할 수 있다.
엔티티가 뭘까? 사전적인 의미는 독립체라고 한다. 어떤 존재라는 의미도 갖고 있다. 프로그래밍 관점에서는 개체라고 부르는 게 적절할 것 같다. 엔티티는 코어데이터에서 사용하는 데이터 모델의 형식이다.
엔티티를 정의하기 전에, 구조체로 모델을 정의하는 걸 상상해보자.
struct PhoneBook {
var name: String
var phoneNumber: String
}
PhoneBook이라는 구조체가 name과 phoneNumber 프로퍼티를 갖고 있는 형태다. 이제 에밀리를 전화번호부에 등록하기 위해 인스턴스를 생성한다.
let emily = PhoneBook(name: "Emily", phoneNumber: "010-1234-5678")
에밀리의 전화번호를 계속 보관하기 위해서는, 이 데이터를 기기에 영구저장 해야할 것이다.(UserDefaults는 이걸 저장한다.)
이런 것처럼 CoreData에서 정의하는 데이터 모델의 형태는 Entity다.

Entity를 정의한 뒤 그 안에 프로퍼티처럼 들어가는 구성요소 Attribute로 name과 phoneNumber를 정의하고, 데이터 타입을 지정한 모습이다.
Entity를 생성하고 나면, 우측 인스펙터에서 Codegen이라는 항목을 볼 수 있다. 코드젠은 3가지 선택 항목이 있다. Entity를 가지고 코드를 작성할 때 코드 작성 방법을 선택하는 것이다.
Manual/None : Entity의 서브 클래스를 자동으로 생성하지 않고 개발자가 클래스를 작성Class Definition : Entity의 서브 클래스를 자동으로 생성Category/Extension : Entity 클래스와 함께 extension을 위한 파일까지 생성Manual로 선택하고, Xcode → Editor → Create NSManagedObject Subclass를 눌러 클래스를 생성해주면, 자동으로 2개의 swift 파일이 생성된다.
import Foundation
import CoreData
// PhoneBook+CoreDataClass.swift
@objc(PhoneBook)
public class PhoneBook: NSManagedObject {
}
// PhoneBook+CoreDataProperties.swift
extension PhoneBook {
@nonobjc public class func fetchRequest() -> NSFetchRequest<PhoneBook> {
return NSFetchRequest<PhoneBook>(entityName: "PhoneBook")
}
@NSManaged public var name: String?
@NSManaged public var phoneNumber: String?
}
extension PhoneBook : Identifiable {
}
Entity와 같은 이름의 public class가 생성되었다.
NSManagedObject는 CoreData 프레임워크에서 관리되는 객체를 나타내는 기본 클래스로, 엔티티와의 상호작용을 관리하며 속성 값의 저장 및 검색을 처리한다.@nonobjc는 Objective-C에서는 동작하지 않고 Swift에서만 동작하는 메소드임을 명시하는 macro다.fetchRequest() 메소드는 PhoneBook에 대한 여러가지 데이터 검색(데이터에 접근)을 돕는 함수다.@NSManaged : CoreData에 의해 관리되는 객체를 의미한다.Identifiale 프로토콜을 채택하는 건 PhoneBook 타입이 고유하게 식별될 수 있도록 하는 것이다.CoreData 기능 구현과 직결되는 부분은 아니지만, 추후에 key 값으로 많이 사용될 값들을 PhoneBook 클래스에 선언해두면 유용하다.
@objc(PhoneBook)
public class PhoneBook: NSManagedObject {
public static let className = "PhoneBook"
public enum Key {
static let name = "name"
static let phoneNumber = "phoneNumber"
}
}
typealias Key = PhoneBook.Key
AppDelegate에 생성하는 Container로, 데이터의 영구 저장소다. 데이터를 저장하고 관리하는데 필요한 핵심 객체로, CoreData와 프로젝트 사이에 데이터를 주고 받을 수 있게하는 매개체다.
lazy var persistentContainer: NSPersistentContainer = {
// name: 에 xcdatamodeld 파일 이름을 적어준다.
let container = NSPersistentContainer(name: "CoreDataPractice")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
saveContext() 함수는 데이터에 변동 발생 시 호출되어 container에 변동사항을 저장한다.
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)")
}
}
}
import UIKit
import CoreData
class ViewController: UIViewController {
var container: NSPersistentContainer!
override func viewDidLoad() {
super.viewDidLoad()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
self.container = appDelegate.persistentContainer
}
}
PhoneBook 엔티티에 저장할 수 있도록 Attribute와 같은 타입의 데이터를 파라미터로 받도록 함수를 선언한다.NSEntityDescription의 메소드를 통해 PhoneBook 엔티티를 생성한다.container의 context에 NSManagedObject 객체를 생성한다.setValue 메소드를 통해 값을 넣어준다.do-catch 문을 통해 container에 객체를 저장한다.// view controller
// 함수 선언 (PhoneBook의 Attribute와 같이 name과 phoneNumber를 파라미터로 받는다.)
func createData(name: String, phoneNumber: String) {
func createData(name: String, phoneNumber: String) {
// PhoneBook entity 생성
guard let entity = NSEntityDescription.entity(forEntityName: PhoneBook.className, in: self.container.viewContext) else { return }
// NSManagedObject 객체 생성
let newPhoneBook = NSManagedObject(entity: entity, insertInto: self.container.viewContext)
// 값 주입
newPhoneBook.setValue(name, forKey: Key.name)
newPhoneBook.setValue(phoneNumber, forKey: Key.phoneNumber)
// container에 객체 저장
do {
try self.container.viewContext.save()
print("저장 성공")
} catch {
print("저장 실패")
}
}
}
PhoneBook extension에 정의한 fetchRequest 함수를 호출하여 container로부터 PhoneBook의 데이터를 불러온다.fetchRequest의 반환 타입은 NSFetchRequest<PhoneBook>viewContext(NSManagedObjectContext)의 fetch 메소드의 반환타입은 [Any][PhoneBook] 형태를 띄는데, 반복문을 통해 저장된 요소를 모두 조회하고 출력한다.PhoneBook extension에 @NSManaged public var로 선언된 프로퍼티들에 접근하기 위해서다.func readData() {
do {
let phoneBooks = try self.container.viewContext.fetch(PhoneBook.fetchRequest())
for phoneBook in phoneBooks as [NSManagedObject] {
if let name = phoneBook.value(forKey: Key.name) as? String,
let phoneNumber = phoneBook.value(forKey: Key.phoneNumber) as? String {
print("name : \(name), phone number : \(phoneNumber)")
}
}
} catch {
print("읽기 실패")
}
}
Attribute의 기존 값으로 검색하여 해당 데이터를 받는다.predicate라는 메소드를 통해 조건을 걸어 검색한다.do-catch문을 통해 container에 저장된 해당 데이터를 받는다.update하고자 하는 값으로 setValue 한다.container에 지금까지의 context를 저장한다.func updateData(currentName: String, updateName: String) {
// 데이터 불러오기
let fetchRequest = PhoneBook.fetchRequest()
// predicate : 조건을 걸도록 함
// name == %@ : name의 값이 파라미터의 값과 같은 것을 검색하라는 뜻
fetchRequest.predicate = NSPredicate(format: "name == %@", currentName)
do {
// container에서 해당 값에 속하는 데이터를 가져와 result에 할당
let result = try self.container.viewContext.fetch(fetchRequest)
// array 형태로 받은 result를 순회하며 새로운 값으로 set
for data in result as [NSManagedObject] {
data.setValue(updateName, forKey: Key.name)
}
// container에 저장
try self.container.viewContext.save()
print("데이터 수정 성공")
} catch {
print("데이터 수정 실패")
}
}
fetch 하는 과정은 위와 같다.result를 순회하며 container에서 삭제한다.context를 저장한다.func deleteData(name: String) {
// name으로 검색하여 data fetch
let fetchRequest = PhoneBook.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "name == %@", name)
do {
let result = try self.container.viewContext.fetch(fetchRequest)
for data in result as [NSManagedObject] {
// 검색해서 가져 온 데이터 삭제
self.container.viewContext.delete(data)
}
// 삭제된 내역을 저장
try self.container.viewContext.save()
print("데이터 삭제 성공")
} catch {
print("데이터 삭제 실패")
}
}
