Room (Kotlin 멀티플랫폼)

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

종속 항목 설정

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

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

[versions]
room = "2.7.2"
sqlite = "2.5.2"
ksp = "<kotlinCompatibleKspVersion>"

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

# Optional SQLite Wrapper available in version 2.8.0 and higher
androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" }

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

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

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

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

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

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

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

dependencies {
    add("kspAndroid", libs.androidx.room.compiler)
    add("kspIosSimulatorArm64", libs.androidx.room.compiler)
    add("kspIosX64", libs.androidx.room.compiler)
    add("kspIosArm64", libs.androidx.room.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에서 데이터베이스 인스턴스를 만들려면 NSFileManager를 사용하여 데이터베이스 경로를 제공합니다. 이는 일반적으로 NSDocumentDirectory에 있습니다.

// 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.room.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 드라이버로 변환하지 않아도 됩니다.

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

마이그레이션 하위 클래스를 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 라이브러리를 활용합니다. 최적의 기능을 위해 suspend 함수는 KMP 프로젝트에서 컴파일된 DAO에 적용됩니다. 기존 코드베이스와의 하위 호환성을 유지하기 위해 androidMain에 구현된 DAO는 예외입니다. 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 Multiplatform에서 사용할 수 없습니다.

쿼리 콜백

다음 쿼리 콜백 구성 API는 공통으로 제공되지 않으므로 Android 이외의 플랫폼에서는 사용할 수 없습니다.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

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

쿼리 콜백 RoomDatabase.Builder.setQueryCallback과 콜백 인터페이스 RoomDatabase.QueryCallbackRoomDatabase를 구성하는 API는 공통으로 제공되지 않으므로 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에서만 사용할 수 있으며 일반 플랫폼이나 기타 플랫폼에서는 사용할 수 없습니다.