티스토리 뷰

TIL

[TIL] 2022 / 10 / 13 - Realm Migration

희철 2022. 10. 13. 18:51

Realm을 사용하다보면 컬럼을 추가하거나 삭제하는 등의 과정이 필요한 경우가 있음.

 

모델 클래스 안에서 프로퍼티를 추가, 삭제하거나 이름을 변경하고 빌드를 하면 오류가 남

 

변화로 인해 스키마의 버전이 바뀌었기때문임

 

그래서 테이블에 변화가 생기는 경우엔 마이그레이션이 필요함.

 

Migration

 

현재의 운영 환경으로 부터 다른 운영 환경으로 옮기는 작업을 일반적으로 마이그레이션이라고 하며, 데이터베이스에서만 사용되는 개념이 아님.

 

데이터베이스에서는 스키마 버전을 관리하기 위해 마이그레이션을 사용.

 

마이그레이션을 통해 데이터 구조를 맘대로 바꿀 수 있음.

 

두 컬럼의 값을 합쳐서 새로운 컬럼 안에 값으로 넣어줄 수도 있고, 컬럼 추가, 컬럼 삭제, 타입 변경 등 전부 가능함.

 

근데 마이그레이션은 꼭 필요한 경우에만 하는 것이 좋음.

 

모든 스키마 버전에 대해서 대응을 해줘야하고, 데이터 구조가 복잡하면 하기 힘들어질거임.

 

스키마의 버전은 반드시 차례대로 업데이트 되어야하기때문에, 마이그레이션을 하는 만큼 코드블럭이 늘어날 것임.

 

또한 차례대로 업데이트 해야하므로 if else 구문이 아닌 if 구문 여러개를 이용해야함.

 

예를 들어, 스키마 버전이 현재 1인 사람이 있고 최신 버전이 4라고 가정하면, 그 사람은 최신 버전으로 바로 갈 수 있는게 아니라 1 2 3 4처럼 차례대로 업데이트 되어야함. 

 

 

deleteRealmIfMigrationNeeded

 

마이그레이션을 진행할 때 Realm.Configuration()을 사용할텐데, 우선 Configuration의 매개변수 중 deleteRealmIfMigrationNeeded부터 확인해보겠음.

 

앞서 말했듯이 스키마의 버전이 다르다면 빌드했을때 런타임에러가 발생할 것임.

 

이전에는 이를 해결하기 위해서 매번 디바이스에서 앱을 지웠다가 다시 빌드시켰음.

 

근데 deleteRealmIfMigrationNeeded를 사용한다면 빌드할때 마이그레이션이 필요한 경우에 자동으로 기존 스키마를 제거해서 오류가 나지 않게 해줌.

 

이는 개발할 때 필요한 것이므로, 릴리즈 버전에서는 반드시 지워야함.

 

그렇지 않으면 사용자가 앱을 실행할 때마다 데이터가 전부 사라지는 일이 발생할거임..

 

아래의 코드를 didFinishLaunchingWithOptions에서 실행시켜준다면, 개발할때는 스키마 버전이 달라서 생기는 오류를 만나지 않을 수 있음.

 

단, Realm studio를 닫고 빌드해야 적용됨.

let config = Realm.Configuration(schemaVersion: 4, deleteRealmIfMigrationNeeded: true)
Realm.Configuration.defaultConfiguration = config

 

스키마 버전은 현재 스키마의 버전을 적어주면됨.

 

스키마 버전은 아래의 코드를 이용해 알 수 있음.

//2. 스키마버전 확인하는 코드
do {
    let version = try schemaVersionAtURL(localRealm.configuration.fileURL!)
    print("Schema Version: \(version)")        
} catch {
    print("오류")
}

 

schemaVersion에 직접 버전을 입력해도됨. 

 

단, 이때 반드시 이전버전보다는 큰 수여야하며 중간에 건너뛴 숫자들로 다시 돌아갈 수 없음.

 

따로 지정해주지 않으면 스키마의 버전은 이전 버전에서 1씩 증가함

 

 

 

Migration 해보기

 

이전에 평가과제로 했던 메모앱에서 진행해보겠음.

 

마이그레이션 실습이 목적이니 컬럼이 앱과 맞지 않을 수도 있음.(아직 안해봄 혹시몰라서)

 

현재 스키마 버전은 0이며, 테이블은 아래와 같음.

 

컬럼 추가, 삭제

 

컬럼을 추가하거나 삭제하는 것은 별도로 마이그레이션을 하지 않아도 스키마 버전만 올려주면 자동으로 진행이됨.

 

text라는 컬럼을 추가해보겠음.

이 상태로 빌드하면 런타임에러가 남.

 

스키마 버전만 올려보겠음.

(아래의 코드는 AppDelegate에서 실행)

