[Android ๊ธฐ์ดˆ] 16. Room (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค)
๋ฐ˜์‘ํ˜•

 

 

 

 

 

์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ์˜ ์˜๊ตฌ(๋น„ํœ˜๋ฐœ์„ฑ) ๋ฐ์ดํ„ฐ ์ €์žฅ๋ฒ•

  1. SharedPreference
  2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
  3. ํŒŒ์ผ

 

 


 

1) Room ๊ฐœ์š”

 

๋ฐฉ  |  Jetpack  |  Android Developers

์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉํ•ด ์ •๋ฆฌํ•˜๊ธฐ ๋‚ด ํ™˜๊ฒฝ์„ค์ •์„ ๊ธฐ์ค€์œผ๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถ„๋ฅ˜ํ•˜์„ธ์š”. Room Room ์ง€์†์„ฑ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” SQLite์— ์ถ”์ƒํ™” ๋ ˆ์ด์–ด๋ฅผ

developer.android.com

  • SQLite๋ฅผ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ์ฒด ๋งคํ•‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  • ์‰ฝ๊ฒŒ Query๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” API๋ฅผ ์ œ๊ณต
  • Query๋ฅผ ์ปดํŒŒ์ผ ์‹œ๊ฐ„์— ๊ฒ€์ฆํ•จ
  • Query๊ฒฐ๊ณผ๋ฅผ LiveData๋กœํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๋งˆ๋‹ค ์‰ฝ๊ฒŒ UI๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Œ
  • SQLite ๋ณด๋‹ค Room์„ ์‚ฌ์šฉํ•  ๊ฒƒ์„ ๊ถŒ์žฅํ•จ

 

 

 

2) Room ์ฃผ์š” 3์š”์†Œ

  • @Database
    - ํด๋ž˜์Šค๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ์ง€์ •ํ•˜๋Š” annotation, RoomDatabase๋ฅผ ์ƒ์† ๋ฐ›์€ ํด๋ž˜์Šค์—ฌ์•ผ ํ•จ
    - Room.databaseBuilder๋ฅผ ์ด์šฉํ•˜์—ฌ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•จ
  • @Entity
    - ํด๋ž˜์Šค๋ฅผ ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ๋กœ ์ง€์ •ํ•˜๋Š” annotation
  • @Dao
    - ํด๋ž˜์Šค๋ฅผ DAO(Data Access Object)๋กœ ์ง€์ •ํ•˜๋Š” annotation
    - ๊ธฐ๋ณธ์ ์ธ insert, delete, update SQL์€ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ๋ฉฐ, ๋ณต์žกํ•œ SQL์€ ์ง์ ‘ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Œ

 

 

 

3) gradle ํŒŒ์ผ ์„ค์ •

plugins {
		..
    kotlin("kapt")
}
..

dependencies {

    ..
    val room_version = "2.6.1" // ์ตœ์‹  ๋ฒ„์ „ ์•ˆ๋“œ๋กœ์ด๋“œ ๊ฒ€์ƒ‰ํ•ด์„œ ๋“ค์–ด๊ฐ€๊ธฐ
    implementation("androidx.room:room-runtime:$room_version")
    annotationProcessor("androidx.room:room-compiler:$room_version")
    kapt("androidx.room:room-compiler:$room_version") // To use Kotlin annotation processing tool (kapt)
    
    
    // To use Kotlin Symbol Processing (KSP)
    ksp("androidx.room:room-compiler:$room_version")

    // optional - Kotlin Extensions and Coroutines support for Room
    implementation("androidx.room:room-ktx:$room_version")

    // optional - RxJava2 support for Room
    implementation("androidx.room:room-rxjava2:$room_version")

    // optional - RxJava3 support for Room
    implementation("androidx.room:room-rxjava3:$room_version")

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation("androidx.room:room-guava:$room_version")

    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$room_version")

    // optional - Paging 3 Integration
    implementation("androidx.room:room-paging:$room_version")
}
  • Room์€ ์•ˆ๋“œ๋กœ์ด๋“œ ์•„ํ‚คํ…์ฒ˜์— ํฌํ•จ๋˜์–ด ์žˆ์Œ
  • ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด build.gradle ํŒŒ์ผ์˜ dependencies์— ์•„๋ž˜ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•จ
        - Androidx ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๊ฐ€์ •ํ•จ, Android Studio์™€ SDK๋Š” ์ตœ์‹  ๋ฒ„์ „์œผ๋กœ ์‚ฌ์šฉ
        - 'kotlin-kapt' ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€
        - dependencies ์ถ”๊ฐ€

 

 

 

 4) Entity ์ƒ์„ฑ

