본문 바로가기

학습 노트/iOS (2021)

197. Batch Processing with CoreData

Batch Processing with CoreData


CoreData에 저장하기 위해서는 Context를 생성하고,
save 메서드를 호출해 context의 내용을 CoreData에 반영하는 방법으로 저장해야 한다.

CoreData의 내용을 변경하기 위해서도 Context를 생성하고,
CoreData의 내용을 Context에 불러온 뒤, 변경된 Context의 내용을 다시 CoreData에 반영해야 한다.

문제는 이러한 방식이 많은 양의 데이터를 처리할 수록
처리 시간과 사용하는 자원의 소모가 커진다는 데 있다.

CoreData는 이러한 문제를 해결하기 위해 BatchUpdate와 BatchDelete를 제공한다.
이 기능들은 Context를 거치지 않고, 변경사항을 직접 CoreData에 반영함으로써
처리 시간과 사용되는 자원의 소모를 극적으로 줄일 수 있다.
다만, CoreData에 직접 반영한다는 부분 덕분에 검증(Validation)을 지원하지 않고,
데이터의 일관성 유지 측면에서 조금 더 신경 써야 한다는 단점이 있다.

메일 앱이나 문자 앱 등에서 '일괄'로 처리하는 동작에 주로 이용되는 기술이다.

xcdatamodeld

사용할 Entity를 만들어 준다.
Task라는 이름의 Entity로 Class 이름은 taskEntity이다.
포함된 Attribute는 다음과 같다.

  • done
    Boolean, non-Optional, Default Value = NO
  • date
    Date, non-Optional
  • task
    String, non-Optional

dataManager+BatchProcessing.swift

   func batchInsert() {
      mainContext.perform {
         for index in 0 ..< 10_000 {
            let newTask = TaskEntity(context: self.mainContext)
            newTask.task = "Task \(index + 1)"
            newTask.date = Date().addingTimeInterval(TimeInterval(3600 * 24 * Int.random(in: -365 ... 365)))
            
            if index % 1_000 == 0 {
               do {
                  try self.mainContext.save()
               } catch {
                  print(error.localizedDescription)
               }
            }
         }
         
         do {
            try self.mainContext.save()
            print("Inserted")
         } catch {
            print(error.localizedDescription)
         }
      }
   }

새롭게 저장할 Entity들은 무작위로 10000개가 생성된다.
이름은 index를 포함하고, date도 무작위 시간을 저장한다.
생성한 index가 1000개가 될 때마다 context의 내용을 적용하고,
생성이 끝난 이후 최종적으로 한 번 더 적용한다.

BatchUpdate

batchUpdate 메서드는 아직 완료하지 않은 Task를 '완료'로 변경한다.

   func batchUpdate() {
      let update = NSBatchUpdateRequest(entityName: "Task")
   }

