아삭아삭 iOS 개발

[Swift] Realm Migration - 컬럼 추가/삭제/이름변경/결합 본문

Swift

[Swift] Realm Migration - 컬럼 추가/삭제/이름변경/결합

바닐라머스크 2022. 10. 14. 18:00

 

어제 수업시간에 배웠던 realm migration 내용을 정리해보고, 연습 프로젝트에 실습한 내용을 기록했습니다.

틀린 내용이 있을 수 있으며 댓글을 통한 피드백은 언제나 환영입니다~

 

Migration

  • Realm 데이터 설계 이후 데이터를 갱신할 때 필요한 과정
    • ex) 새로운 테이블이 추가되거나
    • ex) 기존 테이블의 컬럼을 삭제/추가/이름변경/타입변경 등이 필요할 때 사용
  • 데이터베이스에서 스키마 버전을 관하는데 사용됨
    • 별도로 지정하지 않는 한 realm schema 버전은 자동으로 0 설정
    • 업데이트시 상위 버전으로만 가능

deleteRealmIfMigrationNeeded

개발자가 앱을 개발하거나 디버깅시 realm을 수정할 때마다 해당 수정된 버전의 realm으로 테스트를 하려면

시뮬레이터 앱을 매번 삭제&재설치하고 Realm Browser도 닫고 다시 열어주어야 했습니다.

 

(출처 : mongodb.com)

 

하지만 deleteRealmIfMigrationNeeded 옵션을 사용하면 매번 재설치를 해야하는 귀찮은 작업을 안 해도 됩니다!

 

왜냐하면 기존 스키마랑 현재 스키마가 불일치해서 migration이 필요한 경우에

해당 옵션이 자동으로  기존의 schema를 제거해주기 때문입니다.

 

적용하는 방법은 appDelegate에서 마이그레이션 함수를 작성할 때 아래처럼 해당 옵션값을 true로 주면 끝입니다.

 

extension AppDelegate {
    
    func aboutRealmMigration() {
        let config = Realm.Configuration(schemaVersion: 1, deleteRealmIfMigrationNeeded: true) // 여기!

        Realm.Configuration.defaultConfiguration = config
    }
}

 

주의할 점! 릴리즈 버전에는 해당 옵셩을 사용하면 안되는 점 참고해주세요~


 

Linear Migrations

schema를 상세하게 업데이트 해주기 위해서는 AppDelegate내에서 migrationBlock를 사용해서 수동으로 업데이트를 해주어야 합니다.

이 때, migrationBlock이 중첩되거나 스킵되어지는 상황을 피하기 위해 아래처럼 모든 버전별로 if 구문을 통해 구분을 해줍니다.

 

extension AppDelegate {
    
    func realmMigrationSetting() {
        
        let config = Realm.Configuration(schemaVersion: 2) { migration, oldSchemaVersion in

            if oldSchemaVersion < 1 { } // 버전이 1보다 아래일 경우 구분

            if oldSchemaVersion < 2 { } // 버전이 2보다 아래일 경우 구분
 
        }
        Realm.Configuration.defaultConfiguration = config
    }
}

이렇게 버전별로 세세하게 나눠줌으로써 사용자의 현재 버전이 과거 몇이든 상관없이

모두 적절한 순서를 거쳐 최신 버전으로 업데이트가 될 수 있습니다.

 

버전을 구분해줄 때 else if가 아닌 if 로 구성한 이유 또한모든 케이스의 버전 block을 하나하나 다 체크해주고 지나가기 위함이라고 하니 참고해주세요~

 


< 연습 프로젝트에 실습한 내용 >

 

1) 컬럼 추가/삭제

기존에 작업하던 메모장 프로젝트에서 아래처럼 likeNum 변수를 추가했다가, 삭제해보았습니다.

단순히 항목을 추가하거나 삭제했을 경우에는 manually 작업해줄 코드는 없으며

개발자가 버전을 올려주기만 하면 똑똑한 xcode가 자동으로 migration을 진행해줍니다.