CREATE TABLE student_table (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL); ์˜ ๊ฒฝ์šฐ

@Entity(tableName = "student_table")    // ํ…Œ์ด๋ธ” ์ด๋ฆ„์„ student_table๋กœ ์ง€์ •ํ•จ
data class Student (
    @PrimaryKey @ColumnInfo(name = "student_id") val id: Int,
    val name: String
)
  • Entity๋Š” ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ์ •์˜
    (๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ํ…Œ์ด๋ธ”์„ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ๊ฐ ํ…Œ์ด๋ธ”๋“ค์€ ์–ด๋–ค ์ปฌ๋Ÿผ๋“ค์„ ๊ฐ€์ง€๊ณ  ์žˆ์„์ง€ ์ •์˜ํ•˜๋Š” ๊ฒƒ์ด ์Šคํ‚ค๋งˆ)
  • @Entity data class Student

 

 

 

5) DAO ์ƒ์„ฑ

@Query("SELECT * from table") fun getAllData() : List<Data>
  • DAO๋Š” interface๋‚˜ abstract class๋กœ ์ •์˜๋˜์–ด์•ผ ํ•จ
  • Annotation์— SQL ์ฟผ๋ฆฌ๋ฅผ ์ •์˜ํ•˜๊ณ  ๊ทธ ์ฟผ๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ฉ”์†Œ๋“œ๋ฅผ ์„ ์–ธ
  • ๊ฐ€๋Šฅํ•œ annotation์œผ๋กœ @Insert, @Update, @Delete, @Query๊ฐ€ ์žˆ์Œ
  • @Insert, @Update, @Delete๋Š” SQL ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š์•„๋„ ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•จ
        - @Insert๋‚˜ @Update๋Š” key๊ฐ€ ์ค‘๋ณต๋˜๋Š” ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด onConflict๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Œ
            - OnConflictStrategy.ABORT: key ์ถฉ๋Œ์‹œ ์ข…๋ฃŒ
            - OnConflictStrategy.IGNORE: key ์ถฉ๋Œ ๋ฌด์‹œ
            - OnConflictStrategy.REPLACE: key ์ถฉ๋Œ์‹œ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋กœ ๋ณ€๊ฒฝ
        - @Update๋‚˜ @Delete๋Š” primary key์— ํ•ด๋‹น๋˜๋Š” ํŠœํ”Œ์„ ์ฐพ์•„์„œ ๋ณ€๊ฒฝ/์‚ญ์ œ ํ•จ

 

@Query("SELECT * from table") fun getAllData() : LiveData<List<Data>>
  • @Query๋กœ ๋ฆฌํ„ด๋˜๋Š” ๋ฐ์ดํ„ฐ์˜ ํƒ€์ž…์„ LiveData<>๋กœ ํ•˜๋ฉด,
    ๋‚˜์ค‘์— ์ด ๋ฐ์ดํ„ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ๋  ๋•Œ Observer๋ฅผ ํ†ตํ•ด ํ•  ์ˆ˜ ์žˆ์Œ

 

@Query("SELECT * FROM student_table WHERE name = :sname")
suspend fun getStudentByName(sname: String): List<Student>
  • @Query์— SQL์„ ์ •์˜ํ•  ๋•Œ ๋ฉ”์†Œ๋“œ์˜ ์ธ์ž๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ (์˜ˆ: ์ธ์ž sname์„ SQL์—์„œ :sname์œผ๋กœ ์‚ฌ์šฉ)

 

@Dao
interface MyDAO {
    @Insert(onConflict = OnConflictStrategy.REPLACE)  // INSERT, key ์ถฉ๋Œ์ด ๋‚˜๋ฉด ์ƒˆ ๋ฐ์ดํ„ฐ๋กœ ๊ต์ฒด
    suspend fun insertStudent(student: Student)

