호댕의 iOS 개발

[iOS] CoreData 알아보기 본문

Software Engineering/iOS

[iOS] CoreData 알아보기

호르댕댕댕 2022. 3. 30. 16:06

CoreData의 공식 문서를 살펴보면 CoreData에 대해 이렇게 정의를 하고 있다. 

Persist or cache data on a single device, or sync data to multiple devices with CloudKit.

Data를 단일 기기에 저장하거나 데이터를 캐시하거나, CloudKit을 사용하여 다양한 기기에서 동기화를 할 때 사용되는 프레임워크이다. 

 

즉, 단순히 local 데이터를 저장하는 데이터베이스 역할을 하는 것이 아니라 저장, 캐시, 동기화 등의 기능을 제공하는 프레임워크인 것이다.

이외에도 Undo / Redo Manager를 통해 변화를 감지하여 쉽게 Undo와 Redo를 구현할 수 있도록 하기도 한다. 

 

물론 Local에 저장을 할 때 즉, 온라인 / 오프라인 관계 없이 디바이스에 저장을 하고 싶다면 많이 사용되는 Realm이란 라이브러리를 사용할 수도 있다. 

 

Realm과 CoreData 각각 장단점을 가지고 있긴 하다. 

  CoreData Realm
장점 1. 애플의 First Party 프레임워크로 애플의 다른 프레임워크나 API와 연동이 잘 된다.
2. 애플이 만든 만큼 지속적인 업데이트와 관리가 이뤄진다. 
3. 의존성 관리 도구를 통해 추가적인 설치가 필요하지 않다. 
4. Editor Style을 통해 Entity 간 관계를 쉽게 확인 가능
1. CoreData에 비해 러닝 커브가 낮고 간단하다.
2. 타 플랫폼(안드로이드, Java, MacOS) 등에서도 사용 가능
3. SQLite를 사용하는 CoreData보다 속도가 빠름
단점 1. remote DB의 역할을 하진 못한다. 
2. 러닝커브가 다소 높은 편이다. 
1. 추가적으로 의존성 관리도구를 통해 라이브러리를 설치해야 한다.

 

🤓 CoreData를 어떻게 사용하면 될까?

1️⃣ CoreData를 프로젝트에 추가하기

가장 쉽게 CoreData를 프로젝트에 추가하는 방법은 애초에 생성을 할 때 Use Core Data를 체크하고 프로젝트를 생성하는 것이다.

체크를 하고 추가를 하게 되면 자동으로 DataModel이 생성이 된다. 

이렇게 프로젝트를 생성할 때 Use CoreData를 체크하고 만들게 되면 기본으로 AppDelegatepersistentContainer가 생성이 되며, 이를 저장하기 위한 saveContext 함수가 자동으로 생성이 된다. 

class AppDelegate: UIResponder, UIApplicationDelegate {

    ...

    lazy var persistentContainer: NSPersistentContainer = {        
        let container = NSPersistentContainer(name: "DataModel")
        container.loadPersistentStores { description, error in
            if let error = error {
                fatalError("Unable to load persistent stores: \(error)")
            }
        }
        return container
    }()

    ...
}

 

이 부분이 추후에 CoreData를 추가할 때와 가장 큰 차이점인 것 같다. 

 

 

이미 만들어진 프로젝트에서 CoreData를 추가한다면 cmd + n으로 파일 추가를 위한 창을 띄운 뒤 Core Data 탭의 Data Model을 추가해주면 된다! 

다만 앞서 말했듯 이렇게 추가를 해준다면 따로 AppDelegate에 Persistent Container가 생성되진 않는다. 

 

➕ 추가! 

Persistent Container가 AppDelegate에 생성되는 이유는 앱이 실행되고 이를 사용해야 할 때 Persistent Container 인스턴스가 생성되어야 해서 AppDelegate에서 lazy로 생성하는 것으로 이해했다. (호출할 때 생성이 되도록 lazy로 선언)

 

Persistent Container의 주석 내용을 보더라도 Persistent Container는 앱을 위한 저장소로 앱이 저장을 위해 이를 불러올 때 생성을 하고 저장소를 반환한다고 되어 있다. 

The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it.

 

2️⃣ DataModel의 Entity 추가

일단 CoreData의 Persistent Container 기능에 초점을 맞춰보자면 CoreData에 데이터를 어떻게 저장할 지에 대한 Entity를 구현해줘야 한다.

일반적인 모델을 구현하는 것처럼 Entity의 이름을 정해주고 어떤 Attribute로 구성이 되어있고, 그 Attribute는 어떤 Type으로 되어있는지를 지정해주면 된다. 

 

