본문 바로가기
Project/ReceiptMind

💾 Core Data와 FileManager로 지출 내역 저장하기

by 밤새는 탐험가89 2025. 8. 1.
728x90
SMALL
Core Data + FileManager + Combine 조합으로 데이터와 이미지까지 한 번에 저장하기

✨ 목표

지출 내역을 저장할 때,

  • 기본 정보(날짜, 금액, 메모 등)는 Core Data에
  • 선택 이미지가 있다면 FileManager에 저장하고 → 저장된 이미지 경로를 Core Data에 함께 저장해 관리하는 방식.

✅ 핵심 클래스: TransactionCoreDataManager

final class TransactionCoreDataManager {
    
    // MARK: - Variable
    static let shared = TransactionCoreDataManager()
    private let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    private let storageManager = TransactionFileManager.shared
}

 

  • 싱글톤 패턴으로 한 곳에서 데이터 저장/불러오기 관리
  • context는 Core Data의 NSManagedObjectContext
  • storageManager는 이미지를 저장하기 위한 FileManager 객체

 

📦 트랜잭션(지출 내역) 저장 함수

func createTransaction(_ transaction: ExpenseModel) -> AnyPublisher<ExpenseModel, Error> {
    return Future { [weak self] promise in
        guard let self = self else { return }

        var savePath: String? = nil
        if let selectedImage = transaction.image {
            savePath = self.storageManager.saveImage(selectedImage, transaction.id.uuidString)
            if savePath == nil {
                promise(.failure(NSError(domain: "TransactionImageSaveError", code: 1)))
                return
            }
        }

        print("🧾 저장할 정보 로그...")
        let expenseEntity = ExpenseEntity(context: self.context)
        expenseEntity.id = transaction.id.uuidString
        expenseEntity.amount = Int64(transaction.amount)
        expenseEntity.date = transaction.date
        expenseEntity.category = transaction.category
        expenseEntity.transaction = transaction.transaction.rawValue
        expenseEntity.memo = transaction.memo
        expenseEntity.imagePath = savePath ?? ""

        do {
            try self.context.save()
            if let model = expenseEntity.toModel() {
                promise(.success(model))
            } else {
                promise(.failure(NSError(domain: "ModelConversionError", code: 2)))
            }
        } catch {
            promise(.failure(error))
        }
    }
    .eraseToAnyPublisher()
}

 

⚙️ 실행 흐름 (단계별 설명)

단계 설명
1단계 ExpenseModel을 인자로 받아 트랜잭션 저장 시도
2단계 이미지가 있다면 FileManager를 통해 저장 (파일명은 UUID 기반)
3단계 이미지 경로가 nil이면 실패 처리
4단계 ExpenseEntity 인스턴스 생성 및 데이터 바인딩
5단계 Core Data에 저장 후, Model로 변환해서 전달
6단계 성공/실패 여부에 따라 Combine Publisher 반환

💡 왜 이렇게 구성했는가?

  • MVVM 구조에서 ViewModel → Model → CoreDataManager로 흘러가는 흐름을 유지
  • Combine의 Future를 사용하여 비동기 처리와 에러 핸들링을 분리
  • 이미지 저장은 DB에 직접 넣기보다 FileManager에 저장 후 경로만 DB에 저장, 성능과 구조 모두 고려

✅ 이렇게 하면 좋은 점

  1. 이미지 용량 걱정 없음 → DB에 이미지를 넣지 않음
  2. 이미지 로딩 분리 → 뷰에서 FileManager 경로만 받아 직접 불러오기 가능
  3. Combine 도입으로 ViewModel에서도 코드 간결해짐
  4. 테스트가 쉬워짐 → 저장 성공/실패를 Combine으로 바로 구독 가능

🔚 마무리

이 구조는 단순히 데이터를 저장하는 수준을 넘어, 앱의 확장성과 유지보수를 고려한 아키텍처적 선택

  • 이미지가 필수인 앱(앨범, 메모 등)
  • MVVM + Combine 기반 구조
  • Core Data + FileManager 조합
728x90
LIST