Core Data에 대해 아라보자 (3) - Fetch Data

Yang Si Yeon·2022년 3월 5일
4

CoreData

목록 보기
3/3
post-custom-banner

Core Data에서 NSFetchRequest를 통해 데이터를 가져오는 방법은 총 5개이다.

// 1
let fetchRequest1 = NSFetchRequest<Venue>()
let entity = 
  NSEntityDescription.entity(forEntityName: "Venue",
                             in: managedContext)!
fetchRequest1.entity = entity

// 2
let fetchRequest2 = NSFetchRequest<Venue>(entityName: "Venue")

// 3
let fetchRequest3: NSFetchRequest<Venue> = Venue.fetchRequest()

// 4
let fetchRequest4 = 
  managedObjectModel.fetchRequestTemplate(forName: "venueFR")

// 5
let fetchRequest5 =
  managedObjectModel.fetchRequestFromTemplate(
    withName: "venueFR",
    substitutionVariables: ["NAME" : "Vivi Bubble Tea"])
  • 1번: 초반에 주로 사용하던 방법
  • 2번: NSFetchRequest의 convenience 이니셜라이저를 사용해 1번을 축약한 방법
  • 3번: 2번을 축약한 방법인데, Venue.fetchRequest()는 Venue+CoreDataProperties.swift에서 찾을 수 있음
  • 4번: NSManagedObjectModel에서 fetchRequest를 검색
  • 5번: 4번과 비슷하지만 몇가지 추가적인 variables를 전달해 predicate 용도로 씀

NSFetchRequest는 제네릭 타입이며, 생성자는 <ResultType: NSFetchRequestResult> 매개변수를 받는다.
이때 ResultType은 fetch request의 결과로 예상되는 타입을 지정하면 된다.

fetch request 저장하기

(포스팅 처음에 소개한 4번 방법)

같은 fetch request가 여러 곳에서 사용된다면 fetch request를 저장해둔 뒤 사용할 수 있다. 하지만 fetch 결과에 대한 순서를 따로 정렬할 수 없다는 단점이 있다.

1) 하단에 Add Entity 버튼을 길게 누른 뒤 'Add Fetch Request'를 클릭해 fetch request를 추가하자.

2) fetch request를 설정해준다.

3) 저장한 fetch request를 불러온다.

guard let model = 
  coreDataStack.managedContext
    .persistentStoreCoordinator?.managedObjectModel,
  let fetchRequest = model
    .fetchRequestTemplate(forName: "fetchAllVenue")
    as? NSFetchRequest<Venue> else {
      return
}

self.fetchRequest = fetchRequest
  • 이 방법을 사용할 땐 managed object model의 fetchRequestTemplate 함수를 사용해야한다.
  • managed object model을 사용하기 위해서는 core data stack의 managed context를 통해 persistent store coordinator를 통해 managed object model을 받아와야 한다.

데이터 제한하기

fetch request가 반환하는 데이터 만큼 중요한 것은 반환하지 않는 데이터이다. Core Data의 object는 서로 관계를 가지고 연결되어 object graph(객체 그래프)를 만드는데, fetch request 마다 전체 object graph를 가지고 온다면 엄청난 메모리가 사용될 것이다.

이를 방지할 수 있는 방법은 3가지가 있는데,

1) fetch request의 fetchBatchSize, fetchLimit, fetchOffset 속성을 사용해 반환되는 데이터를 제한시킬 수 있다.

2) Core Data는 faulting이라는 기술을 사용해 메모리 사용을 최소화 한다. fault는 메모리에 완전히 올라오지 않은 managed object를 나타내는 place holder이다.

3) predicate을 사용하면 object graph를 제한할 수 있다.


데이터 필터링

predicate를 사용해 원하는 데이터를 필터링하는 방법을 알아보자.

Venue.priceInfo.priceCategory가 $인 objects를 가져오려면 먼저 NSPredicate 객체를 lazy로 만들고, fetchRequest 객체를 생성한 뒤 fetchRequest.predicate에 만들어 놓은 NSPredicate 객체를 할당해주면 된다.

lazy var cheapVenuePredicate: NSPredicate = {
	return NSPredicate(
    	format: "%K == "%@",
        #keyPath(Venue.priceInfo.priceCategory), "$"
    )
}()

func populateCheapVenueCountLabel() {
	let fetchRequest =
      NSFetchRequest<NSNumber>(entityName: "Venue")
    fetchRequest.predicate = cheapVenuePredicate
    
	do {
      let countResult = try coreDataStack.managedContext.fetch(fetchRequest)
      // ...
    } catch let error as NSError {
      print("count not fetched \(error), \(error.userInfo)")
    }
}

