문제 상황 - 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 문자열 반환하기
- TimerRecordItem의 파라미터를 직접 접근하기
- TimerRecordItem의 파라미터와 TimerRecordItemEntity의 Attribute와 문자열로 가져오기
- 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" }
- type → 가져올 Model
- entityKey → 정의된 CoreDataEntity
- attributes → Model의 KeyPath, 이 값들을 통해 로 Entitiy의 attribute들을 가져온다.
- args → 조건에 맞게 사용할 argument
- 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
}
}
}
}
- 엔티티 타입 확인 및 변환
- Model PathKey 배열에 일지하는 Entitiy 속성이 있는지 확인, 없으면 즉시 에러 throw
- 엔티티 속성 문자열 반환
- 엔티티 속성 문자열을 통해 Predicate format 문자열 반환
- Predicate Format과 args로 Preciate 생성
- 외부에서 정의한 predicate로 Model 변환하는 메서드 실행
댓글