func aboutRealmMigration() {

    let config = Realm.Configuration(schemaVersion: 1) { migration, oldSchemaVersion in
//        if oldSchemaVersion < 1 {
//
//        }
    }
    Realm.Configuration.defaultConfiguration = config
}

 

위와 같이 스키마 버전만 바꿔주어도 아래처럼 컬럼이 정상적으로 추가된 것을 볼 수 있음.

 

따로 초기값을 넣는 등의 코드를 작성하는 경우엔 주석 처리한 블럭 내에 작성해주어야하지만, 컬럼 추가나 삭제는 따로 작성하지않고 스키마의 버전만 올려주면됨.

 

이전 버전이 0이었기때문에 1로 올려서 실행한 것.

 

근데 어떤 컬럼이 추가되는 등의 바뀐 부분을 주석으로 적기도 하기때문에 그냥 형식상 넣어둘 것 같음.

 

이번에는 추가했던 text컬럼을 삭제해보겠음.

 

    func aboutRealmMigration() {

        let config = Realm.Configuration(schemaVersion: 2) { migration, oldSchemaVersion in
            if oldSchemaVersion < 1 {
                // 컬럼 추가
            }
            if oldSchemaVersion < 2 {
                // 컬럼 삭제
            }
        }
        Realm.Configuration.defaultConfiguration = config

    }

스키마 버전 0과 동일한 형태이지만 서로 다른 버전이므로 위와 같이 차례대로 처리를 해줘야함.

 

다시 말하지만 Linear Migration이 진행되어야하므로 if else 구문을 사용해 중간을 건너뛰지않도록 해야함.

 

 

컬럼 이름 변경

 

이번엔 컬럼 이름을 memoTitle에서 title로 변경해보겠음.

 

컬럼의 이름이 변경된다면 추가, 삭제처럼 버전만 바꿔서는 안됨.

 

이름이 변경되었다는 것을 알려줘야함.

 

    func aboutRealmMigration() {

        let config = Realm.Configuration(schemaVersion: 3) { migration, oldSchemaVersion in
            if oldSchemaVersion < 1 {
                // 컬럼 추가
            }
            if oldSchemaVersion < 2 {
                // 컬럼 삭제
            }
            if oldSchemaVersion < 3 {
                //컬럼 이름 변경 memoTitle -> title
                migration.renameProperty(onType: UserMemo.className(), from: "memoTitle", to: "title")
            }
        }
        Realm.Configuration.defaultConfiguration = config

    }

(스키마 버전 올려주는거 까먹지말길...시간날림)

아무튼 migration의 renameProperty를 이용하여 바꿔주면 됨.

 

만약 테이블이 여러개라면 onType에 UserMemo대신 다른 클래스를 적어서 같이 작성하면될듯

 

 

새로운 컬럼을 만들어 데이터 넣기

 

위에서는 새로운 컬럼만 추가했기때문에 따로 코드를 작성하지 않아도 됐음.

 

근데 데이터를 넣어준다면 얘기가 달라짐.

 

migration의 enumerateObjects라는 메서드를 이용해서 summary라는 컬럼을 추가하고 title과 memoContent의 값을 합쳐서 값을 넣어주겠음.

 

일단 데이터가 있어야하므로 먼저 10개정도 데이터를 넣어주겠음.

 

이제 summary라는 컬럼을 추가하고 title과 memoContent의 값을 이용해서 데이터를 넣어보겠음.

    func aboutRealmMigration() {

        let config = Realm.Configuration(schemaVersion: 4) { migration, oldSchemaVersion in
            if oldSchemaVersion < 1 {
                // 컬럼 추가
            }
            if oldSchemaVersion < 2 {
                // 컬럼 삭제
            }
            if oldSchemaVersion < 3 {
                //컬럼 이름 변경 memoTitle -> title
                migration.renameProperty(onType: UserMemo.className(), from: "memoTitle", to: "title")
            }
            if oldSchemaVersion < 4 {
                //summary 컬럼 추가 및 데이터 추가
                migration.enumerateObjects(ofType: UserMemo.className()) { oldObject, newObject in
                    guard let new = newObject, let old = oldObject else { return }
                    new["summary"] = "\(old["title"]) \(old["memoContent"])"
                }
            }
        }
        Realm.Configuration.defaultConfiguration = config
    }

10개에 대하여 데이터가 잘 들어간 것을 확인할 수 있음.

 

summary는 옵셔널로 선언했는데 원래 데이터는 버전 3에서 열개만 있었으므로 새로 생긴 10개에 대해서는 nil이 들어갔음.

-> 노린 게 아니라 데이터 추가하는 코드를 주석 처리 해주는 것을 까먹음. 근데 오히려 좋음. 보여줄 경우가 더 생김

 

oldObject는 이전 버전을, newObject는 현재 버전을 가리키는 것.

 

