본문 바로가기
감정일기(가칭)

🍋 Core Data Fetch 고급 설계 — Predicate, Compound, FetchLimit 완전정복

by 밤새는 탐험가89 2025. 10. 20.
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