    @Query("SELECT * FROM student_table")
    fun getAllStudents(): LiveData<List<Student>>        // LiveData<> ์‚ฌ์šฉ

    @Query("SELECT * FROM student_table WHERE name = :sname")   
    suspend fun getStudentByName(sname: String): List<Student>

    @Delete
    suspend fun deleteStudent(student: Student); // primary key is used to find the student

    // ...
}
  • fun ์•ž์— suspend๋Š” Kotlin coroutine์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ
    ๋‚˜์ค‘์— ์ด ๋ฉ”์†Œ๋“œ๋ฅผ ๋ถ€๋ฅผ ๋•Œ๋Š” runBlocking {} ๋‚ด์—์„œ ํ˜ธ์ถœํ•ด์•ผ ํ•จ
  • LiveData๋Š” ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— coroutine์œผ๋กœ ํ•  ํ•„์š”๊ฐ€ ์—†์Œ
  • @Query("SELECT * from table") fun getAllData() : LiveData<List<Data>>
    @Query("SELECT * FROM student_table WHERE name = :sname")
    suspend fun getStudentByName(sname: String): List<Student>
  • ์ธ์ž sname์„ SQL์—์„œ :sname์œผ๋กœ ์‚ฌ์šฉ

 

 

 

6) Database ์ƒ์„ฑ

@Database(entities = [Student::class, ClassInfo::class, Enrollment::class, Teacher::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
    abstract fun getMyDao() : MyDAO

    companion object {
        private var INSTANCE: MyDatabase? = null
        private val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) { ์ƒ๋žต }
        }

        private val MIGRATION_2_3 = object : Migration(2, 3) {
            override fun migrate(database: SupportSQLiteDatabase) { ์ƒ๋žต }
        }
        fun getDatabase(context: Context) : MyDatabase {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(
                    context, MyDatabase::class.java, "school_database")
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                    .build()
            }
            return INSTANCE as MyDatabase
        }
    }
}
  • RoomDatabase๋ฅผ ์ƒ์†ํ•˜์—ฌ ์ž์‹ ์˜ Room ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•จ
  • ํฌํ•จ๋˜๋Š” Entity๋“ค๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฒ„์ „(version)์„ @Database annotation์— ์ง€์ •ํ•จ
        - version์ด ๊ธฐ์กด์— ์ €์žฅ๋˜์–ด ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ณด๋‹ค ๋†’์œผ๋ฉด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ openํ•  ๋•Œ migration์„ ์ˆ˜ํ–‰ํ•˜๊ฒŒ ๋จ
        - Migration ์ˆ˜ํ–‰ ๋ฐฉ๋ฒ•์€ RoomDatabase ๊ฐ์ฒด์˜ addMigration() ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ์•Œ๋ ค์คŒ
  • DAO๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” getter ๋ฉ”์†Œ๋“œ๋ฅผ ๋งŒ๋“ฌ
        - ์‹ค์ œ ๋ฉ”์†Œ๋“œ ์ •์˜๋Š” ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋จ
  • Room ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋Š” ํ•˜๋‚˜๋งŒ ์žˆ์œผ๋ฉด ๋˜๋ฏ€๋กœ Singleton ํŒจํ„ด์„ ์‚ฌ์šฉ
  • Room ํด๋ž˜์Šค์˜ ๊ฐ์ฒด ์ƒ์„ฑ์€ Room.databaseBuilder()๋ฅผ ์ด์šฉํ•จ

 

 

 

7) Migration

Room.databaseBuilder(...).addMigrations(MIGRATION_1_2, MIGRATION_2_3)

private val MIGRATION_1_2 = object : Migration(1, 2) {   // version 1 -> 2
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
    }
}

private val MIGRATION_2_3 = object : Migration(2, 3) {   // version 2 -> 3
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE class_table ADD COLUMN last_update INTEGER")
    }
}
  • ์•ž์—์„œ MyRoomDatabase๊ฐ์ฒด ์ƒ์„ฑ ํ›„ addMigrations() ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ Migration ๋ฐฉ๋ฒ•์„ ์ง€์ •ํ–ˆ์Œ
  • ์—ฌ๋Ÿฌ๊ฐœ์˜ Migration ์ง€์ • ๊ฐ€๋Šฅ

 

 

 

