Room을 Kotlin Multiplaform으로 이전

이 문서에서는 기존 Room 구현을 Kotlin 멀티플랫폼 (KMP)을 사용하는 구현으로 이전하는 방법을 설명합니다.

기존 Android 코드베이스의 Room 사용을 공통 공유 KMP 모듈로 이전하는 작업은 사용되는 Room API에 따라 또는 코드베이스가 이미 코루틴을 사용하는지 여부에 따라 난이도가 크게 다를 수 있습니다. 이 섹션에서는 Room 사용을 공통 모듈로 이전하려고 할 때 필요한 안내와 도움말을 제공합니다.

먼저 Room의 Android 버전과 KMP 버전의 차이점 및 누락된 기능과 함께 관련 설정을 숙지하는 것이 중요합니다. 기본적으로 성공적인 이전에는 SupportSQLite* API 사용을 리팩터링하고 이를 SQLite Driver API로 대체하고, Room 선언 (@Database 주석이 달린 클래스, DAO, 항목 등)을 공통 코드로 이동하는 것이 포함됩니다.

계속하기 전에 다음 정보를 다시 확인하세요.

다음 섹션에서는 성공적인 이전에 필요한 다양한 단계를 설명합니다.

지원 SQLite에서 SQLite 드라이버로 이전

androidx.sqlite.db의 API는 Android 전용이며 모든 사용법은 SQLite Driver API로 리팩터링해야 합니다. 이전 버전과의 호환성을 위해 RoomDatabaseSupportSQLiteOpenHelper.Factory로 구성된 한 (즉, SQLiteDriver가 설정되지 않은 경우) Room은 '호환성 모드'로 작동하며 여기서 Support SQLite 및 SQLite Driver API가 모두 예상대로 작동합니다. 이렇게 하면 증분 이전이 가능하므로 한 번의 변경으로 모든 지원 SQLite 사용량을 SQLite Driver로 변환할 필요가 없습니다.

다음 예는 지원 SQLite 및 SQLite 드라이버의 일반적인 사용법입니다.

SQLite 지원 (원본)

결과가 없는 쿼리 실행

val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")

결과는 있지만 인수는 없는 쿼리 실행

val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
  while (cusor.moveToNext()) {
    // read columns
    cursor.getInt(0)
    cursor.getString(1)
  }
}

결과 및 인수로 쿼리 실행

database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
  if (cursor.moveToNext()) {
    // row found, read columns
  } else {
    // row not found
  }
}

SQLite 드라이버 (대상)

결과가 없는 쿼리 실행

val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")

결과는 있지만 인수는 없는 쿼리 실행

val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
  while (statement.step()) {
    // read columns
    statement.getInt(0)
    statement.getText(1)
  }
}

결과 및 인수로 쿼리 실행

connection.prepare("SELECT * FROM Pet WHERE id = ?").use { statement ->
  statement.bindInt(1, id)
  if (statement.step()) {
    // row found, read columns
  } else {
    // row not found
  }
}

데이터베이스 트랜잭션 API는 SupportSQLiteDatabase에서 beginTransaction(), setTransactionSuccessful(), endTransaction()를 통해 직접 사용할 수 있습니다. runInTransaction()를 사용하여 Room을 통해서도 사용할 수 있습니다. 이러한 사용법을 SQLite Driver API로 이전하세요.

SQLite 지원 (원본)

트랜잭션 실행 (RoomDatabase 사용)

val database: RoomDatabase = ...
database.runInTransaction {
  // perform database operations in transaction
}

트랜잭션 실행 (SupportSQLiteDatabase 사용)

val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
  // perform database operations in transaction
  database.setTransactionSuccessful()
} finally {
  database.endTransaction()
}

SQLite 드라이버 (대상)

트랜잭션 실행 (RoomDatabase 사용)

val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
  transactor.immediateTransaction {
    // perform database operations in transaction
  }
}

트랜잭션 실행 (SQLiteConnection 사용)

val connection: SQLiteConnection = ...
connection.execSQL("BEGIN IMMEDIATE TRANSACTION")
try {
  // perform database operations in transaction
  connection.execSQL("END TRANSACTION")
} catch(t: Throwable) {
  connection.execSQL("ROLLBACK TRANSACTION")
}

다양한 콜백 재정의도 해당하는 드라이버로 이전해야 합니다.

SQLite 지원 (원본)

마이그레이션 서브클래스

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

자동 이전 사양 서브클래스

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

데이터베이스 콜백 서브클래스

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onOpen(db: SupportSQLiteDatabase) {
    // ...
  }
}

SQLite 드라이버 (대상)

마이그레이션 서브클래스

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(connection: SQLiteConnection) {
    // ...
  }
}

