본문 바로가기
카테고리 없음

KeyPath를 이용한 CoreData NSPredicate 조작어 제작

by ARpple 2025. 5. 12.

문제 상황 - Repository에서 직접 CoreData Entity에 접근함

NSPredicate에 조건을 걸어서 값을 가져와야만 하는 요구사항 발생

  • 특정 날짜에 얼마나 유저가 타이머를 사용한 기록을 가져오고 싶은 요구사항
class TimerRecordRepository {
        ...
    /// 오늘 기록 아이템들 반환
    func get(day: Date) async throws -> [TimerRecordItem] {
        let predicate = NSPredicate(format: "recordCode == %@", day.convertToRecordCode())
        return try await get(date: day, predicate: predicate)
    }
}

중요한 건 아래 코드다.

let predicate = NSPredicate(format: "recordCode == %@", day.convertToRecordCode())

⇒ 이 코드의 recordCode는 CoreDataEntity의 attribute다. 저것을 없애자!!

문제 해결

문제 해결법 접근하기

⇒ TimerRecordItem를 이용해 TimerRecordItemEntity Attribute 문자열 반환하기

  1. TimerRecordItem의 파라미터를 직접 접근하기
  2. TimerRecordItem의 파라미터와 TimerRecordItemEntity의 Attribute와 문자열로 가져오기
  3. NSPredicate의 Format을 작성하기 쉽게 도와주는 메서드 만들기

⚠️ TimerRecordItem

struct TimerRecordItem {
    var id: UUID
    // 기록 코드, 년월일로 구분해서 중복된 것을 모두 가져온다.

    var recordCode: String = "" // 이게 고유 ID라고 보기는 애매해진다.
    var createdAt: Date = .init()
    var duration: Int = 0
    var session: SessionItem = .init(name: "")

    /// 데이터를 변경한 날짜 -> 최신 날짜 기준으로 동기화 할 것
    var userModificationDate: Date? = Date()
}

1. TimerRecordItem의 파라미터를 직접 접근하기

TimerRecodItemEntity attribute와 TimerRecordItem 파라미터 간 변환할 수 있는 프로토콜 정의하기

TimerRecordItem 파라미터 값을 통해서만 조건을 걸고 싶은 것이다.

⇒ 파라미터로 조건을 건다 → KeyPath를 사용한다.

즉, \TimerRecordItem.createAt 으로 TimerRecordItemEntity의 createdAt을 매칭하는 기능을 추가해야한다.

2. TimerRecordItem 파라미터, TimerRecordItemEntity의 Attribute를 문자열로 가져오기

프로토콜로 TimerRecordItem, TimerRecordItemEntity간 연결하기

프로토콜 정의

protocol CoreEntityConvertible {
    associatedtype Model: Identifiable
    static func attributes(key: PartialKeyPath<Model>) throws -> String
}
  • 이 프로토콜로 associatedtype Model을 통해 CoreData 엔티티는 어떤 타입으로 변환해야할지 으로 알고 있다.

프로토콜 구현

extension TimerRecordItemEntity: CoreEntityConvertible {
    typealias Model = TimerRecordItem
    static func attributes(key: PartialKeyPath<TimerRecordItem>) throws -> String {
        switch key {
        case \.createdAt: return "createdAt"
        case \.recordCode: return "recordCode"
        case \.id: return "id"
        case \.session: return "sessionKey"
        case \.duration: return "duration"
        default: throw NSError(domain: "KeyPath Error", code: 1)
        }
    }

}

이제 저 구조에 맞게 CoreData Predicate를 작성하게 해주는 메서드를 만들면 된다.

3. NSPredicate의 Format을 작성하기 쉽게 도와주는 메서드 만들기

사용하는 예시
let findValues: [TimerRecordItem] = try await coreDataService.findWithCondition(
            type: TimerRecordItem.self,
            entityKey: .timerRecordEntity,
            attributes: [\.recordCode, \.duration],
            args: ["하이요","방가요"], self.testItem.duration
        ) { "\($0[0]) IN %@ AND \($0[1]) <= %d" }
  1. type → 가져올 Model
  2. entityKey → 정의된 CoreDataEntity
  3. attributes → Model의 KeyPath, 이 값들을 통해 로 Entitiy의 attribute들을 가져온다.
  4. args → 조건에 맞게 사용할 argument
  5. trailing closure → Predicate에 format
구현 코드
extension CoreDataService {

    func findWithCondition <Model> (
        type: Model.Type,
        entityKey: CoreConstants.Label,
        attributes: [PartialKeyPath<Model>],
        args: any CVarArg... ,
        predicateFormat: (_ attributes: [String]) -> String
    ) async throws -> [Model] {
        switch entityKey {
        case .timerRecordEntity:
        /// Predicate에서 사용할 Attribute 값들을 문자열로 반환
            let predicateKeys = try attributes.map { attribute in
                guard let entityAttribute = attribute as? PartialKeyPath<TimerRecordItem> else {
                    throw CoreError.invalidAttribute(attribute.customDumpDescription)
                }
            return try TimerRecordItemEntity.attributes(key: entityAttribute)
            }
        /// Predicate Format 문자열로 반환
            let predicateFormatString = predicateFormat(predicateKeys)
        /// Predicate argument와 format 문자열을 통해 Predicate 생성
            let predicate = NSPredicate(format: predicateFormatString, args)

            if let res = try await fetchWithPredicate(
                type: TimerRecordItemEntity.self,
                entityDescriptionKey: .timerRecordEntity,
                predicate: predicate
            ) as? [Model] {
                return res
            } else {
                throw CoreError.invalidEntity
            }
        }
    }
}
  1. 엔티티 타입 확인 및 변환
  2. Model PathKey 배열에 일지하는 Entitiy 속성이 있는지 확인, 없으면 즉시 에러 throw
  3. 엔티티 속성 문자열 반환
  4. 엔티티 속성 문자열을 통해 Predicate format 문자열 반환
  5. Predicate Format과 args로 Preciate 생성
  6. 외부에서 정의한 predicate로 Model 변환하는 메서드 실행

댓글