8) UI์™€ ์—ฐ๊ฒฐ

myDao = MyDatabase.getDatabase(this).getMyDao()
runBlocking { // (์ฃผ์˜) UI๋ฅผ ๋ธ”๋กํ•  ์ˆ˜ ์žˆ๋Š” DAO ๋ฉ”์†Œ๋“œ๋ฅผ UI ์Šค๋ ˆ๋“œ์—์„œ ๋ฐ”๋กœ ํ˜ธ์ถœํ•˜๋ฉด ์•ˆ๋จ
    myDao.insertStudent(Student(1, "james"))  // suspend ์ง€์ •๋˜์–ด ์žˆ์Œ
}
val allStudents = myDao.getAllStudents() // LiveData๋Š” Observer๋ฅผ ํ†ตํ•ด ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ด
  • ์•ˆ๋“œ๋กœ์ด๋“œ ์•„ํ‚คํ…์ฒ˜์— ๋”ฐ๋ผ Repository์™€ ViewModel์„ ์‚ฌ์šฉํ•˜๊ธธ ๊ถŒ์žฅํ•˜์ง€๋งŒ
        - ์ฐธ๊ณ : https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/
        - ์—ฌ๊ธฐ์—์„œ๋Š” Room๊ณผ LiveData ์‚ฌ์šฉ๋งŒ ๋‹ค๋ฃธ
  • RoomDatabase๊ฐ์ฒด์—์„œ DAO ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„์˜ค๊ณ , ์ด DAO๊ฐ์ฒด์˜ ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ ‘๊ทผํ•จ

 

 

 

9) UI์™€ ์—ฐ๊ฒฐ - LiveData

 

LiveData?

  • ์•ˆ๋“œ๋กœ์ด๋“œ ์•„ํ‚คํ…์ฒ˜ ์ปดํฌ๋„ŒํŠธ์˜ ์ผ๋ถ€๋กœ, ๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ ํ™€๋” ํด๋ž˜์Šค
  • ์ด๋ฅผ ํ†ตํ•ด UI ์ปดํฌ๋„ŒํŠธ(์˜ˆ: ์•กํ‹ฐ๋น„ํ‹ฐ, ํ”„๋ž˜๊ทธ๋จผํŠธ)๋Š” ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๊ด€์ฐฐํ•˜๊ณ  ์ด์— ๋ฐ˜์‘
  • ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค LiveData๋Š” ๊ด€์ฐฐ์ž์—๊ฒŒ ์•Œ๋ฆผ์„ ๋ณด๋ƒ„

 

LiveData์˜ ํ•ต์‹ฌ ํŠน์ง•

  • 1. ์ˆ˜๋ช…์ฃผ๊ธฐ ์ธ์‹
    - LiveData๋Š” ์•ˆ๋“œ๋กœ์ด๋“œ์˜ ์ˆ˜๋ช…์ฃผ๊ธฐ๋ฅผ ์ธ์‹
    - ์ฆ‰, ์•กํ‹ฐ๋น„ํ‹ฐ๋‚˜ ํ”„๋ž˜๊ทธ๋จผํŠธ์˜ ์ˆ˜๋ช…์ฃผ๊ธฐ ์ƒํƒœ์— ๋”ฐ๋ผ ์•Œ๋ฆผ์„ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌ
    - ์ด๋กœ ์ธํ•ด ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐ ์•กํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์ข…๋ฃŒ๋œ ์ƒํƒœ์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ํฌ๋ž˜์‹œ๋ฅผ ๋ฐฉ์ง€
  • 2. UI์™€ ๋ฐ์ดํ„ฐ ์ƒํƒœ์˜ ์ผ๊ด€์„ฑ ์œ ์ง€
    - LiveData๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด UI๊ฐ€ ๋ฐ์ดํ„ฐ์™€ ์ผ๊ด€๋˜๊ฒŒ ์œ ์ง€
    - ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ UI๊ฐ€ ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ๋˜๊ธฐ ๋•Œ๋ฌธ์—, ์‚ฌ์šฉ์ž์—๊ฒŒ ์ตœ์‹ ์˜ ์ •๋ณด๋ฅผ ์ œ๊ณต
  • 3. ์ค‘์•™ ์ง‘์ค‘์ ์ธ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ
    - LiveData๋Š” ๋ทฐ๋ชจ๋ธ(ViewModel)๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜์–ด ์•ฑ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ค‘์•™์—์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•จ
    - ์ด๋Š” ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ๋ฅผ ๋”์šฑ ํšจ์œจ์ ์œผ๋กœ ๋งŒ๋“ค์–ด ์คŒ
  • 4. ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ์ž๋™ ์—…๋ฐ์ดํŠธ
    - LiveData์˜ ๊ด€์ฐฐ์ž๋Š” ์˜ค์ง ํ™œ์„ฑ ์ˆ˜๋ช…์ฃผ๊ธฐ ์ƒํƒœ(active lifecycle state)์˜ ์ปดํฌ๋„ŒํŠธ์—๋งŒ ์•Œ๋ฆผ์„ ๋ณด๋ƒ„
    - ์ด๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ํ™œ์„ฑ ์ƒํƒœ์˜ UI๋งŒ ์—…๋ฐ์ดํŠธ๋˜์–ด, ๋ถˆํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ์„ ์ค„์—ฌ์คŒ

 

