KMP용 Room 데이터베이스 설정

Room 지속성 라이브러리는 SQLite에 추상화 레이어를 제공하여 SQLite를 완벽히 활용하면서 더 견고한 데이터베이스 액세스를 가능하게 합니다. 이 페이지에서는 Kotlin 멀티플랫폼 (KMP) 프로젝트에서 Room을 사용하는 데 중점을 둡니다. Room 사용에 관한 자세한 내용은 Room을 사용하여 로컬 데이터베이스에 데이터 저장 또는 공식 샘플을 참고하세요.

종속 항목 설정

KMP 프로젝트에서 Room을 설정하려면 KMP 모듈의 build.gradle.kts 파일에 아티팩트의 종속 항목을 추가하세요.

libs.versions.toml 파일에서 종속 항목을 정의합니다.

[versions]
room3 = "3.0.1"
sqlite = "2.7.0"
ksp = "<kotlinCompatibleKspVersion>"

[libraries]
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }

# Optional SQLite Wrapper
androidx-room3-sqlite-wrapper = { module = "androidx.room3:room3-sqlite-wrapper", version.ref = "room3" }

[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room3 = { id = "androidx.room3", version.ref = "room3" }

Room Gradle 플러그인을 추가하여 Room 스키마와 KSP 플러그인을 구성합니다.

plugins {
  alias(libs.plugins.ksp)
  alias(libs.plugins.androidx.room3)
}

Room 런타임 종속 항목과 번들 SQLite 라이브러리를 추가합니다.

commonMain.dependencies {
  implementation(libs.androidx.room3.runtime)
  implementation(libs.androidx.sqlite.bundled)
}

// Optional when using Room SQLite Wrapper
androidMain.dependencies {
  implementation(libs.androidx.room3.sqlite.wrapper)
}

KSP 종속 항목을 루트 dependencies 블록에 추가합니다. 앱에서 사용하는 모든 타겟을 추가해야 합니다. 자세한 내용은 Kotlin 멀티플랫폼을 사용하는 KSP를 참고하세요.

dependencies {
    add("kspAndroid", libs.androidx.room3.compiler)
    add("kspIosSimulatorArm64", libs.androidx.room3.compiler)
    add("kspIosX64", libs.androidx.room3.compiler)
    add("kspIosArm64", libs.androidx.room3.compiler)
    // Add any other platform target you use in your project, for example kspDesktop
}

Room 스키마 디렉터리를 정의합니다. 자세한 내용은 스키마 위치를 Room Gradle 플러그인을 사용하여 설정을 참고하세요.

room {
    schemaDirectory("$projectDir/schemas")
}
을 참고하세요.

데이터베이스 클래스 정의

공유 KMP 모듈의 공통 소스 세트 내에서 DAO 및 항목과 함께 @Database로 주석 처리된 데이터베이스 클래스를 만들어야 합니다. 이러한 클래스를 공통 소스에 배치하면 모든 타겟 플랫폼에서 공유할 수 있습니다.

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

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

// The Room compiler generates the `actual` implementations.
@Suppress("KotlinNoActualForExpect")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}

RoomDatabaseConstructor 인터페이스로 expect 객체를 선언하면 Room 컴파일러가 actual 구현을 생성합니다. Android 스튜디오에서 다음 경고를 발생할 수 있으며, @Suppress("KotlinNoActualForExpect")로 이를 표시하지 않을 수 있습니다.