그 후 Editor > Create NSManagedObject Subclass를 누르게 되면 지정해놓은 Entity 설정에 따라 CoreData를 위한 Class가 생성이 된다. 

이때 Entities의 DataModel Inspector를 살펴보면

Codegen이란 것을 찾아볼 수 있다. 

이는 Manual/None, Class Definition, Category/Extension으로 구성이 되어 있다. 

 

그렇다면 각각이 뭔지에 대해 알아보자~ 

이는 Generating Code라는 공식 문서에서도 자세히 나와있다. 

 

  • Class Definition

이는 자동으로 생성된 Subclass의 프로퍼티나 managedObject의 기능을 수정할 필요가 없을 경우 사용하는 옵션! 

이 옵션을 선택하게 되면 자동으로 소스파일이 생성되게 된다. 

 

따라서 이때 Editor > Create NSManagedObject Subclass를 누르게 되면 Class와 Extension 파일이 자동으로 생성되면서

이런 에러가 발생하게 된다.

 

  • Category / Extension

추가적인 메서드나 비즈니스 로직을 subclass에 정의할 때 사용하는 옵션이다. 이 옵션을 선택하면 클래스 파일의 경우 완전히 제어를 할 수 있다. 직접 클래스를 생성하고 유지할 수 있는 것이다. 

 

이때도 Extension 파일은 자동으로 생성이 되게 된다. 따라서 Extension 파일을 추가로 생성하면 위와 같은 에러가 발생한다. 

 

  • Manual / None

이를 선택하면 하위 클래스의 프로퍼티들을 편집할 수 있다. 이 옵션을 선택한 경우 CoreData는 managed object를 위해 어떤 파일도 생성하지 않는다. 직접 클래스를 생성하고 유지해야 한다. 

 

이때는 직접 Editor > Create NSManagedObject Subclass를 통해 Class와 Extension을 만들어줘야 한다. 

 

 

3️⃣ 데이터를 저장하고 불러오기 

CoreData의 Persitent Container에 값을 저장하고 불러올 때에는 항상 Managed Object Context를 거쳐야 한다

이 과정에서 JSON 파일을 데이터로 변환하는 것처럼 Serializing과 Deserializing 프로세스가 필요하다. 

 

이때 Context에서 원래 가지고 있던 형태로 바꿀 때 사용할 정해진 형식이 앞서 정해두었던 Entity의 Attribute인 것이다. 

또한 Persistent로 넣기 위한 타입으로 변환하기 위해 Class에는 NSManagedObject를 상속하게 된다. 

 

 

코드를 통해 이 과정을 어떻게 구현하면 되는지 살펴보자. 

예제 코드에선 프로젝트를 생성할 때 Use Core Data를 따로 체크하지 않고 추후에 CoreData를 추가해주었고 AppDelegate가 아닌 CoreData의 관리를 위한 타입을 분리하고 싶어 CoreDataManager를 따로 두었다. 

 

private enum Name {
    static let conatainer = "CoreDataModel"
    static let entity = "Work"
}

class WorkCoreDataManager: WorkManagable {
    
    // MARK: - Properties
    static let shared = WorkCoreDataManager()
    
    private let persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: Name.conatainer)
        container.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Unable to load persistent stores: \(error)")
            }
        }
        
        return container
    }()
    
    private var context: NSManagedObjectContext {
        return persistentContainer.viewContext
    }
    
    •••
}

(WorkManagable이란 프로토콜은 해당 Manager를 추상화한 프로토콜이라 무시해주셔도 됩니다!)

 

일단 CoreData 저장을 위한 Persistent Container를 생성해야 하기 때문에 NSPersistentContainer를 만들어주면 된다. 

여기서 NSPersistentContainer의 인스턴스를 생성할 때 파라미터로 name을 받고 있는데 여기는 CoreData의 Data Model의 파일 이름을 작성해주면 된다. 

 

그리고 여기서 앱의 타입의 변경사항을 추적하는 Context를 생성해주어야 한다. 

위 예시에선 persistentContainer의 viewContext를 사용한 것을 볼 수 있다. 

 

    func load(errorHandler: (Error) -> Void) -> [Entity 타입] {
        do {
            return try context.fetch(Entity 타입.fetchRequest())
        } catch {
            errorHandler(error)
        }
        
        return []
    }

불러오는 것은 만들어놓은 context의 fetch 메서드를 사용하면 된다. fetch 메서드에는 파라미터로 NSFetchRequest를 넣어주면 된다. 현재는 만들어놓은 Entity의 클래스에서 가져오면 되기 때문에 해당 클래스의 fetchRequest() 메서드를 사용하면 된다. 

 

