[iOS] CoreData Attribute 변경 시 나타나는 Migration 문제 해결(code=134140)
iOS Native 앱에서 서버로 관리할 필요가 없는 데이터, 또는 사용자 쪽에서 가지고 있으면 좋은 데이터들을 앱 자체의 영구 저장소인 CoreData에 적용하는 경우가 많다. 물론 Realm 같은 라이브러리를 사용할 수도 있지만, 내가 진행했던 프로젝트의 경우 일단은 iOS를 기반으로 만들어졌고, 다른 OS와의 호환을 고려하지 않았기에 일단은 iOS에서 더 안정적인 CoreData를 선택했었다.
앱이 업데이트되면서 Entity를 변경하거나 Attribute를 변경했어야 했다. CoreData는 version up 기능을 지원하는데, 이 과정에서 이전 저장소와 새 저장소를 구분하고, 변경된 Entity와 Attribute를 서로 호환시켜 주는 기능이 있다. 그것이 Migration인데, 이 기능을 적절히 사용하면, 기존 CoreData의 데이터 구조를 새롭게 디자인할 수도 있다.
이 과정에서 나타날 수 있는 문제가 code=134140 등의 문제이다. 만약, 없는 Entity를 추가해준다던지, Attribute를 추가해 주거나, Attribute의 이름만 바꿔주는 것이면 간단하게 version up과 mappingModel을 만들어 주기만 하면 호환이 되는데, 데이터 타입을 바꾼다던지 하면 문제가 일어날 수 있다. 이 문제에 대해 알아보자.
1. 구/신버전 CoreData 호환(Migration) 방법
새 버전을 만들 때, 기존의 CoreData의 데이터 모델을 바꿔서 새 버전을 만들 경우, 기존 버전을 가지고 있는 사용자가 새 버전을 받아 사용하면 충돌이 생긴다. 그렇기 때문에 Migration을 통해서 구/신버전의 저장소를 호환시켜 준다. Migration에는 LightWeight Migration이 있고 HeavyWeight Migration이 있다. 경량 마이그레이션은 몇 가지 설정만 해놓으면 자동으로 알아서 해주는 것이라고 생각하면 된다. 이에 더해 중량(수동) 마이그레이션은 자동으로 해주는 것 이외에 몇 가지 설정을 더 해주는 방법이라고 생각하면 된다.
1) LightWeight Migration 방법
(1) 데이터 모델 버전 설정
기존의 데이터 모델이 있을 것이다. 왼쪽 네비게이터의 DataModel을 클릭하고, 상단의 Editor/Add Model Version을 클릭한다.
만약 이 글을 보기 전에 기존의 CoreData의 Model을 바꾼 상황이라면, 이전 버전의 DataModel로 복구를 시켜놓고 진행해야 한다.
(2) 버전 추가
이런 화면이 나오게 되는데, 버전 이름을 설정해주고, Based on model은 "기준이 될" 데이터 모델(바로 직전 데이터 모델)을 설정해 주면 된다. 마이그레이션이 처음이라면 모델은 당연히 하나밖에 없을 것!
완료를 누르면 이렇게 데이터 모델 안에 두 가지의 데이터 모델이 생기게 된다.
(3) 새로운 데이터 모델 수정
v2를 이제 수정해보자
나의 경우 이전에 todayWorkoutHours라는 Attribute가 있었다. 시간을 String으로 저장했었는데, 여기에서 다시 Integer로 바꾸는 작업을 하고 싶었고, Workout이라는 엔티티를 새로 생성하였다.
(4) 새 버전으로 CoreData Model 설정
아무 Entity를 누르고 오른쪽 인스펙터를 열면, 이렇게 화면이 나온다. 아래의 Model Version이 보이면 클릭해 본다.
이렇게 새로 만든 버전이 보일 것이다. 그것을 클릭하면?
초록색 체크마크가 나타날 것이다. 자 이제 새로운 버전으로 데이터 모델이 업데이트 됐다. 그렇지만, 버전만 바꾼다면 프로젝트에서 알아서
'코어데이터 버전이 바뀌었네? 기존에 있던 속성은 새로운 버전의 속성에 이것이겠구나! 그러면 여기에 저장해야겠다!' 하는 것이 될까?
그렇지 않다. 하나의 과정이 더 남았다.
(5) Mapping Model 추가
새 파일을 추가해 준다.
아래로 내리다 보면 Core Data 카테고리에 Mapping Model이 있을 것이다. 이것을 생성해 준다.
Source Data Model을 설정하라고 한다. 이전 데이터 모델을 당연히 선택해 줘야겠다.
Target Data Model을 설정해 주라고 한다. 당연히 새로 만든 데이터 모델을 설정해 줘야겠다.
이름을 설정해 준다. 적절하게 인식할 수 있는 것으로 만들면 될 것이다. 버전에 맞게 네이밍 하는 것도 방법일 듯하다.
그러면 이렇게 MappingModel이 만들어진다. 엔티티를 잘 보면 UserToUser 이렇게 되어 있는 것을 볼 수 있고, Workout의 경우 새로 만들었기 때문에, Workout으로만 있는 것을 볼 수 있다. 무엇과 무엇이 연관이 되어 있다는 것을 추측해 볼 수 있으며, 실제로 연관되어 있다. 만약 엔티티 이름을 바꿨다면, 오른쪽 인스펙터에서 따로 설정을 해줄 수도 있다.
Destination을 바꾸거나 Source를 바꾸는 방법으로 정확한 엔티티를 설정해 줄 수 있을 것이다.
Mapping Model을 만드는 것으로 LightWeight Migration은 끝이 난 것이다!
! 그리고 새로운 엔티티와 모델에 맞추어 NSManagedObject Subclass를 새로 생성해줘야 하는 것을 잊지 않아야 한다.
2) HeavyWeight Migration이 필요한 경우
1-1)-(3)에서 본 것 같이 String에서 Int16으로 바뀐 todayWorkoutHours의 경우, 기존 프로젝트에서 "00h 00m" 이런 식으로 저장을 했기 때문에, 자동으로 Int로 변환이 되지 않는 것이 당연하다. 그렇기 때문에 속성 이름 변경 또는 추가 정도에 대응이 가능한 자동 마이그레이션으로 해결할 수 없다. 이 경우, 마이그레이션 정책을 따로 설정해 주고, 타입이 어떻게 변경될지를 결정해 주면 된다.
또 다른 예로는 Date를 String으로 저장한 경우가 있겠다. "yyyy/mm/dd" 이런 String 타입으로 저장했던 것을 Date로 바꿔서 저장하는 것을 생각할 수도 있다. 이 경우에도 HeavyWeight Migration이 필요하다. 아무튼 생각해 봤을 때, 간단하지 않은 데이터 변경의 경우, 머리가 아파지는 데이터 디자인 변경의 경우에 HeavyWeight Migration이 필요하다고 생각하면 된다.
3) HeavyWeight Migration에 필요한 NSEntityMigrationPolicy
복잡한 변경의 경우 "정책"을 설정해 주면 된다. 이것의 경우, NSEntityMigrationPolicy 클래스를 상속하여 새로운 커스텀 클래스를 만들어 타입을 변경해 주는 함수를 구현하면 된다.
이런 식으로 구현하였는데, 파일은 CoreDataManager가 있는 폴더에 파일을 넣어두었다. 이것은 폴더링에 따라 개발자가 알아서 선택하면 된다.
코드에 대한 자세한 설명을 해보겠다.
(1) Policy 클래스
final class ProductMigrationPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
if sInstance.entity.name == "User" {
let goalCalories = sInstance.value(forKey: "goalCalories") as? Int16
let todayCalories = sInstance.value(forKey: "todayCalories") as? Int16
let todayWorkoutHours = sInstance.value(forKey: "todayWorkoutHours") as? String
let totalDumbbell = sInstance.value(forKey: "totalDumbbell") as? Int16
let userID = sInstance.value(forKey: "userID") as? UUID
let userName = sInstance.value(forKey: "userName") as? String
let newProductEntity = NSEntityDescription.insertNewObject(forEntityName: "User", into: manager.destinationContext)
newProductEntity.setValue(goalCalories, forKey: "goalCalories")
newProductEntity.setValue(todayCalories, forKey: "todayCalories")
newProductEntity.setValue(todayWorkoutHours?.stringDateToInt16(), forKey: "todayWorkoutHours")
newProductEntity.setValue(totalDumbbell, forKey: "totalDumbbell")
newProductEntity.setValue(userID, forKey: "userID")
newProductEntity.setValue(userName, forKey: "userName")
}
}
}
클래스의 이름은 자유롭게 식별할 수 있는 것으로 하면 된다. 단, NSEntityMigrationPolicy를 꼭 상속해주어야 한다.
그러고 createDestinationInstances 함수를 override 해준다. 그러고 sIntsance의 엔티티 중에 타입 변경이 일어나는 엔티티를 조건문으로 골라내서 새로운 객체로 변환해 주는 로직을 넣어준다.
todayWorkoutHours와 관련된 것만 잘 보자!. 원래 String이었던 탓에 일단 처음에 value를 가져올 때는 String으로 캐스팅하여 가져온다. 이것을 newProductEntity에 setValue 할 때는 Int로 변환하여 세팅한다. todayWorkoutHours가 String이기 때문에, String의 Extention을 구현하여 간단히 메서드로 만들었다.
extension String {
func stringDateToInt16() -> Int16? {
let result = self.components(separatedBy: " ")
let numberStrings = result.filter { str in
return CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: str))
}
return Int16(numberStrings.compactMap { Int($0) }.reduce(0, +))
}
}
나의 경우 원래 값이 "00h 00m"이었기 때문에, 시간을 분으로 바꿔주는 코드를 삽입하여 리턴하는 것으로 함수를 만들었다. 이렇게 하면 Int16 옵셔널 값이 리턴이 될 것이고, Policy에서 새로운 객체가 만들어지고 호환이 되게끔 처리가 될 수 있다.
그리고 forKey라는 파라미터에는 존재하는 속성의 이름을 정확히 집어넣어야 된다!
이렇게 커스텀 클래스를 구현해 주고, 마지막으로 해줘야 할 것이 있다.
이전에 만들었던 Mapping Model을 클릭하여 오른쪽 인스펙터에서 Custom Policy를 입력해주어야 한다.
프로젝트 이름은 왼쪽 네비게이터에서 확인할 수 있고, 아까 만든 Policy 클래스의 명을 적어주면 된다.
이렇게 하면 정상적으로 HeavyWeight Migration이 진행되고, 정상적으로 빌드/실행이 되는 것을 볼 수 있다.
2. 정리
CoreData의 디자인 변경에는 Migration 기능을 사용하면 용이하게 되는 부분이 많다. 다만, 수동 마이그레이션을 해야 하는 경우를 잘 살펴보고, 데이터 타입을 좀 더 세밀하게 만져야 할 때는 정책 클래스를 따로 만들어서 연결해주어야 한다. 이렇게 하면 기존의 버전과 새로운 버전이 충돌하지 않고 잘 연동되게 할 수 있다.
'iOS' 카테고리의 다른 글
[iOS] 커스텀 버튼 컴포넌트 하이라이트 처리 (0) | 2024.01.08 |
---|---|
[iOS] MapKit 주소 자동검색 구현하기 (0) | 2024.01.07 |
[Swift] init과 Conveience init 그리고 ? (0) | 2023.10.14 |
[UIKit] collectionView 델리게이트 활용 시 주의사항 (0) | 2023.10.10 |
[Xcode] The file “.swiftlint.yml” couldn’t be opened because you don’t have permission to view it. (0) | 2023.10.07 |
댓글