본문 바로가기

학습 노트/iOS (2021)

201. Migration

Migration


CoreData는 Stack을 초기화할 때마다 데이터 모델을 검증한다.

 

Light Weight Migration (Automatic Migration)

LWM은 Core Data의 기본 기능이며, 아래의 방식으로 동작한다.

  • 실제 저장소에 저장된 파일과 데이터 모델이 동일하면 Stack을 초기화한다.
  • 실제 저장소에 저장된 파일과 데이터 모델이 동일하지 않다면 차이점을 분석한 후 mapping 모델을 생성한다.
  • 자동으로 생성한 mapping 모델을 사용해 데이터 구조를 바꿀 수 있다면 적용해 기존의 데이터를 새로운 모델로 이전하고, Stack을 초기화한다.

또한 이전 방식에 따라 다음의 조건을 따른다.

  • Non-Optional Attr에서 Optional Attr로 이전하는 경우 제약이 존재하지 않는다.
  • Optional Attr 에서 Non-Optional Attr로 이전하는 경우 기본값을 설정해야 한다.
  • Non-Optional Attr을 추가하는 경우 기본값을 설정해야 한다.
  • Attr을 삭제하는 경우 데이터 일관성이 유지되는 경우에 한해 이전할 수 있다.
  • Entity의 이름과 Attr의 이름을 바꾸는 경우 renamingIdentifier를 사용한다.
  • ...

생성한 mapping 모델을 적용할 수 없거나,
위의 조건을 만족하지 못하는 경우 다른 방식의 Migration을 진행해야 한다.

 

Standard Migration (Manual Migration)

동작은 다음과 같다.

  • 새로운 데이터 모델을 구성한다.

  • 기존에 존재하던 데이터를 그대로 복사한다.

  • Original Store의 연결 정보를 기반으로 데이터를 연결한다.
  • Original Store의 데이터와 New Store의 데이터를 비교하며 Validation을 진행한다.

  • 기존의 데이터를 삭제한다.

 

실습

앱을 삭제하고 다시 실행한 뒤 Batch Insert를 실행한 상태에서 진행한다.

Task Entity에 latitude라는 Attr을 추가한다.
Type은 Double, Optional, Default = 0로 설정했다.

이렇게 추가한 latitude는 Optional의 성격을 가지고 있고, 기본값이 존재하므로 Auto Migration이 작동한다.
만약 Optional을 해제하고, 기본값 마저 없다면 Auto Migration을 진행할 수 없고, Migration에 실패하게 된다.
이는 데이터의 신뢰도가 낮아지며, Fatal Error 등으로 처리하지 않으면 당장은 문제가 없어 보이지만 앱의 정상적인 작동을 보장할 수 없다.

Migration Scene은 Employee 데이터의 이름만 TableView에 표시하지만,
이를 위해 name Attr에 접근할 필요가 있고, 이때 발생하는 Faults로 인해 실제로는 사용하지 않는
photo Attr의 사진까지 전부 불러오게 돼 메모리 낭비가 발생한다.

이를 해결하기 위해 photo Attr을 별도의 Entity로 구성하고,
이를 Employee에 Relationship으로 연결하는 방식으로 변경해 본다.

dataModel 파일을 선택하고 'Editor > add Model Version'
을 선택해 기존의 dataModel을 기반으로 하는 새로운 dataModel을 생성한다.

이러한 방식은 앱을 출시한 후 DataModel을 변경해야 하는 경우 사용한다.

DataModel 파일을 선택하고 File Inspector에서 Current를 새로운 DataModel로 변경한다.

새로 생성한 버전의 DataModel 파일에
새로운 'Photo' Entity를 생성하고 Class 명을 수정한 뒤,
photo Attr을 추가한다.
속성은 Binary Data, Optional, Allows External Storage를 사용하도록 설정한다.

EmployeeEntity의 photo Attr을 삭제하고.
Relationship을 추가한다.
이름은 profile, Destination은 Photo이다.
이후 Employee Entity의 Codegen 설정을 'Class Definition'으로 변경하고
기존의 설정값을 위한 파일인 CoreDataClass 파일과 CoreDataProperties 파일을 삭제한다.

이후 실행했을 때 발생하는 ValidationViewController.swift 파일의 오류 부분을 주석 처리하고 실행한다.
이렇게 하면 앱은 정상적으로 실행된다.

지금까지의 과정으로 EmployeeEntity의 photo Attr을 삭제하고,
새로운 Photo Enity가 생성됐다.
하지만 새로 생긴 Photo Entity로 데이터를 복사하지는 못하는데,
이렇게 되는 경우 이전의 photo 데이터가 삭제되고, 복구가 불가능해진다.

따라서 우리는 Photo Entity로 데이터를 복사할 수 있도록 Migration 규칙을 생성할 필요가 있다.

다시 원래의 DataModel을 선택하고 Codegen을 'Class Definition'으로 변경한 뒤
'product > clean Build Folder'를 선택해 초기화하고 앱을 실행한다.
Migration Scene의 '+'를 터치해 삭제된 사진들을 복구하고 규칙을 생성하도록 한다.

다시 새로운 버전의 Data Model로 돌아와서 Shared에 새로운 파일을 추가한다.
파일은 mappingModel 파일로 mapping 규칙을 설정하게 될 파일이다.
첫 번째로 원본 Model을 선택하고, 두 번째로 대상 Model을 선택하면 된다.

해당 파일의 Photo Entity를 확인해 보면 다른 Entity와는 다르게 Value Expression이 비어있는 것을 확인할 수 있다.
이는 Photo Entity가 원본 Model에는 존재하지 않는 Entity이기 때문에 추론을 지원하지 않기 때문이며,
이를 위해 정책을 직접 구현할 필요가 있다.