BatchUpdate를 위한 인스턴스를 생성한다.
NSBatchUpdateRequest 생성자를 사용하고, 파라미터로 새로 생성한 Entity 이름인 Task를 전달한다.

   func batchUpdate() {
	   let update = NSBatchUpdateRequest(entityName: "Task")
	   update.propertiesToUpdate = [#keyPath(TaskEntity.done): true]
   }

 

proertiesToUpdate 속성에 업데이트 대상을 저장한다.
Task가 완료됐음을 표시하기 위해 'done' Attribute를 true로 변경한다.

   func batchUpdate() {
	   let update = NSBatchUpdateRequest(entityName: "Task")
	   update.propertiesToUpdate = [#keyPath(TaskEntity.done): true]
	   update.predicate = NSPredicate(format: "%K == NO", #keyPath(TaskEntity.done))
   }

상태를 변경해야 할 대상은 'done' Attribute가 'NO'로 돼 있는 task들 뿐이다.
predicate를 사용해 대상을 한정해 준다.
%K는 속성의 이름을 동적으로 할당할 때 사용한다.
여기서는 TaskEntity.done의 keyPath로 대체된다.

   func batchUpdate() {
	   let update = NSBatchUpdateRequest(entityName: "Task")
	   update.propertiesToUpdate = [#keyPath(TaskEntity.done): true]
	   update.predicate = NSPredicate(format: "%K == NO", #keyPath(TaskEntity.done))
	   update.resultType = .updatedObjectsCountResultType
   }

마지막으로 업데이트가 완료된 다음 반환받을 값을 지정한다.
resultType 속성을 updateObjectsCountResultType으로 설정할 경우
업데이트 완료한 속성의 수를 반환한다..
updatedObjectIDsResultType은 업데이트 완료한 속성의 타입을 반환한다

   func batchUpdate() {
	   let update = NSBatchUpdateRequest(entityName: "Task")
	   update.propertiesToUpdate = [#keyPath(TaskEntity.done): true]
	   update.predicate = NSPredicate(format: "%K == NO", #keyPath(TaskEntity.done))
	   update.resultType = .updatedObjectIDsResultType
	   
	   do {
		   if let result = try mainContext.execute(update) as? NSBatchUpdateResult, let cnt = result.result as? Int {
			   print("updated : \(cnt)")
		   }
	   } catch {
		   print(error.localizedDescription)
	   }
   }

최종적으로 완성된 BatchUpdate를 실행하면 된다.
판단 조건은 update request의 반환 형이 NSBatchUpdateResult일 것,
그리고 설정한 반환 값인 resultType의 반환이 성공적으로 이뤄졌을 경우다.

BatchDelete

batchDelete 메서드는 '완료' 표시된 task를 일괄로 삭제한다.

   func batchDelete() {
      let delete = NSBatchDeleteRequest(fetchRequest: )
   }

마찬가지로 BatchDelete를 위한 인스턴스를 생성해야 한다.
NSBatchDeleteRequest의 대상을 지정하기 위해서는 파라미터로 request를 전달해야 한다.

   func batchDelete() {
	   let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Task")
	   request.predicate = NSPredicate(format: "%K == YES", #keyPath(TaskEntity.done))
	   let delete = NSBatchDeleteRequest(fetchRequest: request)
   }

따라서 위와 같이 request를 생성하고 이를 전달한다.
delete 인스턴스 자체는 predicate를 적용할 수 없으므로,
실행할 request에 predicate를 적용해서 전달하는 방식으로 대상을 제한한다.

   func batchDelete() {
	   let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Task")
	   request.predicate = NSPredicate(format: "%K == YES", #keyPath(TaskEntity.done))
	   let delete = NSBatchDeleteRequest(fetchRequest: request)
	   delete.resultType = .resultTypeCount
   }

delete 인스턴스의 반환형을 지정해 준다.

   func batchDelete() {
	   let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Task")
	   request.predicate = NSPredicate(format: "%K == YES", #keyPath(TaskEntity.done))
	   let delete = NSBatchDeleteRequest(fetchRequest: request)
	   delete.resultType = .resultTypeCount
	   
	   do {
		   if let result = try mainContext.execute(delete) as? NSBatchDeleteResult, let cnt = result.result as? Int {
			   print("deleted : \(cnt)")
		   }
	   } catch {
		   print(error.localizedDescription)
	   }
   }

완성된 delete 인스턴스도 실행해 결과를 확인한다.


결과


동작은 하지만 UI에 제대로 반영이 되지 않는다.
이는 BatchUpdate와 BatchDelete가 Context를 거치지 않고 CoreData에 직접 작용하기 때문으로,
UI는 MainContext와 연결되어 있기 때문에 반영이 될 수 없다.

   func batchUpdate() {
	   DataManager.shared.batchUpdate()
	   
	   do {
		   try self.resultController.performFetch()
		   
		   if let list = tableView.indexPathsForVisibleRows {
			   tableView.reloadRows(at: list, with: .automatic)
		   }
	   } catch {
		   print(error.localizedDescription)
	   }
   }
   
   func batchDelete() {
	   DataManager.shared.batchDelete()
	   
	   do {
		   try self.resultController.performFetch()
		   
		   tableView.reloadData()
	   } catch {
		   print(error.localizedDescription)
	   }
   }

따라서 위와 같이 메서드를 호출한 이후,
Fetch를 다시 실행해 CoreData의 변경 사항을 Context에 적용하고,
이를 기반으로 UI를 업데이트해야 한다.


결과


작업이 완료되면 UI 또한 업데이트된다.

'학습 노트 > iOS (2021)' 카테고리의 다른 글

199. Context Synchronization  (0) 2022.06.29
198. Concurrency with Context  (0) 2022.06.09
196. Data Validation  (0) 2022.05.30
195. Faulting & Uniquing  (0) 2022.05.24
194. Transformable  (0) 2022.05.20