val allStudents = myDao.getAllStudents()
allStudents.observe(this) {   // Observer::onChanged() ๋Š” SAM ์ด๊ธฐ ๋•Œ๋ฌธ์— lambda๋กœ ๋Œ€์ฒด
    val str = StringBuilder().apply {
            for ((id, name) in it) {
                append(id)
                append("-")
                append(name)
                append("\n")
            }
        }.toString()
    binding.textStudentList.text = str
}
  • LiveData<> ํƒ€์ž…์œผ๋กœ ๋ฆฌํ„ด๋˜๋Š” DAO ๋ฉ”์†Œ๋“œ ๊ฒฝ์šฐ
        - observe() ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•˜์—ฌ Observer๋ฅผ ์ง€์ •
        - ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ Observer์˜ onChanged()๊ฐ€ ํ˜ธ์ถœ๋จ
  • LiveData<>๋ฅผ ๋ฆฌํ„ดํ•˜๋Š” DAO ๋ฉ”์†Œ๋“œ๋Š” Observer๋ฅผ ํ†ตํ•ด ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ธฐ ๋•Œ๋ฌธ์—, UI ์Šค๋ ˆ๋“œ์—์„œ ์ง์ ‘ ํ˜ธ์ถœํ•ด๋„ ๋ฌธ์ œ ์—†์Œ

 

 

