[Room] Migrations

이전 포스팅

https://jinudmjournal.tistory.com/157

 

[Room 오류 해결] java.lang.IllegalStateException: Room cannot verify the data integrity.

문제 상황 룸 데이터베이스를 활용하는 중에 아래와 같은 오류가 발생하였습니다. java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number.

jinudmjournal.tistory.com

이전에 Room Migration 관련해서 오류를 해결한 내용을 포스팅하였습니다.

매번 기능 구현에 급급해서 제대로 학습이 안된채로 사용하였는데, 포스팅하면서 개념을 다시 잡았습니다.

 

Migrations

https://developer.android.com/training/data-storage/room/migrating-db-versions?hl=ko

 

Room 데이터베이스 이전  |  Android Developers

Room 라이브러리를 사용하여 데이터베이스를 안전하게 이전하는 방법 알아보기

developer.android.com

 

앱에서 기능을 추가하고 변경하는 경우 Room 항목 클래스와 기본 데이터베이스 테이블을 수정하여 이러한 변경사항을 반영해야 합니다.

Domain이나 Entity를 수정할 때 Migrations을 통해서 변경 사항을 명시하고 벼전을 업데이트 해주어야 합니다.

Room의 버전이 2.4.0-alpha01 이상에서는 자동 이전을 지원하지만,

함께 개발하는 동료에게 적절한 migration을 명시해주는 것도 좋은 컨벤션이 될 수 있습니다.

만약에 자동 이전을 선언하려면 @AutoMigration 주석을 사용할 수 있습니다.

// Database class before the version update.
@Database(
  version = 1,
  entities = [User::class]
)
abstract class AppDatabase : RoomDatabase() {
  ...
}

// Database class after the version update.
@Database(
  version = 2,
  entities = [User::class],
  autoMigrations = [
    AutoMigration (from = 1, to = 2)
  ]
)
abstract class AppDatabase : RoomDatabase() {
  ...
}

 

migration 수동 이전

복잡한 스키마 변경이 포함되거나, 마이그레이션을 필수로 명시하는 경우 Migration.migrate() 메서드를 재정의하여

startVersion과 endVersion 간의 이전 경로를 명시적으로 정의해야 합니다.

이 후 addMigrations() 메서드를 사용하여 Migration 클래스를 데이터베이스 빌더에 추가할 수 있습니다.

@Database(
    entities = [
        CartItemEntity::class,
    ],
    version = 3,
)
@TypeConverters(CartItemConverters::class)
abstract class CartItemDatabase : RoomDatabase() {
    abstract fun cartItemDao(): CartItemDao

    companion object {
        private var instance: CartItemDatabase? = null
        const val CART_ITEMS_DB_NAME = "cartItems"

        private val MIGRATION_2_3 =
            object : Migration(2, 3) {
                override fun migrate(db: SupportSQLiteDatabase) {
                    db.execSQL("ALTER TABLE $CART_ITEMS_DB_NAME ADD COLUMN productId INTEGER NOT NULL DEFAULT 0")
                }
            }

        @Synchronized
        fun getInstance(context: Context): CartItemDatabase {
            return instance
                ?: synchronized(CartItemDatabase::class) {
                    Room.databaseBuilder(
                        context.applicationContext,
                        CartItemDatabase::class.java,
                        CART_ITEMS_DB_NAME,
                    ).addMigrations(MIGRATION_2_3).build()
                }
        }
    }
}

 

대표적으로 사용되는 migration의 예시는 아래와 같습니다.

1. 테이블에 새로운 컬럼 추가

기존 테이블에 새로운 컬럼을 추가하는 마이그레이션입니다. 예를 들어, productId 컬럼을 CART_ITEMS_DB_NAME 테이블에 추가합니다.

private val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE $CART_ITEMS_DB_NAME ADD COLUMN productId INTEGER NOT NULL DEFAULT 0")
    }
}

2. 테이블 이름 변경

기존 테이블의 이름을 변경하는 마이그레이션입니다.

private val MIGRATION_3_4 = object : Migration(3, 4) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE old_table_name RENAME TO new_table_name")
    }
}​

3. 테이블 삭제

기존 테이블을 삭제하는 마이그레이션입니다.

private val MIGRATION_4_5 = object : Migration(4, 5) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("DROP TABLE IF EXISTS table_to_be_deleted")
    }
}

4. 테이블 재구성 (리팩토링)

기존 테이블을 새 구조로 재구성하는 마이그레이션입니다. 새로운 테이블을 만들고 데이터를 옮긴 다음, 기존 테이블을 삭제합니다.

private val MIGRATION_5_6 = object : Migration(5, 6) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("CREATE TABLE new_table AS SELECT * FROM old_table")
        db.execSQL("DROP TABLE old_table")
        db.execSQL("ALTER TABLE new_table RENAME TO old_table")
    }
}

 

마이그레이션 테스트 

Room은 Room.inMemoryDatabaseBuilder()를 사용하여 인메모리 데이터베이스를 만들고, 마이그레이션을 테스트할 수 있습니다.

@RunWith(AndroidJUnit4::class)
class MigrationTest {

    @Rule
    @JvmField
    val helper: MigrationTestHelper = MigrationTestHelper(
        InstrumentationRegistry.getInstrumentation(),
        AppDatabase::class.java.canonicalName,
        FrameworkSQLiteOpenHelperFactory()
    )

    @Test
    fun migrate1To2() {
        // 기존 버전의 데이터베이스 생성
        helper.createDatabase(TEST_DB, 1).apply {
            execSQL("INSERT INTO user (id, name) VALUES (1, 'John')")
            close()
        }

        // 마이그레이션 실행 및 검증
        helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)
    }
}

 

정리

Room은 Android SQLite 데이터베이스에 대한 추상화 계층을 제공하는 라이브러리 입니다.

안전하고 간편하게 데이터베이스 작업을 수행할 수 있게 도와주며, Room을 사용하면서 데이터베이스 구조가 변경될 때,

마이그레이션을 처리하는 방법을 잘 이애하는 것이 중요합니다.

마이그레이션은 데이터베이스의 스키마가 변경될 때 데이터 손실 없이 기존 데이터를 새로운 스키마에 맞게 변경하는 과정입니다.

마이그레이션을 통해서 앱의 지속적인 유지보수와 확장이 가능해지며, 마이그레이션을 잘 설계하고 테스트하는 것은 데이터 일관성과 무결성을 유지하는 데 중요합니다.