// Relam model
class UserMemo: Object {
    @Persisted var memoTitle: String
    @Persisted var memoContent: String?
    @Persisted var memoDate = Date()
    @Persisted var pin: Bool
    
    @Persisted var likeNum: Int // 좋아요 클릭수 항목을 추가했다가 삭제했음!

    @Persisted(primaryKey: true) var objectId: ObjectId
    
    convenience init(memoTitle: String, memoContent: String?, memoDate: Date) {
        self.init()
        self.memoTitle = memoTitle
        self.memoContent = memoContent
        self.memoDate = memoDate
        self.pin = false
    }
}elegate


// AppDelegate
extension AppDelegate {
    
    func realmMigrationSetting() {
        
        let config = Realm.Configuration(schemaVersion: 2) { migration, oldSchemaVersion in
            
            if oldSchemaVersion < 1 { } // ver1. 단순히 컬럼 추가만 했으므로, 별도 코드 필요 x

            if oldSchemaVersion < 2 { } // ver2. 단순히 컬럼 삭제만 했으므로, 별도 코드 필요 x
 
        }
        Realm.Configuration.defaultConfiguration = config
    }
}

 

 

2) 컬럼명 변경

migration을 통해 컬럼명을 변경할 수도 있습니다.

단순히 컬럼을 추가하거나 삭제하는게 아닌 컬럼명을 변경하는 작업을 해줄 때에는 migration.renameProperty(onType:from:to)메서드를 활용해서 manually작업을 해주어야 합니다.

 

한번에 여러 컬럼에 대한 변경작업을 괜히 해보고 싶어서

한 번 4개 컬럼명 전체를 다 변경해보았는데요, 정상적으로 변경은 되었지만..

다른 뷰컨이나 realm repository 함수내에서 사용됐던 컬럼명도 다 변경된 이름으로 적용해주어야 합니다.

(but 이렇게 한번에 여러 컬럼에 대해 데이터를 다 변경하는게 안전하지는 않은 방법이라고 합니다!)

class UserMemo: Object {
    @Persisted var title: String //제목(필수)
    @Persisted var content: String? // 내용(옵션)
    @Persisted var writtenDate = Date() // 작성 날짜(필수)
    @Persisted var fixed: Bool // 고정여부(필수)

    @Persisted(primaryKey: true) var objectId: ObjectId
    
    convenience init(memoTitle: String, memoContent: String?, memoDate: Date) {
        self.init()
        self.title = memoTitle
        self.content = memoContent
        self.writtenDate = memoDate
        self.fixed = false
    }
}


extension AppDelegate {
    
    func realmMigrationSetting() {
        
        let config = Realm.Configuration(schemaVersion: 3) { migration, oldSchemaVersion in
            
            if oldSchemaVersion < 1 { }

            if oldSchemaVersion < 2 { }
            
            // v3. 컬럼명 4개를 변경해주었으므로, 코드 작업을 해줌
            if oldSchemaVersion < 3 {
                migration.renameProperty(onType: UserMemo.className(), from: "memoTitle", to: "title")
                migration.renameProperty(onType: UserMemo.className(), from: "memoContent", to: "content")
                migration.renameProperty(onType: UserMemo.className(), from: "memoDate", to: "writtenDate")
                migration.renameProperty(onType: UserMemo.className(), from: "pin", to: "fixed")
            }
        }
        Realm.Configuration.defaultConfiguration = config
    }   
}

 

 

3) 두 컬럼을 결합해서 신규컬럼으로 추가

기존의 두 컬럼을 결합해서 신규 컬럼으로 추가할 경우에도 enumerateObjects을 사용합니다.

title과 content 컬럼을 결합해서 userDescription이라는 신규 컬럼으로 추가해보았습니다.

class UserMemo: Object {
    .
    .
    @Persisted var userDescription : String // title + content 결합한 컬럼
    .
    .
}


extension AppDelegate {
    
