[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/SwiftData & CoreData

[SwiftData] Migration과 버전관리

inu 2023. 8. 3. 18:33
반응형

본 게시글은 WWDC 2023의 Model your schema with SwiftData 세션의 내용을 정리한 것입니다.

https://developer.apple.com/videos/play/wwdc2023/10195


Modeling 복습

  • SwiftData를 Import하고 class에 @Model 매크로를 부여하는 것으로 스키마를 생성할 수 있다.
  • @Attribute(.unique) 매크로를 통해 특정 속성을 유니크하게 만들 수 있다. 만약 이미 존재하는 프로퍼티를 Insert하려 하면 기존에 존재하던 데이터를 업데이트한다. (upsert)
  • 기존에 존재하던 속성의 이름을 그냥 바꾸면 단순히 새로운 속성만 생성되고 이전 속성의 데이터는 모두 삭제된다. 이름을 바꾸고 기존의 데이터를 보유하기 위해서는 @Attribute(originalName: "oldName") 매크로를 속성에 적용해주어야한다.
  • 이 외에도 @Attribute 는 대용량 데이터를 외부에 저장하거나 Tansformable을 지원하는 등 다양한 기능을 지원한다.
  • SwiftData는 같은 Model Class의 프로퍼티를 Relationship으로 인식한다. 이러한 Relationship에 아무런 옵션을 주지 않으면 해당 관계를 가진 모델의 데이터가 삭제되었을 때 해당 부분은 nil값으로 전환된다.
  • @Relationship(.cascade) 매크로를 적용하면 관계를 가진 모델의 데이터가 삭제되면 해당 데이터도 삭제된다.
  • 이 외에도 @Relationship 은 originalName를 수정하거나 최소 및 최대 개수 제약조건을 부여하는 등의 기능을 지원한다.
  • 스키마에서 특정 속성을 제외하고 싶다면 @Transient 매크로를 부여하면 된다. (기본값 필수)

Migration

프로퍼티를 제거하거나 추가하는 등 스키마에 변경을 가하면 Data Migration이 발생한다. 마이그레이션은 꽤나 복잡한 작업이지만, VersionedSchema 와 SchemaMigrationPlan 이 이를 간편하게 수행할 수 있도록 도와준다.

 

VersionedSchema를 통해 이전에 사용하던 스키마를 캡슐화한다. 이렇게 버전을 관리함으로써 SwiftData가 스키마간의 변경사항을 인지할 수 있다. 그리고 이를 기반으로 SchemaMigrationPlan을 생성하여 마이그레이션을 어떻게 수행할지 정의한다.

 

마이그레이션에는 두가지 타입이 존재한다.

  • LightWeight Migration : 단순히 속성을 추가하거나 삭제 규칙을 지정하는 등 기존 데이터에 영향을 주지 않는 변경이 이에 해당한다. 기존 데이터를 마이그레이션하는 추가 코드가 필요하지 않다.
  • Custom Migration : 특정 속성을 Unique하게 바꾸는 등의 작업을 수행하는 경우 이에 해당한다.

Custom Migration 과정

아래는 Custom Migration의 과정이다.

 

먼저 스키마의 각 버전은 VesionedSchema enum안으로 넣는다.

enum SampleTripsSchemaV1: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }

    @Model
    final class Trip {
        var name: String
        var destination: String
        var start_date: Date
        var end_date: Date

        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }

    // Define the other models in this version...
}

enum SampleTripsSchemaV2: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }

    @Model
    final class Trip {
        @Attribute(.unique) var name: String
        var destination: String
        var start_date: Date
        var end_date: Date

        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }

    // Define the other models in this version...
}

enum SampleTripsSchemaV3: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }

    @Model
    final class Trip {
        @Attribute(.unique) var name: String
        var destination: String
        @Attribute(originalName: "start_date") var startDate: Date
        @Attribute(originalName: "end_date") var endDate: Date

        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }

    // Define the other models in this version...
}

그리고각 버전 변경에 대한 수정을 직접 Swift 코드로 작성한다.

enum SampleTripsMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SampleTripsSchemaV1.self, SampleTripsSchemaV2.self, SampleTripsSchemaV3.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: SampleTripsSchemaV1.self,
        toVersion: SampleTripsSchemaV2.self,
        willMigrate: { context in
            let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV1.Trip>())

            // De-duplicate Trip instances here...

            try? context.save() 
        }, didMigrate: nil
    )

    static let migrateV2toV3 = MigrationStage.lightweight(
        fromVersion: SampleTripsSchemaV2.self,
        toVersion: SampleTripsSchemaV3.self
    )
}

Migration이 필요한 데이터는 Container에서 불러올 때 MingrationPlan을 추가해주어야 한다.

반응형