이전 포스팅
https://jinudmjournal.tistory.com/157
이전에 Room Migration 관련해서 오류를 해결한 내용을 포스팅하였습니다.
매번 기능 구현에 급급해서 제대로 학습이 안된채로 사용하였는데, 포스팅하면서 개념을 다시 잡았습니다.
Migrations
https://developer.android.com/training/data-storage/room/migrating-db-versions?hl=ko
앱에서 기능을 추가하고 변경하는 경우 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을 사용하면서 데이터베이스 구조가 변경될 때,
마이그레이션을 처리하는 방법을 잘 이애하는 것이 중요합니다.
마이그레이션은 데이터베이스의 스키마가 변경될 때 데이터 손실 없이 기존 데이터를 새로운 스키마에 맞게 변경하는 과정입니다.
마이그레이션을 통해서 앱의 지속적인 유지보수와 확장이 가능해지며, 마이그레이션을 잘 설계하고 테스트하는 것은 데이터 일관성과 무결성을 유지하는 데 중요합니다.