fetchRequest 메서드는 Editor > Create NSManagedObject Subclass를 통해 Subclass를 만들 때 자동으로 생성이 된다. 

@nonobjc public class func fetchRequest() -> NSFetchRequest<Entity타입> {
    return NSFetchRequest<Entity타입>(entityName: "Entity타입")
}

 

저장의 경우 context는 변경사항을 계속 추적하고 있기 때문에 hasChanges에서 true가 나올 경우 저장을 해주면 된다. 

func saveContextChange () {
    if context.hasChanges {
        do {
            try context.save()
        } catch {
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }
}

 

context의 save와 fetch 메서드의 경우 모두 error를 throw할 수 있는 메서드이기 때문에 따로 에러 처리를 해줘야 한다. 

 

 

4️⃣ 데이터의 생성, 삭제

그렇다면 데이터를 생성, 삭제를 하여 Persistent Container에 저장하려면 어떻게 해야 할까?

 

✅ 생성

func create(title: String, body: String, dueDate: Date) {
    let localData = Work(context: context)

    localData.id = UUID()
    localData.title = title
    localData.body = body
    localData.dueDate = dueDate
    localData.categoryTag = Work.Category.todo.tag

    do {
        try context.save()
    } catch {
        print(Content.saveError)
    }
}

NSManagedObject를 상속받은 Entity의 클래스의 경우 init(context:)를 통해 생성을 할 수 있다. 이를 통해 생성하여 프로퍼티에 값을 넣어주는 방식으로 생성할 인스턴스를 정의해준 뒤 이전처럼 context의 save 메서드를 통해 Persistent Container에 저장을 해주면 된다. 

 

값은 setValue를 통해 넣어줄 수도 있다. 

 

🚫 삭제

삭제는 더 간단하다. context에 이미 delete 메서드가 있기 때문에 어떤 인스턴스를 지울지만 파라미터로 전달해주고 저장을 해주면 된다. 

func delete(_ data: Work) {
    context.delete(data)

    do {
        try context.save()
    } catch {
        print(Content.saveError)
    }
}

 

5️⃣ 특정 조건을 충족하는 값만 꺼내기

그렇다면 만약 Persistent Container에서 특정 조건을 충족하는 것만 꺼내오고 싶을 때에는 어떻게 해야 할까?

 

이때 사용할 수 있는 것이 바로 NSPredicate이다. 

이는 NSFetchRequest의 predicate 프로퍼티를 정해주고 이를 통해 값을 가져와주면(context.fetch(NSFetchRequest))된다.

 

private func sort(for categoryTag: Int16) -> [Work] {
    let request = Work.fetchRequest()
    let predicate = NSPredicate(format: Content.predicateFormat, categoryTag)
    request.predicate = predicate

    let searchedWorks = try? context.fetch(request)

    return searchedWorks ?? []
}

일단 값을 가져와야 하는 Entity의 클래스에서 fetchRequest() 메서드를 통해 NSFetchRequest 인스턴스를 생성한 후 원하는 조건을 작성하기 위해 NSPredicate를 사용하면 된다. 

 

이때 init(format:_:)을 사용하면 된다. 

format에는 어떤 것을 가지고 올 지에 대한 조건을 작성해주고 _ args에는 비교를 위해 들어오는 값을 작성해주면 된다. 

 

위 예시에선 일단 categoryTag를 기준으로 필터링할 것이기 때문에 _ args에는 categoryTag를 작성해주었다. 

format에 작성할 조건의 경우 아카이브 문서 Predicate Programming Guide에 나와있다. 

 

정리해보면 다음과 같다. 

 

기본 연산자

  • =, == : 같다
  • >=, => : 왼쪽이 오른쪽보다 크거나 같다. 
  • <=, =< : 오른쪽이 왼쪽보다 크거나 같다.
  • > : 왼쪽이 크다
  • < : 오른쪽이 크다
  • <>, != : 양쪽이 다르다

 

지정자

  • %@ : String, Date로 들어오는 값
  • %K : keypath로 들어오는 값
  • %d : Int로 들어오는 값

 

 

만약 categoryTag가 특정 값과 같은 경우만 필터링을 하고 싶다면 Content.predicateFormat에는

"categoryTag == %d" 이렇게 작성을 해주면 되는 것이다. 

(format은 String으로 작성해줘야 한다)

 

이후 request의 predicate에 정해준 NSPredicate를 할당해주고 이 request대로 context에서 fetch함수를 통해 값을 가지고 오면 원하는 값만 가져올 수 있다. 

Comments