정책을 작성할 새로운 swift 파일을 생성한다.

//
//  PhotoMigrationV1toV2.swift
//  CoreData
//
//  Created by Martin.Q on 2022/07/06.
//

import Foundation
import CoreData

@objc(PhotoMigrationV1toV2)
class PhotoMigrationV1toV2: NSEntityMigrationPolicy {
	
}

CoreData를 import 하고 NSEntityMigrationPolicy를 상속하는 새로운 class를 생성한다.
해당 Class는 objc 특성을 추가해 준다.

//
//  PhotoMigrationV1toV2.swift
//  CoreData
//
//  Created by Martin.Q on 2022/07/06.
//

import Foundation
import CoreData

@objc(PhotoMigrationV1toV2)
class PhotoMigrationV1toV2: NSEntityMigrationPolicy {
	override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
		
	}
}

메서드를 하나 추가한다.
해당 메서드는 migration 과정 중 새 Entity를 생성할 때마다 반복적으로 호출된다.
param1: Source의 데이터
param2: mapping 정보
param3: mapping을 실행하는 manager

해당 메서드 내의 코드는 반드시 param3의 manager가 제공하는 context를 사용해야 한다.

//
//  PhotoMigrationV1toV2.swift
//  CoreData
//
//  Created by Martin.Q on 2022/07/06.
//

import Foundation
import CoreData

@objc(PhotoMigrationV1toV2)
class PhotoMigrationV1toV2: NSEntityMigrationPolicy {
	override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
		try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
		
		if let photo = sInstance.value(forKey: "photo") as? Data {
			
		}
	}
}

상위 구현을 호출하고 작성을 시작한다.
첫 번째 조건은 원본에 'photo'가 존재하는지를 확인한다.
존재한다면 새로 생성하고, employee 객체와 연결하게 된다.

//
//  PhotoMigrationV1toV2.swift
//  CoreData
//
//  Created by Martin.Q on 2022/07/06.
//

import Foundation
import CoreData

@objc(PhotoMigrationV1toV2)
class PhotoMigrationV1toV2: NSEntityMigrationPolicy {
	override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
		try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
		
		if let photo = sInstance.value(forKey: "photo") as? Data {
			let context = manager.destinationContext
		}
	}
}

새로운 객체의 생성은 source 가 아닌 target의 context에서 진행해야 한다.

//
//  PhotoMigrationV1toV2.swift
//  CoreData
//
//  Created by Martin.Q on 2022/07/06.
//

import Foundation
import CoreData

@objc(PhotoMigrationV1toV2)
class PhotoMigrationV1toV2: NSEntityMigrationPolicy {
	override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
		try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
		
		if let photo = sInstance.value(forKey: "photo") as? Data {
			let context = manager.destinationContext
			
			let newPhoto = NSEntityDescription.insertNewObject(forEntityName: "Photo", into: context)
			newPhoto.setValue(photo, forKey: "photo")
			
			print("New Photo")
		}
	}
}

새 Photo 객체를 생성한다.
반드시 NSManagedObject나 NSManagedDescription이 제공하는 API를 사용해야 한다.

//
//  PhotoMigrationV1toV2.swift
//  CoreData
//
//  Created by Martin.Q on 2022/07/06.
//

import Foundation
import CoreData

@objc(PhotoMigrationV1toV2)
class PhotoMigrationV1toV2: NSEntityMigrationPolicy {
	override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
		try super.createDestinationInstances(forSource: sInstance, in: mapping, manager: manager)
		
		if let photo = sInstance.value(forKey: "photo") as? Data {
			let context = manager.destinationContext
			
			let newPhoto = NSEntityDescription.insertNewObject(forEntityName: "Photo", into: context)
			newPhoto.setValue(photo, forKey: "photo")
			
			print("New Photo")
			
			let destResults = manager.destinationInstances(forEntityMappingName: mapping.name, sourceInstances: [sInstance])
			if let employee = destResults.last {
				employee.setValue(newPhoto, forKey: "profile")
				print("employee -> photo")
			}
		}
	}
}

새 Photo 객체와 Employee 객체를 연결한다.
param1의 객체는 원본 객체이기 때문에 연결이 불가능하다.
따라서 연결에 필요한 새로운 객체는 manager를 통해서 얻어야 한다.

destination context에 생성된 객체를 배열로 반환하도록 구현한다.
param1: mapping 이름
param2: 원본 객체

최종적으로 반환 갑에 접근해 profile relationship으로 연결하도록 구현한다.

완성된 정책을 사용할 수 있도록 mappingModel에서 EmployeeToEmployee의 Cutom Policy에 적용한다.

   override func viewDidLoad() {
      super.viewDidLoad()
      
      if let target = target {
         if let data = target.value(forKeyPath: "profile.photo") as? Data {
            profileImageView.image = UIImage(data: data)
         }
         
         deptLabel.text = target.value(forKeyPath: "department.name") as? String
         salaryLabel.text = formatter.string(for: target.value(forKey: "salary"))
      }
   }
}

최종적으로 profile을 받아 올 수 있도록 접근 경로를 변경해 준다.


결과


앱을 시작하자마자 많은 양의 로그를 표시하며 migration을 진행하고,
문제가 됐던 Migration Scene에 진입해도 이전의 190MB가 아닌 50MB 수준으로 효율적으로 사용하고 있는 것을 확인할 수 있다.

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

203. Display Web Contents  (0) 2022.07.06
202. Networking  (0) 2022.07.06
200. Performance & Debugging  (0) 2022.06.30
199. Context Synchronization  (0) 2022.06.29
198. Concurrency with Context  (0) 2022.06.09