본문 바로가기

iOS

[Realm] Migration (실습)

728x90

앞선 글에서 마이그레이션이 무엇인지, 디버깅 모드에서 쉽게 마이그레이션 하는 방법과 현재 스키마 버전을 알아보는 과정까지 다루었다.

이번 글에서는 실제 프로젝트에서 DB 구조를 바꿔가면서 마이그레이션을 진행해보자! 아자자!

 

 

실습을 진행할 프로젝트 .. 는 메모앱 ~ 당첨 .. ~~

https://github.com/pcsoyeon/SSAC-Memo

 

GitHub - pcsoyeon/SSAC-Memo: 서브웨이 이탈리안 비엠티에 .. 에그마요 추가 .. (메 .. 모 .... )

서브웨이 이탈리안 비엠티에 .. 에그마요 추가 .. (메 .. 모 .... ). Contribute to pcsoyeon/SSAC-Memo development by creating an account on GitHub.

github.com

 

코드를 작성하기 전에 !!!

마이그레이션 관련 코드는 앱을 실행할 때 진행이 되어야 하므로 AppDelegate에서 관리한다.

 

 

#Automatically  Update Schema

단순히 기존 DB 테이블 구조에서 새로운 컬럼을 추가하거나 삭제할 때는 별도의 마이그레이션을 진행하지 않아도(코드를 작성하지 않아도) 스키마 버전 숫자만 올려주면 자동으로 마이그레이션이 진행된다.

 

일단, 현재 테이블 구조는 아래와 같다.

objectId memoTitle memoContent memoDate isPinned
         

 

Realm Browser에서 확인하면 아래와 같다.

 

앱에서도 이렇게 두개의 메모가 저장되어 있다. (처음이라서 앱 사진을 넣었지만 .. 아마 아래로 내려가면 .. 앱 사진보다는 Realm Browser 캡처 화면으로 마이그레이션이 적용된 것을 확인할 예정 ~ .. )

 

 

그리고 현재 스키마 버전은 0인 것을 확인할 수 있다.

 

 

#컬럼 추가 및 삭제

✔️ Realm Schema Version 0

objectId memoTitle memoContent memoDate isPinned
         

 

✔️ Realm Schema Version 1 - 컬럼 추가 

objectId memoTitle memoContent memoDate isPinned isFavorite
           

버전2에서 버전1에 없던 새로운 컬럼인 isFavorite을 추가해보자.

단순히 새로운 컬럼을 추가하는 경우이므로 별도로 마이그레이션을 하지 않더라도 스키마 버전 숫자만 올려주면 자동으로 마이그레이션이 진행된다.

 

코드를 보면 아래와 같다.

 

AppDelegate.swift

extension AppDelegate {
    func aboutRealmMigration() {
        // ✅ schemaVersion 수정
        let config = Realm.Configuration(schemaVersion: 1) { migration, oldSchemaVersion in
            // ✅ 0 -> 1로 업데이트, 새로운 컬럼 추가
            if oldSchemaVersion < 1 { 
                // 새로운 컬럼 추가는 따로 코드를 구현하지 않아도 된다.
            }
        }
        
        Realm.Configuration.defaultConfiguration = config
    }
}

 

그리고 빌드를 해서 확인하면, 아래와 같이 스키마 버전이 업데이트 된 것을 확인할 수 있고 

 

 

 

Realm Brower를 통해서 DB 테이블 구조를 확인하면 아래와 같이 

새로운 컬럼이 추가된 것을 볼 수 있다.

 

 

✔️ Realm Schema Version 2 - 컬럼 삭제 

개발을 하면서 생각해보니 .. isFavorite과 isPinned가 비슷한 속성이라서 .. 필요가 없다고 느끼고 삭제를 한다면? 이 역시 컬럼 추가와 마찬가지로 단순 새로운 컬럼만 삭제하는 것이므로 별도 마이그레이션을 진행하지 않더라도 스키마 버전 숫자만 올려주면 된다.

 