    func realmMigrationSetting() {
        
        let config = Realm.Configuration(schemaVersion: 4) { migration, oldSchemaVersion in
            .
            .
            
            // v4. 기존 컬럼 2개를 결합하여 신규컬럼 생성, 코드 작업을 해줌
            if oldSchemaVersion < 4 {
                migration.enumerateObjects(ofType: UserMemo.className()) { oldObject, newObject in
                    guard let new = newObject else { return }
                    guard let old = oldObject else { return }
                    
                    new["userDescription"] = "제목은 \(old["title"])이고, 내용은 \(old["content"])입니다!!!"
                }
            }
        }
        Realm.Configuration.defaultConfiguration = config
    }
    
}

 

 

4) 기본값이 있는 컬럼 추가

신규 컬럼으로 추가시 기본값이 존재할 경우에도 enumerateObjects 메서드를 활용합니다.

user라는 기본값을 갖는 author 컬럼을 추가해보았습니다.

 

class UserMemo: Object {
    .
    .
    @Persisted var author: String // 메모 저자 - 기본값 존재
    .
    .
}

extension AppDelegate {
    
    func realmMigrationSetting() {
        
        let config = Realm.Configuration(schemaVersion: 5) { migration, oldSchemaVersion in
            .
            .
            // v5. 신규 컬럼 1개를 추가하는데 기본값이 존재함, 코드 작업을 해줌
            if oldSchemaVersion < 5 {
                migration.enumerateObjects(ofType: UserMemo.className()) { oldObject, newObject in
                    
                    guard let new = newObject else { return }
                    new["author"] = "user"
                }
            }
        }
        Realm.Configuration.defaultConfiguration = config
    }
}

 

 

5) (시도) 기존컬럼의 컬럼명, 타입, 기본값 동시에 변경

위에 4번까지 차근차근 migration 연습을 해보다가, 한 컬럼에 대해 이름, 타입, 기본값이 한번에 바뀔 수 있을지가 갑자기 궁금해졌습니다.

그래서 renameProperty로 컬럼명을 변경해주고, enumerateObjects로 타입과 기본값을 새로 할당해보았는데..

.. 에러가 났습니다!ㅜㅜ

 

왜 그럴까 생각해보니 타입변경 관련해서는 (수업실습 때와 달리) realm model내에서 타입을 변경해주는 문구도 누락하기도 했고,단계적으로 바꾸는게 아니라 한번에 변경작업을 진행하려고 해서로 보였습니다.

 

 

나중에 이 여섯번째 시도 케이스에 대해 멘토님께 질문해보았고 아래와 같은 피드백을 받았습니다.

- 마이그레이션 작업 시에는 단계적으로 해주는게 안전
- 하나의 컬럼에 대해 다양한 변경을 하기에는 (가능하다고 할지라도) 안전하지 않은 코드로 참고하는 것이 좋음
- 실제로 보통 마이그레이션을 작업할 때 한 컬럼을 한번에 다양하게 바꾸는 케이스가 많이 없음
- 마이그레이션은 일반적으로 데이터를 보다 더 잘 정규화하기 위해 테이블을 쪼개거나, 신규 컬럼이 필요한 경우에 많이 사용됨

 

정말 그러고보니, 한 컬럼에 대해 이름도 바꾸고, 타입도 바꾸고, 기본값도 바꿀꺼면

아예 삭제하고 다른 컬럼이 들어오는게 차라리 나을 법할 수도 있겠다 싶었습니다.. 흠.. 

 

무튼 여섯번째 마이그레이션은 시도는 좋았으나, 실제 현업에서는 잘 쓰지 않는 케이스라는 것

그리고 마이그레이션을 주요하게 쓰는 경우에 대해 더 생각해볼 수 있었습니다!

 

 

 

https://www.mongodb.com/docs/realm/sdk/swift/model-data/change-an-object-model/

 

Change an Object Model - Swift SDK — Realm

Realm Database does not automatically set values for new required properties. You must use a migration block to set default values for new required properties. For new optional properties, existing records can have null values. This means you don't need a

www.mongodb.com