10) Room Database์˜ ์ฃผ์š” ์–ด๋…ธํ…Œ์ด์…˜(Annotations)

  • @Database
        - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ•จ
        - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ํฌํ•จ๋  ์—”ํ‹ฐํ‹ฐ์™€ ๋ฒ„์ „์„ ๋ช…์‹œํ•จ
  • @Entity
        - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋‚ด์˜ ํ…Œ์ด๋ธ”์„ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ•จ
        - ํด๋ž˜์Šค ์ด๋ฆ„์ด ํ…Œ์ด๋ธ” ์ด๋ฆ„์œผ๋กœ ์‚ฌ์šฉ๋˜๋ฉฐ, ํ•„๋“œ๋Š” ์ปฌ๋Ÿผ์œผ๋กœ ๋งคํ•‘๋จ
  • @PrimaryKey
        - ์—”ํ‹ฐํ‹ฐ์˜ ๊ธฐ๋ณธ ํ‚ค(primary key)๋ฅผ ์ •์˜ํ•จ
        - ์œ ๋‹ˆํฌํ•œ ๊ฐ’์ด์–ด์•ผ ํ•˜๋ฉฐ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋‚ด์—์„œ ๊ฐ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋จ
  • @ColumnInfo
        - ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ์ •๋ณด๋ฅผ ์„ธ๋ถ€์ ์œผ๋กœ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ•จ
        - ์ปฌ๋Ÿผ์˜ ์ด๋ฆ„, ํƒ€์ž…, ์ธ๋ฑ์Šค ๋“ฑ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Œ
  • @Dao
        - ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ฐ์ฒด(Data Access Object)๋ฅผ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ•จ
        - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ CRUD(Create, Read, Update, Delete) ์—ฐ์‚ฐ์„ ์œ„ํ•œ ๋ฉ”์†Œ๋“œ๋ฅผ ํฌํ•จํ•จ
  • @Insert
        - ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฝ์ž…ํ•˜๋Š” ๋ฉ”์†Œ๋“œ์— ์‚ฌ์šฉํ•จ
        - ํ•ด๋‹น ๋ฉ”์†Œ๋“œ๋Š” ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ธ์ž๋กœ ๋ฐ›์•„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ถ”๊ฐ€ํ•จ
  • @Query
        - ๋ณต์žกํ•œ SQL ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๋ฉ”์†Œ๋“œ์— ์‚ฌ์šฉํ•จ
        - ๋ฉ”์†Œ๋“œ์— ์ฃผ์–ด์ง„ SQL ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•จ
  • @Update
        - ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฉ”์†Œ๋“œ์— ์‚ฌ์šฉํ•จ
        - ์ธ์ž๋กœ ๋ฐ›์€ ์—”ํ‹ฐํ‹ฐ์˜ ๋ฐ์ดํ„ฐ๋กœ ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ๋ฅผ ๊ฐฑ์‹ ํ•จ
  • @Delete
        - ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋ฉ”์†Œ๋“œ์— ์‚ฌ์šฉํ•จ
        - ์ธ์ž๋กœ ๋ฐ›์€ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ œ๊ฑฐํ•จ
  • @Transaction
        - ๋ฉ”์†Œ๋“œ๊ฐ€ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์‹คํ–‰๋˜์–ด์•ผ ํ•จ์„ ๋‚˜ํƒ€๋ƒ„
        - ์—ฌ๋Ÿฌ ์—ฐ์‚ฐ์„ ํ•˜๋‚˜์˜ ์ž‘์—…์œผ๋กœ ๋ฌถ์–ด ์‹คํ–‰ํ•  ๋•Œ ์‚ฌ์šฉํ•จ
  • @ForeignKey
        - ์—”ํ‹ฐํ‹ฐ ๊ฐ„์˜ ์™ธ๋ž˜ ํ‚ค ๊ด€๊ณ„๋ฅผ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ•จ
        - ์ฐธ์กฐ ๋ฌด๊ฒฐ์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ๋ฐ ๋„์›€์„ ์คŒ
  • @Index
        - ํŠน์ • ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ์‚ฌ์šฉํ•จ
        - ์ฟผ๋ฆฌ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๋ฐ ์œ ์šฉํ•จ

 

 

11) Room ์˜ˆ์ œ

๋”๋ณด๊ธฐ

 

build.gradle(:app)

plugins {
    alias(libs.plugins.androidApplication)
    alias(libs.plugins.jetbrainsKotlinAndroid)
    kotlin("kapt")
}

android {
    namespace = "com.limheejin.test"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.limheejin.test"
        minSdk = 26
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }

    buildFeatures {
        viewBinding = true
    }
}

dependencies {
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.material)
    implementation(libs.androidx.activity)
    implementation(libs.androidx.constraintlayout)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)

    val room_version = "2.6.1"
    implementation("androidx.room:room-runtime:$room_version")
    annotationProcessor("androidx.room:room-compiler:$room_version")
    kapt("androidx.room:room-compiler:$room_version")
    implementation("androidx.room:room-ktx:$room_version")
    testImplementation("androidx.room:room-testing:$room_version")
}

 

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="5dp"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/edit_student_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="ID"
        android:inputType="number"
        app:layout_constraintEnd_toStartOf="@+id/query_student"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintHorizontal_chainStyle="spread_inside"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/edit_student_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="student name"
        android:inputType="textPersonName"
        app:layout_constraintEnd_toStartOf="@+id/add_student"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintHorizontal_chainStyle="spread_inside"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/edit_student_id" />

    <Button
        android:id="@+id/add_student"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Add Student"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/edit_student_name"
        app:layout_constraintTop_toBottomOf="@+id/query_student" />

    <Button
        android:id="@+id/query_student"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Query Student"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/edit_student_id"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="Result of Query Student"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/edit_student_name" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="Student List"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text_query_student" />

    <TextView
        android:id="@+id/text_query_student"
        android:layout_width="0dp"
        android:layout_height="100sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <TextView
        android:id="@+id/text_student_list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView2" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

