→ Taster

[Project-Taster] SwiftData 마이그레이션

Swift librarian 2024. 10. 20. 16:54

🤔 문제 상황

가장 먼저 수정해야할 것은 모델이라고 느꼈다. 우선 모델이 중복되는 프로퍼티가 너무 많다고 느꼈고, 각 Type이나 Flavor, Color의 경우 저장하는 형식이 너무 비효율적이라고 생각했다.

Migration 문제

SwiftData를 사용한 앱이었기 때문에 custom Migration이 먼저 잘 되는지 테스트하기 위해서 코드를 작성했다.

enum MigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self]
    }
    
    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }
    
    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self,
        willMigrate: { context in
            do {
                let wineNotes = try context.fetch(FetchDescriptor<SchemaV1.WineNote>())
                let coffeeNotes = try context.fetch(FetchDescriptor<SchemaV1.CoffeeNote>())
                let whiskeyNotes = try context.fetch(FetchDescriptor<SchemaV1.WhiskeyNote>())
                let cocktailNotes = try context.fetch(FetchDescriptor<SchemaV1.CocktailNote>())
                
                migrate(from: wineNotes, by: context)
                migrate(from: coffeeNotes, by: context)
                migrate(from: whiskeyNotes, by: context)
                migrate(from: cocktailNotes, by: context)

                try context.save()
            } catch {
                print("Migration error: \(error)")
            }
        },
        didMigrate: nil
    )
}

enum SchemaV1: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [User.self, WineNote.self, CoffeeNote.self, WhiskeyNote.self, CocktailNote.self]
    }
    
    static var versionIdentifier = Schema.Version(1, 0, 0)
}

extension SchemaV1 {
    @Model final class User { ... }
    
    @Model final class WineNote { ... }
    
    @Model final class CoffeeNote { ... }

    @Model final class WhiskeyNote { ... }
    
    @Model final class CocktailNote { ... }
}

enum SchemaV2: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [WineTastingNote.self, CoffeeTastingNote.self, WhiskeyTastingNote.self, CocktailTastingNote.self]
    }
    
    static var versionIdentifier = Schema.Version(2, 0, 0)
}

extension SchemaV2 {
    protocol TastingNote { ... }
    
    @Model final class WineTastingNote: TastingNote { ... }
    
    @Model final class CoffeeTastingNote: TastingNote { ... }
    
    @Model final class WhiskeyTastingNote: TastingNote { ... }

    @Model final class CocktailTastingNote: TastingNote { ... }
    
    struct Ingredient: Hashable, Codable { ... }
}

아래처럼 마이그레이션이 전혀 되지 않는 문제 발생...

아래처럼 SwiftTest를 활용하여 테스트 코드도 만들어 보았으나...

struct TasterTests {
    @Test func example() async throws {
        let url = FileManager.default.temporaryDirectory.appending(component: "default.store")
        var container = try setupModelContainer(for: SchemaV1.self, url: url)
        var context = ModelContext(container)
        try loadSamleDataSchemaV1(context: context)
        let wineNotes = try context.fetch(FetchDescriptor<SchemaV1.WineNote>())
        
        container = try setupModelContainer(for: SchemaV2.self, url: url)
        context = ModelContext(container)
        
        let wineTastingNotes = try context.fetch(FetchDescriptor<SchemaV2.WineTastingNote>())
        
        #expect(wineTastingNotes.first?.think == "think")
    }

    
    private func loadSamleDataSchemaV1(context: ModelContext) throws { ... }
}

마이그레이션 자체가 잘 안되는 듯 했다... 분명 빌드는 오류없이 잘 되는데...!

가장 큰 문제 중 하나는 SwiftDataData Migration에 대한 정보가 많이 없다는 점이었다. SwiftDataMigration에는 lightweightcustom이 있었는데 ligthweight의 경우 쉽게 가능했는데 custom으로 하는 경우 아래와 같이 breakpoint를 찍어 확인해보니 print는 잘 되었는데... context.insert 부분이 잘 안되는 듯 했다...

🤓 해결 과정

🎉 Migration 성공!

해결방법은 생각보다 간단했다... willMigrate, didMigrate를 간과했는데 나는 context가 남아있는줄 알고, willMigrate에서 저장까지 하면 된다고 생각했다. 하지만 willMigrate가 된 후 context가 리셋되고, didMigrate를 할때 context가 다시 생겨나게 된다는 것을 알았다. 따라서 willMigrate단계에서 새로운 데이터 구조로 매핑을 해주고 didMigrateinsert해주면 되는 생각보다 간단한 문제였다...!

    private static var wineTastingNotes: [SchemaV2.WineTastingNote] = []
    private static var coffeeTastingNotes: [SchemaV2.CoffeeTastingNote] = []
    private static var whiskeyTastingNotes: [SchemaV2.WhiskeyTastingNote] = []
    private static var cocktailTastingNotes: [SchemaV2.CocktailTastingNote] = []
    
    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self,
        willMigrate: { context in
            let wineNotes = try context.fetch(FetchDescriptor<SchemaV1.WineNote>())
            let coffeeNotes = try context.fetch(FetchDescriptor<SchemaV1.CoffeeNote>())
            let whiskeyNotes = try context.fetch(FetchDescriptor<SchemaV1.WhiskeyNote>())
            let cocktailNotes = try context.fetch(FetchDescriptor<SchemaV1.CocktailNote>())
            
            wineTastingNotes = wineNotes.map { ... }
            coffeeTastingNotes = coffeeNotes.map { ... }
            whiskeyTastingNotes = whiskeyNotes.map { ... }
            cocktailTastingNotes = cocktailNotes.map { ... }
        }, didMigrate: { context in
            wineTastingNotes.forEach { context.insert($0) }
            coffeeTastingNotes.forEach { context.insert($0) }
            whiskeyTastingNotes.forEach { context.insert($0) }
            cocktailTastingNotes.forEach { context.insert($0) }
            
            try context.save()
        }
    )

우선 문제해결을 위해서 코드를 막(?) 작성했는데 조금더 디테일한 변경을 위한 수정이 필요하다. 다음 게시글에 모델을 어떻게 변경하는지에 대한 구체적인 과정을 담을 예정...!

 

우선 몇시간동안 붙잡고 있었던 문제가 해결되니 너무 좋다. 😇 솔직히 SwiftData를 포기하고 다른 저장방식을 사용할까도 고민했는데 잘 해결되었다...! 끝까지 붙잡고 있어서 다행이다.