꼭 oldObject의 값을 사용해야하는건 아님.

 

new["summary"] = "초기값" 이런식으로 동일한 초기값을 줄 수도 있음.

 

 

타입 변경

 

위에서 summary를 옵셔널로 선언한 이유는 타입 변경을 확인하기 위해서임

 

타입 변경을 할 때에도 enumerateObjects 메서드를 사용하면 됨

    func aboutRealmMigration() {

        let config = Realm.Configuration(schemaVersion: 5) { migration, oldSchemaVersion in
            if oldSchemaVersion < 1 {
                // 컬럼 추가
            }
            if oldSchemaVersion < 2 {
                // 컬럼 삭제
            }
            if oldSchemaVersion < 3 {
                //컬럼 이름 변경 memoTitle -> title
                migration.renameProperty(onType: UserMemo.className(), from: "memoTitle", to: "title")
            }
            if oldSchemaVersion < 4 {
                //summary 컬럼 추가 및 데이터 추가
                migration.enumerateObjects(ofType: UserMemo.className()) { oldObject, newObject in
                    guard let new = newObject, let old = oldObject else { return }
                    new["summary"] = "\(old["title"]) \(old["memoContent"])"
                }
            }
            if oldSchemaVersion < 5 {
                //옵셔널 타입 변경
                migration.enumerateObjects(ofType: UserMemo.className()) { oldObject, newObject in
                    guard let new = newObject, let old = oldObject else { return }
                    new["summary"] = old["summary"] ?? "nil이었던애들"
                }
            }
        }
        Realm.Configuration.defaultConfiguration = config
    }

옵셔널 타입인 경우엔 nil을 받을 수 있었지만 그냥 String의 경우는 nil을 가질 수 없기 때문에 nil인 애들은 따로 값을 설정해줘야했음.

 

summary의 타입과 값이 제대로 적용된 것을 확인할 수 있음.

 

지금 summary 데이터에 보이는 Optional부분은 아래에 보이는 것과 같이 넣어주었던 데이터인 old["title"]이 옵셔널 값이기 때문에 저렇게 보이는 것임.

 

옵셔널뿐만 아니라 아예 타입을 변경할 수도 있음.

summary의 타입을 Int로 바꾸고, 기존의 값들을 전부 Int값으로 바꿔보겠음.

    func aboutRealmMigration() {

        let config = Realm.Configuration(schemaVersion: 6) { migration, oldSchemaVersion in
            if oldSchemaVersion < 1 {
                // 컬럼 추가
            }
            if oldSchemaVersion < 2 {
                // 컬럼 삭제
            }
            if oldSchemaVersion < 3 {
                //컬럼 이름 변경 memoTitle -> title
                migration.renameProperty(onType: UserMemo.className(), from: "memoTitle", to: "title")
            }
            if oldSchemaVersion < 4 {
                //summary 컬럼 추가 및 데이터 추가
                migration.enumerateObjects(ofType: UserMemo.className()) { oldObject, newObject in
                    guard let new = newObject, let old = oldObject else { return }
                    new["summary"] = "\(old["title"]) \(old["memoContent"])"
                }
            }
            if oldSchemaVersion < 5 {
                //옵셔널 타입 변경
                migration.enumerateObjects(ofType: UserMemo.className()) { oldObject, newObject in
                    guard let new = newObject, let old = oldObject else { return }
                    new["summary"] = old["summary"] ?? "nil이었던애들"
                }
            }
            if oldSchemaVersion < 6  {
                //타입 변경
                migration.enumerateObjects(ofType: UserMemo.className()) { _ , newObject in
                    guard let new = newObject else { return }
                    new["summary"] = 123
                }
            }
        }
        Realm.Configuration.defaultConfiguration = config
    }

타입 변경도 문제없이 잘 되는 것을 확인할 수 있음.

 

타입 변경하는 경우엔 nil같은 부분만 잘 신경써서 하면될듯

 

 

 

 

위의 코드를 보면 알 수 있듯이 모든 버전에 대응해야하기때문에 보기 싫은 코드가 계속해서 늘어나며 지울 수도 없음...

 

그래서 최대한 마이그레이션은 안하는 것이 좋은 것 같음.

 

만약 하게 된다면 스키마 버전 올려주는 것과 Linear Migration만 잘 생각해주면 문제는 없을 것임.

 

'TIL' 카테고리의 다른 글

[TIL] 2022 / 10 / 25 - Rx  (0) 2022.10.25
[TIL] 2022 / 10 / 24 - Rx  (0) 2022.10.24
[TIL] 2022/ 10 / 12 - Remote Notification, Method Swizzling  (0) 2022.10.12
[TIL] 2022 / 09 / 06  (0) 2022.09.06
[TIL] 2022 / 09 / 05  (0) 2022.09.05
댓글
최근에 올라온 글
Total
Today
Yesterday