자동 이전 사양 서브클래스

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(connection: SQLiteConnection) {
    // ...
  }
}

데이터베이스 콜백 서브클래스

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(connection: SQLiteConnection) {
    // ...
  }

  override fun onDestructiveMigration(connection: SQLiteConnection) {
    // ...
  }

  override fun onOpen(connection: SQLiteConnection) {
    // ...
  }
}

요약하면 콜백 재정의 (onMigrate, onCreate 등)에서와 같이 RoomDatabase를 사용할 수 없는 경우 SQLiteDatabase 사용을 SQLiteConnection로 바꿉니다. RoomDatabase를 사용할 수 있으면 RoomDatabase.openHelper.writtableDatabase 대신 RoomDatabase.useReaderConnectionRoomDatabase.useWriterConnection를 사용하여 기본 데이터베이스 연결에 액세스합니다.

차단 DAO 함수를 정지 함수로 변환

Room의 KMP 버전은 코루틴을 사용하여 구성된 CoroutineContext에서 I/O 작업을 실행합니다. 즉, 차단 DAO 함수를 이전하여 함수를 정지해야 합니다.

DAO 함수 차단 (소스)

@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>

DAO 함수 정지 (to)

@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>

기존 코드베이스에 아직 코루틴이 통합되지 않은 경우 기존 DAO 차단 함수를 정지 함수로 이전하는 것이 복잡할 수 있습니다. 코드베이스에서 코루틴 사용을 시작하려면 Android의 코루틴을 참고하세요.

반응형 반환 유형을 Flow로 변환

모든 DAO 함수가 정지 함수일 필요는 없습니다. LiveData 또는 RxJava의 Flowable와 같은 반응형 유형을 반환하는 DAO 함수는 정지 함수로 변환하면 안 됩니다. 그러나 LiveData와 같은 일부 유형은 KMP와 호환되지 않습니다. 반응형 반환 유형이 있는 DAO 함수는 코루틴 흐름으로 이전해야 합니다.

호환되지 않는 KMP 유형 (소스:)

@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>

호환되는 KMP 유형 (to)

@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>

코드베이스에서 Flow를 사용하려면 Android의 흐름을 참고하세요.

코루틴 컨텍스트 설정 (선택사항)

RoomDatabaseRoomDatabase.Builder.setQueryExecutor()를 사용하여 데이터베이스 작업을 실행하는 공유 애플리케이션 실행자로 선택적으로 구성할 수 있습니다. 실행자는 KMP와 호환되지 않으므로 일반 소스에 Room의 setQueryExecutor() API를 사용할 수 없습니다. 대신 RoomDatabaseCoroutineContext로 구성해야 합니다. 컨텍스트는 RoomDatabase.Builder.setCoroutineContext()를 사용하여 설정할 수 있으며, 아무것도 설정하지 않으면 RoomDatabase는 기본적으로 Dispatchers.IO를 사용합니다.

SQLite 드라이버 설정

Support SQLite 사용이 SQLite Driver API로 이전되면 RoomDatabase.Builder.setDriver를 사용하여 드라이버를 구성해야 합니다. 권장 드라이버는 BundledSQLiteDriver입니다. 사용 가능한 드라이버 구현에 관한 설명은 드라이버 구현을 참고하세요.

RoomDatabase.Builder.openHelperFactory()를 사용하여 구성된 맞춤 SupportSQLiteOpenHelper.Factory는 KMP에서 지원되지 않으므로 맞춤 열기 도우미에서 제공하는 기능은 SQLite 드라이버 인터페이스로 다시 구현해야 합니다.

Room 선언 이동

대부분의 이전 단계가 완료되면 Room 정의를 공통 소스 세트로 이동할 수 있습니다. expect / actual 전략을 사용하여 Room 관련 정의를 점진적으로 이동할 수 있습니다. 예를 들어 차단 함수 중 일부를 정지 함수로 이전할 수 없는 경우 일반 코드에서는 비어 있지만 Android에서는 차단 함수를 포함하는 expect @Dao 주석이 달린 인터페이스를 선언할 수 있습니다.

// shared/src/commonMain/kotlin/Database.kt

@Database(entities = [TodoEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
  abstract fun getDao(): TodoDao
  abstract fun getBlockingDao(): BlockingTodoDao
}

@Dao
interface TodoDao {
    @Query("SELECT count(*) FROM TodoEntity")
    suspend fun count(): Int
}

@Dao
expect interface BlockingTodoDao
// shared/src/androidMain/kotlin/BlockingTodoDao.kt

@Dao
actual interface BlockingTodoDao {
    @Query("SELECT count(*) FROM TodoEntity")
    fun count(): Int
}