해당 포스팅의 예시에서는 단일 조건으로 predicate를 만들었지만 AND, OR, NOT과 같은 연산자를 사용해서 두가지 조건을 체크하는 predicate를 작성할 수 있다. 또는 NSCompoundPredicate 클래스를 사용해 두 개의 단순한 predicate를 하나의 복합(compound) predicate로 묶을 수 있다.

NSPredicate는 Core Data의 일부가 아닌 Foundation의 일부이지만, 해당 클래스의 활용법을 잘 알고 있다면 Core Data를 더 잘 활용할 수 있다.

데이터 정렬

NSFetchRequestNSSortDescriptor를 사용해 가져온 data를 정렬할 수 있다.
메모리가 아닌 SQLite 레벨에서 정렬이 일어나기 때문에 더 빠르고 효율적이다.

방법은 predicate를 적용하는 것과 비슷하며, Venue 데이터들을 거리순으로 정렬하는 코드는 다음과 같다.

lazy var distanceSortDescriptor: NSSortDescriptor = {
    return NSSortDescriptor(
      key: #keyPath(Venue.location.distance),
      ascending: true
   )
}()

func sortVenueByDistance() {
	let fetchRequest =
      NSFetchRequest<NSNumber>(entityName: "Venue")
    fetchRequest.predicate = cheapVenuePredicate
    fetchRequest.sortDescriptors = [distanceSortDescriptor]
    
	do {
      let countResult = try coreDataStack.managedContext.fetch(fetchRequest)
      // ...
    } catch let error as NSError {
      print("count not fetched \(error), \(error.userInfo)")
    }
}

다른 result type 가져오기

NSFetchRequest는 단순히 objects를 가져오는 역할만 하는 애가 아니다. NSFetchRequest를 사용해 개별 값을 가져오고 average, max, min 값 등과 같이 데이터에 대한 통계도 계산할 수 있다.

NSFetchRequest에는 resultType 이라는 속성이 있다. 평소엔 기본 값인 .managedObjectResultType 만 사용했지만 이 외에도 다양한 값들이 있다.

  • .managedObjectResultType: managed object를 반환 (기본값)
  • .countResultType: fetch request와 일치하는 objects의 수 반환
  • .dictionaryResultType: 다양한 계산 결과를 반환 (포괄적인 유형)
  • .managedObjectIDResultType: 고유 식별자 반환

위의 것들 중 .countResultType 사용 예제를 살펴보자.

Venue.priceInfo.priceCategory가 $인 objects의 count를 가져오고 싶은 경우 아래 코드로 가져올 수 있다.

