Skip to content

Persistable 추상화

구병조(GEN) edited this page Nov 19, 2024 · 4 revisions

🛠️ 문제 상황

데이터를 로컬에 저장할 수 있음에 대한 추상화로 Persistable이라는 프로토콜을 생성하였습니다.

protocol Persistable {
    /// CREATE
    func add<Entity: NSManagedObject>(entityProvider: (NSManagedObjectContext) -> Entity) async throws -> Entity
    /// READ
    func fetch<Entity: NSManagedObject>(by request: NSFetchRequest<Entity>) throws -> [NSManagedObject]
    /// UPDATE
    func update<Entity: NSManagedObject>(entity: Entity, updateHandler: (Entity) -> Void) throws -> Entity
    /// DELETE
    func delete<Entity: NSManagedObject>(entity: Entity) throws
    func delete<Entity: NSManagedObject>(by request: NSFetchRequest<Entity>) throws
}

다만 로컬에 데이터를 저장하는 방식이 Core Data만 있는 것이 아니기에, 좀 더 통합적인 추상으로 표현할 수 없을까 하는 고민에서 시작되었습니다.

💡 해결 과정

문제는 NSManagedObject 생성에 있습니다.

NSManagedObject를 생성하여 Core Data에 추가할 때는 반드시 NSManagedObjectContext를 지정해야 합니다.

NSManagedObjectContext는 Core Data의 데이터 관리 단위이며, 생성한 NSManagedObject 인스턴스가 Core Data 스택의 컨텍스트에 연결되어야 해당 객체가 영구 저장소와의 상호작용을 할 수 있습니다.

관건은 비즈니스 로직의 엔티티와 NSManagedObject를 어떻게 매핑할 것인가 입니다.

힌트는 일괄 연산 API에서 얻었습니다.

코어 데이터의 일괄 추가 요청(NSBatchInsertRequest)을 만들때, 매칭되는 엔티티의 이름과 각 속성에 넣을 값을 딕셔너리 형태로 제공해서 생성할 수 있습니다.

💡 각 속성을 key, 값을 value로 하여 딕셔너리를 통해 변환하는 것을 각 레이어(비즈니스 로직 - 저장소)의 엔티티 매핑에서도 활용합니다.

다음과 같은 엔티티 요구사항을 만듭니다.

protocol EntityRepresentable: Sendable {
    /// 현재의 데이터를 기반으로 변환한 딕셔너리 값.
    var dictionaryValue: [String: Any] { get }
    /// 해당 엔티티를 식별하기 위해 필요한 속성 딕셔너리 값.
    var matchingAttributes: [String: Any] { get }
    
    init(dictinary: [String: Any])
    
    static var entityName: String { get }
}

딕셔너리를 통해 속성값을 다룰 수 있도록 합니다.

그리고 CRUD의 R(fetch) 연산을 할 때 필요한 NSFetchRequest로 범용으로 사용할 수 있도록 추상화를 수행합니다.

protocol PersistFetchRequestable<Entity> {
    associatedtype Entity: EntityRepresentable
    
    var predicate: NSPredicate? { get }
    var sortDescriptors: [NSSortDescriptor] { get }
    /// 검색을 통해 최대로 가져올 수 있는 수.
    var fetchLimit: Int { get }
    /// 검색 후 데이터를 가져오는 시작점.
    var fetchOffset: Int { get }
}

결과

protocol Persistable {
    /// 로컬 저장소에 엔티티 데이터를 추가합니다.
    /// - Parameter entities: 추가할 엔티티 배열.
    /// - Returns: 추가된 데이터.
    func add<Entity>(contentsOf entities: [Entity]) async throws -> [Entity] where Entity: EntityRepresentable
    /// 로컬 저장소에서 요청 조건에 맞는 엔티티 데이터를 불러옵니다.
    /// - Parameter request: 요청 조건.
    /// - Returns: 엔티티 데이터.
    func fetch<Entity>(
        by request: any PersistFetchRequestable<Entity>
    ) async throws -> [Entity] where Entity: EntityRepresentable
    /// 로컬 저장소의 엔티티 데이터에 대한 최신화를 수행합니다.
    /// - Parameters:
    ///   - entity: 최신화할 엔티티 값.
    ///   - predicate: 최신화 할 엔티티 데이터를 찾기 위한 조건문.
    /// - Returns: 최신화 된 엔티티 데이터.
    func update<Entity>(
        to entity: Entity,
        forMatching predicate: NSPredicate
    ) async throws -> Entity where Entity: EntityRepresentable
    /// 로컬 저장소에서 엔티티 데이터를 제거합니다.
    /// - Parameter entities: 제거할 엔티티 데이터.
    func delete<Entity>(contentsOf entities: [Entity]) async throws where Entity: EntityRepresentable
}
Clone this wiki locally