728x90
SMALL
Core Data를 쓰다 보면 결국 “데이터를 어떻게 불러오느냐” 가 핵심이 된다.
특히 감정일기처럼 텍스트, 감정, 이미지가 함께 저장되는 앱이라면
검색 기능과 최신 데이터 조회는 필수다.
이번 글에서는 실제 예시를 통해
Core Data의 fetch를 더 “정확하고, 빠르고, 확장 가능하게” 만드는 설계법
을 단계별로 정리해보자.
🧩 1️⃣ 상황 — 감정일기 데이터를 Core Data에서 조회하기
앱에서 필요한 주요 조회 기능은 다음과 같았다 👇
- 특정 감정으로 검색하기 (happy, sad 등)
- 키워드로 내용 검색하기
- 최근 일기 1개만 불러오기
기존에는 아래와 같은 코드로 구현되어 있었다.
🧱 2️⃣ 변경 전 코드 (Before)
// MARK: - Additional Fetch Functions
extension DiaryCoreDataManager {
// MARK: ✅ 특정 감정 검색
func fetchDiaries(by emotion: String) -> [EmotionDiaryModel] {
let request: NSFetchRequest<EmotionDiaryEntity> = EmotionDiaryEntity.fetchRequest()
request.predicate = NSPredicate(format: "emotion == %@", emotion)
let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: false)
request.sortDescriptors = [sortDescriptor]
do {
let entities = try context.fetch(request)
let models = entities.compactMap { $0.toModel() }
LogManager.print(.success, "총 \(models.count)개의 감정일기 로드 완료")
return models
} catch {
LogManager.print(.error, "감정일기 불러오기 실패: \(error.localizedDescription)")
return []
}
}
// MARK: ✅ 키워드 검색 (내용 기반)
func searchDiaries(by keyword: String) -> [EmotionDiaryModel] {
let request: NSFetchRequest<EmotionDiaryEntity> = EmotionDiaryEntity.fetchRequest()
request.predicate = NSPredicate(format: "content CONTAINS[c] %@", keyword)
let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: false)
request.sortDescriptors = [sortDescriptor]
do {
let entities = try context.fetch(request)
let models = entities.compactMap { $0.toModel() }
LogManager.print(.success, "총 \(models.count)개의 감정일기 로드 완료")
return models
} catch {
LogManager.print(.error, "감정일기 검색 실패: \(error.localizedDescription)")
return []
}
}
// MARK: ✅ 최근 일기 1개 가져오기
func fetchLatestDiary() -> EmotionDiaryModel? {
let request: NSFetchRequest<EmotionDiaryEntity> = EmotionDiaryEntity.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "createdAt", ascending: false)
request.sortDescriptors = [sortDescriptor]
do {
let entities = try context.fetch(request)
let models = entities.compactMap { $0.toModel() }
let lastestDiary = models.first
LogManager.print(.success, "최근 감정읽기 불러오기 완료")
return lastestDiary
} catch {
LogManager.print(.error, "최근 감정일기 불러오기 실패: \(error.localizedDescription)")
return nil
}
}
}
⚠️ 3️⃣ 문제점 정리
| 항목 | 문제 내용 |
| ⚡ 성능 | fetchLatestDiary()에서 전체 데이터를 다 불러온 뒤 first만 사용 → 불필요한 fetch |
| 🔍 확장성 | searchDiaries(by:)는 오직 content만 검색 → 감정, 태그 등 추가 불가 |
| 🧱 중복 | 세 함수 모두 request 생성, sort 설정, fetch 반복 → 유지보수 어려움 |
| 📉 가독성 | fetch 로직이 매번 길게 반복되어 핵심 로직이 묻힘 |
💪 4️⃣ 개선 목표
✅ 성능 향상 — 필요한 만큼만 fetch
✅ 검색 확장성 강화 — 다중 조건 검색
✅ 유지보수성 향상 — 중복 제거
✅ 가독성 개선 — 핵심 로직만 남기기
🚀 5️⃣ 변경 후 코드 (After)
🔹 공통 헬퍼 추가
먼저 중복되는 코드를 통합하는 헬퍼 함수를 추가한다.
private func makeBaseFetchRequest() -> NSFetchRequest<EmotionDiaryEntity> {
let request: NSFetchRequest<EmotionDiaryEntity> = EmotionDiaryEntity.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
return request
}
private func performFetch(_ request: NSFetchRequest<EmotionDiaryEntity>) -> [EmotionDiaryModel] {
do {
let entities = try context.fetch(request)
let models = entities.compactMap { $0.toModel() }
LogManager.print(.success, "총 \(models.count)개의 감정일기 로드 완료")
return models
} catch {
LogManager.print(.error, "감정일기 불러오기 실패: \(error.localizedDescription)")
return []
}
}
🔹 특정 감정 검색
func fetchDiaries(by emotion: String) -> [EmotionDiaryModel] {
let request = makeBaseFetchRequest()
request.predicate = NSPredicate(format: "emotion == %@", emotion)
return performFetch(request)
}
👉 불필요한 중복 코드 제거, 핵심 로직만 남음.
🔹 키워드 검색 (내용 + 감정 통합)
func searchDiaries(by keyword: String) -> [EmotionDiaryModel] {
let request = makeBaseFetchRequest()
let contentPredicate = NSPredicate(format: "content CONTAINS[c] %@", keyword)
let emotionPredicate = NSPredicate(format: "emotion CONTAINS[c] %@", keyword)
request.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: [contentPredicate, emotionPredicate])
return performFetch(request)
}
👉 NSCompoundPredicate로 검색 범위 확장.
하나의 키워드로 내용 + 감정을 동시에 검색할 수 있다.
나중에 태그, 날씨 같은 속성도 쉽게 추가 가능.
🔹 최근 일기 1개만 가져오기
func fetchLatestDiary() -> EmotionDiaryModel? {
let request = makeBaseFetchRequest()
request.fetchLimit = 1 // ✅ 핵심 포인트!
do {
guard let entity = try context.fetch(request).first else {
LogManager.print(.warning, "최근 감정일기를 찾을 수 없습니다.")
return nil
}
LogManager.print(.success, "최근 감정일기 불러오기 완료")
return entity.toModel()
} catch {
LogManager.print(.error, "최근 감정일기 불러오기 실패: \(error.localizedDescription)")
return nil
}
}
👉 fetchLimit = 1 추가로 DB 레벨에서 딱 하나만 가져옴.
성능 개선 + 메모리 효율성 향상.
📊 6️⃣ 결과 비교
| 항목 | 변경 전 | 변경 후 |
| 코드 라인 수 | 110줄 이상 | 약 60줄로 단축 |
| fetch 성능 | 전체 fetch 후 필터링 | fetchLimit로 최소 fetch |
| 검색 기능 | content만 검색 | content + emotion (다중 필드) |
| 확장성 | 새 필드 추가 시 매 함수 수정 필요 | Predicate 배열에 추가만 하면 됨 |
| 가독성 | 중복 fetch 코드 다수 | 핵심 로직만 남음 |
✅ 이번 리팩토링의 핵심
| 포인트 | 설명 |
| 🎯 fetchLimit | 불필요한 데이터 fetch 방지 |
| 🔍 NSCompoundPredicate | 여러 필드를 한 번에 검색 가능 |
| 🧱 공통 함수화 | 중복 제거, 유지보수성 향상 |
| ⚡ 로그 일원화 | LogManager로 통일된 피드백 관리 |
// MARK: - Additional Fetch Functions
extension DiaryCoreDataManager {
// MARK: - Private helper
private func makeBaseFetchRequest() -> NSFetchRequest<EmotionDiaryEntity> {
let request: NSFetchRequest<EmotionDiaryEntity> = EmotionDiaryEntity.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
return request
}
private func performFetch(_ request: NSFetchRequest<EmotionDiaryEntity>) -> [EmotionDiaryModel] {
do {
let entities = try context.fetch(request)
let models = entities.compactMap { $0.toModel() }
LogManager.print(.success, "총 \(models.count)개의 감정일기 로드 완료")
return models
} catch {
LogManager.print(.error, "감정일기 불러오기 실패: \(error.localizedDescription)")
return []
}
}
// MARK: ✅ 특정 감정 검색
func fetchDiaries(by emotion: String) -> [EmotionDiaryModel] {
let request = makeBaseFetchRequest()
request.predicate = NSPredicate(format: "emotion == %@", emotion)
return performFetch(request)
}
// MARK: ✅ 키워드 검색 (내용 + 감정)
func searchDiaries(by keyword: String) -> [EmotionDiaryModel] {
let request = makeBaseFetchRequest()
let contentPredicate = NSPredicate(format: "content CONTAINS[c] %@", keyword)
let emotionPredicate = NSPredicate(format: "emotion CONTAINS[c] %@", keyword)
request.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: [contentPredicate, emotionPredicate])
return performFetch(request)
}
// MARK: ✅ 최신 일기 1개 가져오기
func fetchLatestDiary() -> EmotionDiaryModel? {
let request = makeBaseFetchRequest()
request.fetchLimit = 1
do {
guard let entity = try context.fetch(request).first else {
LogManager.print(.warning, "최근 감정일기를 찾을 수 없습니다.")
return nil
}
LogManager.print(.success, "최근 감정일기 불러오기 완료")
return entity.toModel()
} catch {
LogManager.print(.error, "최근 감정일기 불러오기 실패: \(error.localizedDescription)")
return nil
}
}
}728x90
LIST
'감정일기(가칭)' 카테고리의 다른 글
| ⚙️ Core Data에서 catch가 작동하지 않는 이유 (0) | 2025.10.20 |
|---|---|
| 🧪 DiaryTestManager, 어디에 두는 게 맞을까? (0) | 2025.10.20 |
| 🍋 Swift FileManager 설계 — private 유지하면서 안전하게 경로 노출하기 (0) | 2025.10.19 |
| 🚀 Swift 6에서 바뀐 Concurrency 규칙 완전 정리 (0) | 2025.10.19 |
| 🧠 Swift Concurrency — “Capture of 'self' with non-sendable type” 완전 정리 (1) | 2025.10.19 |