let fetchRequest = NSFetchRequest<NSNumber>(entityName: "Venue")
    fetchRequest.resultType = .countResultType
    fetchRequest.predicate = NSPredicate(format: "%K == %@",
                                #keyPath(Venue.priceInfo.priceCategory), "$")
    
    do {
      let countResult = try coreDataStack?.managedContext.fetch(fetchRequest)
      
      let count = countResult?.first?.intValue ?? 0
      let pluralized = count == 1 ? "place" : "places"
      label?.text = "\(count) bubble tea \(pluralized)"
    } catch let error as NSError {
      print("error: \(error), \(error.userInfo)")
    }

실제 object 배열을 가져온 뒤 배열의 count 속성으로도 개수를 얻을 수 있지만, resultType = .countResultType을 사용하는게 당연히 성능에 더 좋다.

NSFetchRequest의 resultType을 .countResultType 으로 지정해주는 것 말고도 Core Data에서 직접 개수를 가져올 수 있는 함수가 있다.

let fetchRequest = NSFetchRequest<NSNumber>(entityName: "Venue")
fetchRequest.predicate = NSPredicate(format: "%K == %@",
                                      #keyPath(Venue.priceInfo.priceCategory), "$")

do {
  let count = try coreDataStack?.managedContext.count(for: fetchRequest) ?? 0
  let pluralized = count == 1 ? "place" : "places"
  label?.text = "\(count) bubble tea \(pluralized)"
} catch let error as NSError {
  print("error: \(error), \(error.userInfo)")
}

비동기로 가져오기

fetch reqeust는 결과가 올때 까지 메인 스레드를 차단한다는 치명적인 문제를 가지고 있다.
이를 막기 위해 Core Data는 iOS 8부터 백그라운드에서 fetch request를 수행하고 결과를 받으면 완료 콜백을 전달하는 API를 제공한다.

예제 코드를 살펴보자.

var asyncFetchRequest: NSAsynchronousFetchRequest<Venue>?

NSAsynchronousFetchRequest 클래스가 비동기로 fetch를 수행할 수 있게 해주는데, 이름을 보면 fetchRequest와 관련있는 것 처럼 생겼지만 사실은 NSPersistentStoreRequest의 하위 클래스이다.

// 1
    let venueFetchRequest: NSFetchRequest<Venue> =
    Venue.fetchRequest()
    fetchAllVenueRequest = venueFetchRequest
    
    // 2
    asyncFetchRequest =
    NSAsynchronousFetchRequest<Venue>(
      fetchRequest: venueFetchRequest) {
        [unowned self] (result: NSAsynchronousFetchResult) in
        
        guard let venues = result.finalResult else {
          return
        }
        
        self.venues = venues
        self.tableView.reloadData()
      }
    
    // 3
    do {
      guard let asyncFetchRequest = asyncFetchRequest else {
        return
      }
      try coreDataStack.managedContext.execute(asyncFetchRequest)
      // Returns immediately, cancel here if you want
    } catch let error as NSError {
      print("Could not fetch \(error), \(error.userInfo)")
    }

코드 1을 보면 비동기로 수행되는 fetch request가 일반 fetch request를 대체하지 않는 것을 알 수 있다. 비동기 fetch request가 일반 fetch reqeust를 감싸는 형태이다.

NSAsynchronousFetchRequest 클래스의 이니셜라이저는 NSFetchRequest와 completionBlock을 필요로 한다. 그리고 가져온 결과는 NSAsynchronousFetchResult의 finalResult에 포함되어 있다.

init(fetchRequest: NSFetchRequest<ResultType>, completionBlock: ((NSAsynchronousFetchResult<ResultType>) -> Void)?)

따라서 코드 2에서 venueFetchRequest를 파라미터로 넣어주고, completionBlock에서 results.finalResult를 받아와 장소(self.venues)를 업데이트 해주고 있다.

비동기 fetch request를 실행하기 위해서는 따로 작업이 필요한데, 기존에 사용하던 context.fetch(:)가 아닌, context.execute(:)를 통해 비동기 fetch request를 실행할 수 있다.

Batch updates: fetch가 필요없는 업데이트

즐겨찾기 등록, 좋아요 표시 등 Core Data에서 가져온 object의 속성 하나만을 바꾸는 기능이 있다고 해보자. 우리는 이때 Core Data에서 object를 fetch하고, 속성을 변경 한 후 해당 object를 다시 저장소에 커밋한다. 굉장히 자연스러운 과정이다.

하지만 엄청나게 많은 수의 object를 한 번에 모두 업데이트 하려면 어떻게 해야할까? (예를 들면 메일 앱에서 '모두 읽음으로 표시' 기능을 구현한다던가..) 하나의 속성을 업데이트하기 위해 이 모든 object들을 가져오려면 정말 많은 시간과 메모리가 필요하다.

iOS 8부터 메모리에 아무것도 가져올(fetch) 필요 없이 Core Data object를 업데이트 하는 새로운 방법인 batch updates를 제공한다. batch updates는 NSManagedObjectContext를 우회(bypass)하고 저장소로 바로 이동하는데, 이를 통해 많은 양의 업데이트를 수행하는데 필요한 시간과 메모리를 크게 줄일 수 있다.

예제 코드를 살펴보자.

let batchUpdate = NSBatchUpdateRequest(entityName: "Venue")
batchUpdate.propertiesToUpdate = 
  [#keyPath(Venue.favorite): true]

batchUpdate.affectedStores = 
  coreDataStack.managedContext
    .persistentStoreCoordinator?.persistentStores

batchUpdate.resultType = .updatedObjectsCountResultType

do {
  let batchResult = 
    try coreDataStack.managedContext.execute(batchUpdate)
      as? NSBatchUpdateResult
  print("Records updated \(String(describing: batchResult?.result))")
} catch let error as NSError {
  print("Could not update \(error), \(error.userInfo)")
}

먼저 업데이트 하려는 Entity로 NSBatchUpdateRequest를 생성하고 batch update의 propertiesToUpdate에 [업데이트하려는 keyPath: 값] 형태로 dictionary를 넣어준다. 그 다음 affectedStores에 persistentStores를 넣어주자. 마지막으로 batch update를 실행시켜주면 된다.

코드에서 resultType을 .updatedObjectsCountResultType으로 설정해줬기 때문에 콘솔에는 Records updated 30가 찍힌다.

일괄 업데이트가 아닌 일괄 삭제를 위해서는 NSBatchDeleteRequest를 사용할 수 있다. 얘는 iOS 9부터 제공한다.

참고

raywenderlich - Core Data by Tutorials

profile
가장 젊은 지금, 내가 성장하는 데에 쓰자
post-custom-banner

0개의 댓글