MyEntity

package com.limheejin.test

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "student_table")
data class Student(
    @PrimaryKey @ColumnInfo(name = "student_id") val id: Int,
    val name: String
)

 

MyDatabase.kt

package com.limheejin.test

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

@Database(entities = [Student::class], exportSchema = false, version = 1)
abstract class MyDatabase : RoomDatabase() {
    abstract fun getMyDao(): MyDAO

    companion object {
        private var INSTANCE: MyDatabase? = null
        private val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(db: SupportSQLiteDatabase) {
            }
        }

        private val MIGRATION_2_3 = object : Migration(2, 3) {
            override fun migrate(db: SupportSQLiteDatabase) {
                db.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
            }
        }

        fun getDatabase(context: Context): MyDatabase {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(
                    context, MyDatabase::class.java, "school_database"
                )
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                    .build()
                // for in-memory database
                /*INSTANCE = Room.inMemoryDatabaseBuilder(
                    context, MyDatabase::class.java
                ).build()*/
            }
            return INSTANCE as MyDatabase
        }
    }
}

 

MyDao

package com.limheejin.test

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface MyDAO {
    @Insert(onConflict = OnConflictStrategy.REPLACE) // INSERT, key ์ถฉ๋Œ์ด ๋‚˜๋ฉด ์ƒˆ ๋ฐ์ดํ„ฐ๋กœ ๊ต์ฒด
    suspend fun insertStudent(student: Student) // suspend๋Š” ์ฝ”๋ฃจํ‹ด์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ (๋ผ์ด๋ธŒ๋ฐ์ดํ„ฐ์˜ ๊ฒฝ์šฐ ํ•„์š” ์—†์Œ)

    @Query("SELECT * FROM student_table")
    fun getAllStudents(): LiveData<List<Student>> // LiveData<> ์‚ฌ์šฉ

    @Query("SELECT * FROM student_table WHERE name = :sname") // ๋ฉ”์†Œ๋“œ ์ธ์ž๋ฅผ SQL๋ฌธ์—์„œ :๋ฅผ ๋ถ™์—ฌ ์‚ฌ์šฉ
    suspend fun getStudentByName(sname: String): List<Student>

    @Delete
    suspend fun deleteStudent(student: Student);

}

 

MainActivity.kt

package com.limheejin.test

import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.limheejin.test.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity() {
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    lateinit var myDao: MyDAO

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        myDao = MyDatabase.getDatabase(this).getMyDao()

        val allStudents = myDao.getAllStudents()
        allStudents.observe(this) {
            val str = StringBuilder().apply {
                for ((id, name) in it) {
                    append(id)
                    append("-")
                    append(name)
                    append("\n")
                }
            }.toString()
            binding.textStudentList.text = str
        }

        binding.addStudent.setOnClickListener {
            val id = binding.editStudentId.text.toString().toInt()
            val name = binding.editStudentName.text.toString()
            if (id > 0 && name.isNotEmpty()) {
                CoroutineScope(Dispatchers.IO).launch {
                    myDao.insertStudent(Student(id, name))
                }
            }
            binding.editStudentId.text = null
            binding.editStudentName.text = null
        }

        binding.queryStudent.setOnClickListener {
            val name = binding.editStudentName.text.toString()
            CoroutineScope(Dispatchers.IO).launch {
                val results = myDao.getStudentByName(name)
                if (results.isNotEmpty()) {
                    val str = StringBuilder().apply {
                        results.forEach { student ->
                            append(student.id)
                            append("-")
                            append(student.name)
                        }
                    }
                    withContext(Dispatchers.Main) {
                        binding.textQueryStudent.text = str
                    }
                } else {
                    withContext(Dispatchers.Main) {
                        binding.textQueryStudent.text = ""
                    }
                }
            }
        }
    }
}

 

 

 

App Insepection์—์„œ ์ €์žฅํ•œ Room ๋ฐ์ดํ„ฐ ํ™•์ธ ๊ฐ€๋Šฅ

 

 

 

 

 

๋ฐ˜์‘ํ˜•
 ๐Ÿ’ฌ C O M M E N T