주의할 점은 이전으로 돌아간다고 해서 (테이블 구조를 보게 되면 1에서 0으로 돌아간다고 생각할 수 있다.) 스키마 버전을 다운하는 것은 아니다.

마이그레이션 진행 시 이전 스키마 버전과 동일한 스키마 구조가 되더라도, 스키마 버전은 항상 상위 버전으로 증가해야 한다.

 

objectId memoTitle memoContent memoDate isPinned
         

위와 같은 구조로 변경하는 것이다. 

 

코드를 보면 아래와 같다.

1에서 2로 업데이트 했으므로 스키마 버전을 수정하고 아래에 코드 블럭을 만들면 된다.

extension AppDelegate {
    func aboutRealmMigration() {
        // ✅ schemaVersion 수정 
        let config = Realm.Configuration(schemaVersion: 2) { migration, oldSchemaVersion in
            // 0 -> 1로 업데이트, 새로운 컬럼 추가
            if oldSchemaVersion < 1 {
                // 새로운 컬럼 추가는 따로 코드를 구현하지 않아도 된다.
            }
            
            //  ✅ 1 -> 2로 업데이트, 컬럼 삭제
            if oldSchemaVersion < 2 {
                // 컬럼 삭제는 따로 코드를 구현하지 않아도 된다.
            }
        }
        
        Realm.Configuration.defaultConfiguration = config
    }
}

 

그리고 빌드를 하면 아래와 같이 스키마 버전이 업데이트 되고

Realm Brower로 확인하면 컬럼이 삭제된 것을 볼 수 있다.

 

#Manually  Update Schema

위의 과정은 코드 블럭 내에서 별도의 작업 없이 스키마 버전을 올려주면 자동으로 마이그레이션이 진행되었다.

그러나 아래의 과정부터는 별도의 작업이 필요한 경우이다. 

 

✔️ Realm Schema Version 3 - 컬럼명 변경 

isPinned라고 설정했던 컬럼명을 isFixed로 수정하고 싶다면?

 

objectId memoTitle memoContent memoDate isFixed 
         

위와 같은 테이블 구조로 변경하는 것이다.

 

마이그레이션을 하는 동안 컬럼명이 변경된다면, Migration.renameProperty(onType: from: to:) 메서드를 활용한다.

 

 

코드를 보면 아래와 같다. 

데이터 구조는 아래와 같이 변경 되고 

import Foundation

import RealmSwift

class Memo: Object {
    @Persisted var memoTitle: String
    @Persisted var memoContent: String?
    @Persisted var memoDate = Date()
    @Persisted var isFixed: Bool // ✅ 
    
    @Persisted(primaryKey: true) var objectId: ObjectId
    
    convenience init(memoTitle: String, memoContent: String?, memoDate: Date, isPinned: Bool = false) {
        self.init()
        self.memoTitle = memoTitle
        self.memoContent = memoContent
        self.memoDate = memoDate
        self.isFixed = isPinned // ✅
    }
}

 

AppDelegate.swift

extension AppDelegate {
    func aboutRealmMigration() {
        // ✅ 
        let config = Realm.Configuration(schemaVersion: 3) { migration, oldSchemaVersion in
            // 0 -> 1로 업데이트, 새로운 컬럼 추가
            if oldSchemaVersion < 1 {
                // 새로운 컬럼 추가는 따로 코드를 구현하지 않아도 된다.
            }
            
            // 1 -> 2로 업데이트, 컬럼 삭제
            if oldSchemaVersion < 2 {
                // 컬럼 삭제는 따로 코드를 구현하지 않아도 된다.
            }
            
            // ✅ 2 -> 3으로 업데이트, 컬럼명 변경
            if oldSchemaVersion < 3 {
                // onType: 테이블 이름
                // from: 이전 컬럼명
                // to: 바꾸고 싶은 컬럼명
                migration.renameProperty(onType: Memo.className(), from: "isPinned", to: "isFixed")
            }
        }
        
        Realm.Configuration.defaultConfiguration = config
    }
}

-> 이 때, 오타가 나지 않도록 주의한다 !!! (오타나면 인식하지 못해서 런타임 오류가 발생한다.)

 

 

