blanq 개발일기4 – 앱DB

앱내에서 사용하는 데이터 저장소는 저널앱 특성상 가장 중요한 사항이다. 속도도 중요하지만 안정성이 가장 중요하다.
서버 없이 iCloud 동기화를 통해 데이터를 보존해야 하는데 사용자에 따라서 여러가지 이유로 iCloud를 사용하지 못하면 앱내 데이터 저장소가 데이터 보관의 모든것이다. 데이터 저장소가 깨지거나 크래시 발생때문에 앱을 사용하지 못하면 사용자 데이터를 모두 잃을 수 있다.

사용자들로부터 데이터 없어졌다는 메일이 아직도 가장 공포스럽다. 서버쪽에 저장된 데이터 없이 앱내에서만 발생하는 오류로 사용자 데이터가 없어진 이유를 개발자가 알 수 있는 방법은 없다.
즉 사용자 데이터가 어떠한 이유로 없어지거나 사용하지 못하게 되었을 때 개발자가 복구할 수 없다.

blanq 앱내 데이터 저장소로 CoreData를 사용하기로 했다. CoreData와 Realm 모두 상용 서비스에서 사용해 봤는데 Realm은 안정성 문제로 앞으로 사용하지 않을 것이다. 특히 Realm 5.x 이후 스레드 관련 크래시는 아직까지 수정되지 않았다. “DayMore” 일기앱은 Realm을 사용하는데 거의 모든 크래시가 Realm에서 발생하는 크래시이고 지금도 계속 새로운 유형의 크래시가 만들어 지고 있다. 그래도 꼭 Realm을 사용하려 한다면 4.x버전을 사용할 것을 권한다.

Realm 관련 이슈, 경우에 따라서 랜덤하게 크래시 발생하는데 5.0으로 업데이트된 이후 발생함.
현재 해결책은 없고 스레드 스위칭 일어나지 않게 호출하면 크래시 발생 피할 수도 있음.
https://github.com/realm/realm-cocoa/issues/6556

CoreData 또한 스레드 안정성에 대하여 주의해야 하는데 동시에 여러 스레드에서 접근하는 것을 피하도록 했다. Realm에 비해 엄청 큰 장점은 A스레드에서 읽어서 메모리에 저장한 뒤 B스레드에서 읽을 수 있다. 다만 동시에 접근하는 것만 피하면 아무런 문제 없다. Realm은 스레드간 데이터 전달은 기본적으로 불가능하다.

아래는 CoreData 사용 구현부이다. 메인스레드에서는 메인 context 사용하고 그 이외 스레드에서는 private(backgroud) context 를 사용하여 데이터에 접근한다.

let coreDataStack = CoreDataStack()

class CoreDataStack {
    //....
    lazy var backgroundContext: NSManagedObjectContext = {
        return persistentContainer.newBackgroundContext()
    }()
}

class CoreDataOperation {
    class func backgroundBlock(_ block: @escaping (() -> Void)) {   //serial queue
        serialQueue.async(execute: block)
    }
    
    class func perform(_ block: ((NSManagedObjectContext?) -> Void)?, done: (() -> Void)? = nil) {
        if Thread.current.isMainThread {
            block?(coreDataStack.persistentContainer.viewContext)
            coreDataStack.save()
            done?()
        } else {
            let ctx = coreDataStack.backgroundContext
            ctx.performAndWait {
                block?(ctx)
            }

            if ctx.hasChanges {
                try? ctx.save()
            }

            done?()
        }
    }
}

다음은 fetch, insert 호출 예이다.

//백그라운드로 데이터 가져오는 예
CoreDataOperation.backgroundBlock {
    CoreDataOperation.perform { (ctx) in
        if let _entrys = ctx?.fetch("Entry", predicate: ["type":DataType.entry, "range":range].predEntry()) as? [Entry] {
              //...        
        }
    }
}

//데이터 추가
CoreDataOperation.perform { (ctx) in
    ctx?.insert("Tracker") { (t) in
        if let _t = t as? Tracker {
            _t.key = UUID().uuidString
            _t.title = self.trackering.name
        }
    }
}

답글 남기기

이메일 주소는 공개되지 않습니다.