Expected object 'AppDatabaseConstructor' has no actual declaration in module`

다음으로 새 DAO 인터페이스를 정의하거나 기존 인터페이스를 commonMain으로 이동합니다.

// shared/src/commonMain/kotlin/TodoDao.kt

@Dao
interface TodoDao {
  @Insert
  suspend fun insert(item: TodoEntity)

  @Query("SELECT count(*) FROM TodoEntity")
  suspend fun count(): Int

  @Query("SELECT * FROM TodoEntity")
  fun getAllAsFlow(): Flow<List<TodoEntity>>
}

항목commonMain으로 정의하거나 이동합니다.

// shared/src/commonMain/kotlin/TodoEntity.kt

@Entity
data class TodoEntity(
  @PrimaryKey(autoGenerate = true) val id: Long = 0,
  val title: String,
  val content: String
)

플랫폼별 데이터베이스 빌더 만들기

각 플랫폼에서 Room을 인스턴스화하려면 데이터베이스 빌더를 정의해야 합니다. 파일 시스템 API의 차이로 인해 플랫폼별 소스 세트에서 정의해야 하는 API의 유일한 부분입니다.

Android

Android에서는 일반적으로 데이터베이스 위치를 사용하여 가져옵니다. Context.getDatabasePath API. 데이터베이스 인스턴스를 만들려면 Context와 데이터베이스 경로를 지정합니다.

// shared/src/androidMain/kotlin/Database.android.kt

fun getDatabaseBuilder(context: Context): RoomDatabase.Builder<AppDatabase> {
  val appContext = context.applicationContext
  val dbFile = appContext.getDatabasePath("my_room.db")
  return Room.databaseBuilder<AppDatabase>(
    context = appContext,
    name = dbFile.absolutePath
  )
}

iOS

iOS에서 데이터베이스 인스턴스를 만들려면 일반적으로 NSDocumentDirectory에 있는 NSFileManager를 사용하여 데이터베이스 경로를 제공합니다.

// shared/src/iosMain/kotlin/Database.ios.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFilePath = documentDirectory() + "/my_room.db"
    return Room.databaseBuilder<AppDatabase>(
        name = dbFilePath,
    )
}

private fun documentDirectory(): String {
  val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
    directory = NSDocumentDirectory,
    inDomain = NSUserDomainMask,
    appropriateForURL = null,
    create = false,
    error = null,
  )
  return requireNotNull(documentDirectory?.path)
}

JVM 데스크톱

데이터베이스 인스턴스를 만들려면 Java 또는 Kotlin API를 사용하여 데이터베이스 경로를 제공합니다.

// shared/src/jvmMain/kotlin/Database.desktop.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
    return Room.databaseBuilder<AppDatabase>(
        name = dbFile.absolutePath,
    )
}

데이터베이스 인스턴스화

플랫폼별 생성자 중 하나에서 RoomDatabase.Builder를 가져오면 실제 데이터베이스 인스턴스화와 함께 공통 코드에서 나머지 Room 데이터베이스를 구성할 수 있습니다.

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

fun getRoomDatabase(
    builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
  return builder
      .setDriver(BundledSQLiteDriver())
      .setQueryCoroutineContext(Dispatchers.IO)
      .build()
}

SQLite 드라이버 선택

이전 코드 스니펫은 setDriver 빌더 함수를 호출하여 Room 데이터베이스에서 사용할 SQLite 드라이버를 정의합니다. 이러한 드라이버는 타겟 플랫폼에 따라 다릅니다. 이전 코드 스니펫은 BundledSQLiteDriver를 사용합니다. 소스에서 컴파일된 SQLite를 포함하는 권장 드라이버로, 모든 플랫폼에서 가장 일관되고 최신 버전의 SQLite를 제공합니다.

OS에서 제공하는 SQLite를 사용하려면 플랫폼별 드라이버를 지정하는 플랫폼별 소스 세트에서 setDriver API를 사용하세요. 사용 가능한 드라이버 구현에 관한 설명은 드라이버 구현을 참고하세요. 다음 중 하나를 사용할 수 있습니다.

NativeSQLiteDriver를 사용하려면 iOS 앱이 시스템 SQLite와 동적으로 연결되도록 링커 옵션 -lsqlite3을 제공해야 합니다.

// shared/build.gradle.kts

kotlin {
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "TodoApp"
            isStatic = true
            // Required when using NativeSQLiteDriver
            linkerOpts.add("-lsqlite3")
        }
    }
}

코루틴 컨텍스트 설정

Android의 RoomDatabase 객체는 데이터베이스 작업을 실행하기 위해 RoomDatabase.Builder.setQueryExecutor를 사용하여 공유 애플리케이션 실행기로 선택적으로 구성할 수 있습니다.

실행기는 KMP와 호환되지 않으므로 Room의 setQueryExecutor API는 commonMain에서 사용할 수 없습니다. 대신 RoomDatabase 객체를 CoroutineContext로 구성해야 합니다. 이 객체는 RoomDatabase.Builder.setCoroutineContext를 사용하여 설정할 수 있습니다. 컨텍스트를 설정하지 않으면 RoomDatabase 객체의 기본값은 Dispatchers.IO입니다.

압축 및 난독화

프로젝트가 압축되거나 난독화된 경우 Room에서 데이터베이스 정의의 생성된 구현을 찾을 수 있도록 다음 ProGuard 규칙을 포함해야 합니다.

-keep class * extends androidx.room3.RoomDatabase { <init>(); }

Kotlin 멀티플랫폼으로 이전

Room은 원래 Android 라이브러리로 개발되었으며 나중에 API 호환성에 중점을 두고 KMP로 이전되었습니다. Room의 KMP 버전은 플랫폼 간에 그리고 Android 전용 버전과 약간 다릅니다. 이러한 차이점은 다음과 같이 나열되고 설명됩니다.

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

androidx.sqlite.dbSupportSQLiteDatabase 및 기타 API의 모든 사용을 SQLite 드라이버 API로 리팩터링해야 합니다. androidx.sqlite.db의 API는 Android 전용이므로 (KMP 패키지와 다른 패키지임)

이전 버전과의 호환성을 위해 RoomDatabaseSupportSQLiteOpenHelper.Factory로 구성되어 있는 한 (예: SQLiteDriver가 설정되지 않음) Room은 지원 SQLite와 SQLite 드라이버 API가 모두 예상대로 작동하는 '호환성 모드'로 동작합니다. 이렇게 하면 모든 지원 SQLite 사용을 단일 변경사항에서 SQLite 드라이버로 변환할 필요가 없도록 점진적 이전을 사용 설정할 수 있습니다.

Room SQLite 래퍼 사용

androidx.room3:room3-sqlite-wrapper 아티팩트는 이전 중에 SQLiteDriverSupportSQLiteDatabase 간에 연결하는 API를 제공합니다.

SQLiteDriver로 구성된 RoomDatabase에서 SupportSQLiteDatabase를 가져오려면 새 확장 함수 RoomDatabase.getSupportWrapper를 사용하세요. 이 호환성 래퍼는 SupportSQLiteDatabase의 기존 사용을 유지하는 데 도움이 됩니다. 이 사용은 RoomDatabase.openHelper.writableDatabase에서 자주 가져옵니다. 동시에 SQLiteDriver를 채택합니다. 특히 BundledSQLiteDriver를 사용하려는 광범위한 SupportSQLite API 사용이 있는 코드베이스의 경우에 유용합니다.

마이그레이션 하위 클래스 변환

마이그레이션 하위 클래스를 SQLite 드라이버 대응 항목으로 이전해야 합니다.

Kotlin 멀티플랫폼

마이그레이션 하위 클래스

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

자동 마이그레이션 사양 하위 클래스

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

Android 전용

마이그레이션 하위 클래스

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

자동 마이그레이션 사양 하위 클래스

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

데이터베이스 콜백 변환

데이터베이스 콜백을 SQLite 드라이버 대응 항목으로 이전해야 합니다.

Kotlin 멀티플랫폼

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

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

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

Android 전용

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

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

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

@RawQuery DAO 함수 변환

Android 이외의 플랫폼용으로 컴파일된 @RawQuery로 주석 처리된 함수는 SupportSQLiteQuery 대신 RoomRawQuery 유형의 매개변수를 선언해야 합니다.

Kotlin 멀티플랫폼

원시 쿼리 정의

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query: RoomRawQuery): List<TodoEntity>
}

그러면 RoomRawQuery를 사용하여 런타임에 쿼리를 만들 수 있습니다.

suspend fun AppDatabase.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
    val query = RoomRawQuery(
        sql = "SELECT * FROM TodoEntity WHERE title = ?",
        onBindStatement = {
            it.bindText(1, title.lowercase())
        }
    )

    return todoDao().getTodos(query)
}

Android 전용

원시 쿼리 정의

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query: SupportSQLiteQuery): List<TodoEntity>
}

그러면 SimpleSQLiteQuery를 사용하여 런타임에 쿼리를 만들 수 있습니다.

suspend fun AndroidOnlyDao.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
  val query = SimpleSQLiteQuery(
      query = "SELECT * FROM TodoEntity WHERE title = ?",
      bindArgs = arrayOf(title.lowercase())
  )
  return getTodos(query)
}

차단 DAO 함수 변환

Room은 Kotlin이 여러 플랫폼에 제공하는 기능이 풍부한 비동기 kotlinx.coroutines 라이브러리를 활용합니다. 최적의 기능을 위해 기존 코드베이스와의 이전 버전과의 호환성을 유지하기 위해 androidMain에서 구현된 DAO를 제외하고 KMP 프로젝트에서 컴파일된 DAO에는 suspend 함수가 적용됩니다. KMP에 Room을 사용하는 경우 Android 이외의 플랫폼용으로 컴파일된 모든 DAO 함수는 suspend 함수여야 합니다.

Kotlin 멀티플랫폼

쿼리 정지

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

트랜잭션 정지

@Transaction
suspend fun transaction() {  }

Android 전용

쿼리 차단

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

트랜잭션 차단

@Transaction
fun blockingTransaction() {  }

반응형 유형을 흐름으로 변환

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

Kotlin 멀티플랫폼

반응형 유형 Flows

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

Android 전용

LiveData 또는 RxJava의 Flowable과 같은 반응형 유형

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

트랜잭션 API 변환

Room KMP의 데이터베이스 트랜잭션 API는 쓰기(useWriterConnection) 트랜잭션과 읽기 (useReaderConnection) 트랜잭션을 구분할 수 있습니다.

Kotlin 멀티플랫폼

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

Android 전용

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

쓰기 트랜잭션

쓰기 트랜잭션을 사용하여 여러 쿼리가 원자적으로 데이터를 쓰도록 하여 리더가 데이터에 일관되게 액세스할 수 있도록 합니다. 세 가지 트랜잭션 유형 중 하나와 함께 useWriterConnection을 사용하여 이 작업을 실행할 수 있습니다.

  • immediateTransaction: 미리 쓰기 로깅 (WAL) 모드 (기본값)에서 이 유형의 트랜잭션은 시작 시 잠금을 획득하지만 리더는 계속 읽을 수 있습니다. 대부분의 경우에 권장되는 선택사항입니다.

  • deferredTransaction: 트랜잭션은 첫 번째 쓰기 문이 있을 때까지 잠금을 획득하지 않습니다. 트랜잭션 내에서 쓰기 작업이 필요한지 확실하지 않은 경우 이 유형의 트랜잭션을 최적화로 사용하세요. 예를 들어 재생목록의 이름만 지정하고 재생목록이 존재하지 않는 경우 재생목록에서 노래를 삭제하는 트랜잭션을 시작하면 쓰기(삭제) 작업이 필요하지 않습니다.

  • exclusiveTransaction: 이 모드는 WAL 모드의 immediateTransaction과 동일하게 동작합니다. 다른 저널링 모드에서는 트랜잭션이 진행되는 동안 다른 데이터베이스 연결이 데이터베이스를 읽지 못하도록 합니다.

읽기 트랜잭션

읽기 트랜잭션을 사용하여 데이터베이스에서 여러 번 일관되게 읽습니다. 예를 들어 두 개 이상의 별도 쿼리가 있고 JOIN 절을 사용하지 않는 경우입니다. 리더 연결에서는 지연된 트랜잭션만 허용됩니다. 리더 연결에서 즉시 또는 독점 트랜잭션을 시작하려고 하면 예외가 발생합니다. 이러한 트랜잭션은 '쓰기' 작업으로 간주되기 때문입니다.

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

Kotlin 멀티플랫폼에서 사용할 수 없음

Android에서 사용할 수 있었던 일부 API는 Kotlin 멀티플랫폼에서 사용할 수 없습니다.

쿼리 콜백

쿼리 콜백을 구성하기 위한 다음 API는 공통에서 사용할 수 없으므로 Android 이외의 플랫폼에서는 사용할 수 없습니다.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

향후 Room 버전에서 쿼리 콜백 지원을 추가할 계획입니다.

쿼리 콜백 RoomDatabase.Builder.setQueryCallback으로 RoomDatabase를 구성하는 API와 콜백 인터페이스 RoomDatabase.QueryCallback는 공통에서 사용할 수 없으므로 Android 이외의 다른 플랫폼에서는 사용할 수 없습니다.

자동 닫기 데이터베이스

제한 시간 후 자동 닫기를 사용 설정하는 API인 RoomDatabase.Builder.setAutoCloseTimeout은 Android에서만 사용할 수 있으며 다른 플랫폼에서는 사용할 수 없습니다.

사전 패키징된 데이터베이스

기존 데이터베이스 (즉, 사전 패키징된 데이터베이스)를 사용하여 RoomDatabase를 만드는 다음 API는 공통에서 사용할 수 없으므로 Android 이외의 다른 플랫폼에서는 사용할 수 없습니다. 이러한 API는 다음과 같습니다.

  • RoomDatabase.Builder.createFromAsset
  • RoomDatabase.Builder.createFromFile
  • RoomDatabase.Builder.createFromInputStream
  • RoomDatabase.PrepackagedDatabaseCallback

향후 Room 버전에서 사전 패키징된 데이터베이스 지원을 추가할 계획입니다.

멀티 인스턴스 무효화

멀티 인스턴스 무효화를 사용 설정하는 API인 RoomDatabase.Builder.enableMultiInstanceInvalidation은 Android에서만 사용할 수 있으며 공통 또는 다른 플랫폼에서는 사용할 수 없습니다.