빌드해서 확인하면 아래와 같다.

스키마 버전과 함께 isPinned 컬럼명이 isFixed로 변경된 것을 확인할 수 있다. 

 

 

✔️ Realm Schema Version 4 - 컬럼 결합 (컬럼 합치기)

두가지 컬럼을 합쳐 새로운 컬럼을 만들고자 한다면?

objectId memoTitle memoContent memoDate isFixed 
         

기존의 위의 테이블 구조에서 memoTitle, memoContent, memoDate를 합쳐서 새로운 컬럼인 memoInfo에 넣는다고 할 때

objectId memoTitle memoContent memoDate isFixed memoInfo
           

컬럼을 결합하는 경우는 enumerateObjects를 활용하면 된다.

 

코드를 보면 

데이터 구조는 아래와 같이 수정하면 되고,

import Foundation

import RealmSwift

class Memo: Object {
    @Persisted var memoTitle: String
    @Persisted var memoContent: String?
    @Persisted var memoDate = Date()
    @Persisted var isFixed: Bool
    @Persisted var memoInfo: String
    
    @Persisted(primaryKey: true) var objectId: ObjectId
    
    convenience init(memoTitle: String, memoContent: String?, memoDate: Date, isPinned: Bool = false) {
        self.init()
        self.memoTitle = memoTitle
        self.memoContent = memoContent
        self.memoDate = memoDate
        self.isFixed = isPinned
    }
}

 

AppDelegate.swift로 가서 스키마 버전을 올리고 대응을 하면 된다.

(점점 코드가 길어지니 .. 변경/추가된 부분은 ✅ 표시를 보면 된다.)

extension AppDelegate {
    func aboutRealmMigration() {
        // ✅ 
        let config = Realm.Configuration(schemaVersion: 4) { migration, oldSchemaVersion in
            // 0 -> 1로 업데이트, 새로운 컬럼 추가
            if oldSchemaVersion < 1 {
                // 새로운 컬럼 추가는 따로 코드를 구현하지 않아도 된다.
            }
            
            // 1 -> 2로 업데이트, 컬럼 삭제
            if oldSchemaVersion < 2 {
                // 컬럼 삭제는 따로 코드를 구현하지 않아도 된다.
            }
            
            // 2 -> 3으로 업데이트, 컬럼명 변경
            if oldSchemaVersion < 3 {
                // onType: 테이블 이름
                // from: 이전 컬럼명
                // to: 바꾸고 싶은 컬럼명
                migration.renameProperty(onType: Memo.className(), from: "isPinned", to: "isFixed")
            }
            
            // ✅ 3 -> 4로 업데이트, 컬럼 결합
            if oldSchemaVersion < 4 {
                migration.enumerateObjects(ofType: Memo.className()) { oldObject, newObject in
                    guard let new = newObject else { return } // 새롭게 생길 컬럼
                    guard let old = oldObject else { return } // 기존의 컬럼 
                    
                    new["memoInfo"] = "제목: \(old["memoTitle"]!), 내용: \(old["memoContent"]!), 날짜: \(old["memoDate"]!)"
                }
            }
        }
        
        Realm.Configuration.defaultConfiguration = config
    }
}

 

 

빌드를 해서 스키마 버전과 테이블 구조를 확인하면 아래와 같다.

가장 오른쪽에 memoTitle과 memoContent 그리고 memoDate가 합쳐진 새로운 컬럼이 추가된 것을 확인할 수 있다.

 

 

✔️ Realm Schema Version 5 - 초기값이 있는 새로운 컬럼 추가 

새로운 컬럼을 추가할 때, 이 컬럼에 초기값이 필요한 경우라면 이 역시 위에서 사용한 enumerateObjects 메서드를 사용하면 된다.

 

objectId memoTitle memoContent memoDate isFixed memoInfo
           

기존의 위와 같은 테이블 구조에서 

 

objectId memoTitle memoContent memoDate isFixed memoInfo count
            127

위의 구조로 새로운 컬럼이 추가될 때, 초기값을 모두 127로 갖고 있는 형태를 만들어보자.

 

 

마찬가지로 데이터 모델 코드와 마이그레이션 코드를 수정하면 된다.

Memo.swift

import Foundation

import RealmSwift

class Memo: Object {
    @Persisted var memoTitle: String
    @Persisted var memoContent: String?
    @Persisted var memoDate = Date()
    @Persisted var isFixed: Bool
    @Persisted var memoInfo: String
    
    @Persisted var count: Int // ✅
    
    @Persisted(primaryKey: true) var objectId: ObjectId
    
    convenience init(memoTitle: String, memoContent: String?, memoDate: Date, isPinned: Bool = false) {
        self.init()
        self.memoTitle = memoTitle
        self.memoContent = memoContent
        self.memoDate = memoDate
        self.isFixed = isPinned
    }
}

 

AppDelegate.swift

extension AppDelegate {
    func aboutRealmMigration() {
        // ✅ 
        let config = Realm.Configuration(schemaVersion: 5) { migration, oldSchemaVersion in
            // 0 -> 1로 업데이트, 새로운 컬럼 추가
            if oldSchemaVersion < 1 {
                // 새로운 컬럼 추가는 따로 코드를 구현하지 않아도 된다.
            }
            
            // 1 -> 2로 업데이트, 컬럼 삭제
            if oldSchemaVersion < 2 {
                // 컬럼 삭제는 따로 코드를 구현하지 않아도 된다.
            }
            
            // 2 -> 3으로 업데이트, 컬럼명 변경
            if oldSchemaVersion < 3 {
                // onType: 테이블 이름
                // from: 이전 컬럼명
                // to: 바꾸고 싶은 컬럼명
                migration.renameProperty(onType: Memo.className(), from: "isPinned", to: "isFixed")
            }
            
            // 3 -> 4로 업데이트, 컬럼 결합
            if oldSchemaVersion < 4 {
                migration.enumerateObjects(ofType: Memo.className()) { oldObject, newObject in
                    guard let new = newObject else { return } // 새롭게 생길 컬럼
                    guard let old = oldObject else { return } // 기존의 컬럼 
                    
                    new["memoInfo"] = "제목: \(old["memoTitle"]!), 내용: \(old["memoContent"]!), 날짜: \(old["memoDate"]!)"
                }
            }
            
            // ✅ 4 -> 5로 업데이트, 초기값이 있는 컬럼 추가
            if oldSchemaVersion < 5 {
                migration.enumerateObjects(ofType: Memo.className()) { oldObject, newObject in
                    guard let new = newObject else { return }
                    new["count"] = 127
                }
            }
        }
        
        Realm.Configuration.defaultConfiguration = config
    }
}

 

 

그리고 빌드를 해서 스키마 버전과 Realm 테이블 구조를 확인하면 아래와 같다.

 

 


추가로 .. 마이그레이션을 진행하면서 Realm Browser로 구조 변경을 확인할텐데 .. 

앞선 글에서 말한 deleteRealmIfMigrationNeeded 함수의 경우에는 이 창을 꼭 닫고(= 끄고) 실행해야 런타임 오류가 발생하지 않는다.

 

다른 경우에서는 런타임 오류까지는 아니더라도 바로 반영이 안될 수 있으니 끄고 > 실행하고 > 확인하는 것이 안정적인 방법이지만, 

만약 켜놓고 실행했다가 창이 하얗게 나온다면 .. 당황하지 않고 껐다가 키면 된다 .. !! 

 

 

언제 마이그레이션을 해야 하는가?

마이그레이션은 필요할 경우에만 하는 것이 좋다. (굳이 할 필요가 없다면 안하는 것이 좋다.)

마이그레이션을 위한 코드들은 결국 레거시한 코드가 될 확률이 높기 때문이다. 이전 버전으로 사용하고 있는 유저들이 있다면 마이그레이션 코드를 계속 대응해야 하므로 (스키마 버전에 따른 대응을 해야하므로) 결국 레거시한 코드가 되는 것이다.