개요
보통 프로젝트를 할 때에는 서버에서 데이터를 받아와 UI를 갱신한다던지, 액세스 토큰값과 같이 적은 양의 데이터를 저장해야 할 때에는 SharedPreference나 DataStore를 사용했었다. 하지만 안드로이드에는 스마트폰 내장 DB인 Room이라는 것이 있다. 이번에는 이것에 대해 정리를 해보려고 한다.
1. Room이란?
Android Jetpack의 데이터 저장 라이브러리로, SQLite를 간편하고 안전하게 사용할 수 있도록 추상화한 ORM(Object Relational Mapping) 라이브러리이다.
즉, SQLite의 복잡한 API를 직접 사용하지 않고, 객체 중심으로 데이터베이스를 다룰 수 있어 코드의 유지보수성을 높여준다.
1-1. SQLite?
경량 관계형 데이터베이스 관리 시스템(RDBMS)로, 안드로이드에서 가장 기본적으로 제공되는 내장형 데이터베이스이다.
파일 기반으로 작동하기 때문에 작은 규모의 데이터 저장에 적합하다.
하지만 얘의 경우 직접 사용하려면 SQL 문을 작성해야 하고, Cursor, ContentValues 같은 객체를 관리해야 해서 코드가 복잡해 진다.
class DatabaseHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL("CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)")
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS users")
onCreate(db)
}
companion object {
private const val DATABASE_NAME = "app_database.db"
private const val DATABASE_VERSION = 1
}
}
-> 위의 코드와 같이 SQLiteOpenHelper를 상속받고, SQL 문을 작성해 사용한다.
1-2. 기본 구성요소

Entity
-> @Entity 어노테이션을 사용해 데이터베이스의 테이블을 정의한다.
-> 이 Entity라는 것은 Room에서 데이터가 저장되는 기본 단위이고, 객체와 데이터베이스 테이블 간의 매핑을 담당한다.
: @PrimaryKey
-> 기본키를 설정한다.
: @ColumnInfo
-> 컬럼명을 지정한다.
: @Ignore
-> 특정 필드를 Room에 저장하지 않는다.
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "user_name") val name: String,
val age: Int
)
DAO (Data Access Object)
-> @Dao 어노테이션을 사용해 데이터 접근 메서드를 정의한다.
-> DAO는 쿼리를 캡슐화하고 데이터베이스의 읽기 및 쓰기 작업을 수행하는 인터페이스이다.
-> Flow나 LiveData를 활용해 데이터의 변경을 감지할 수 있다.
: @Insert, @Update, @Delete
-> CRUD 연산 수행
: @Query
-> 복잡한 SQL 작성 가능
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User)
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
}
Database
-> @Database 어노테이션을 사용해 데이터베이스 인스턴스를 생성한다.
-> 앱에서 단 하나만 존재해야 하는 싱글톤(Singleton) 인스턴스이고, abstract fun을 통해 DAO를 반환한다.
속성
: entities
-> 여기에 포함된 모든 엔티티를 데이터베이스에 등록한다.
: version
-> 데이터베이스 버전 지정 및 마이그레이셔 관리
: getInstance()
-> 싱글톤 인스턴스로 유지한다.
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao // DAO 인스턴스를 제공하는 추상 메서드
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
private fun getDatabase(context : Context): AppDatabase {
return INSTANCE ?: synchronized(this){
val instance = Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
-> 데이터베이스를 생성한 뒤, INSTANCE 변수에 저장해 재사용할 수 있도록 했다.
: @Volatile
-> 여러 스레드에서 동시에 접근해도 최신 값을 즉시 반영할 수 있도록 한다.
: fallbackToDestructiveMigration()
-> 마이그레이션 없이 데이터베이스 버전이 변경된다면 기존 데이터를 삭제하고 새로 생성한다.
2. Room의 Migration
예전 프로젝트를 할때, Room에 엔티티를 추가하면서 version을 올려줬는데 이것 때문에 오류가 발생했던 기억이 있다. 그래서 Migration에 대해서는 반드시 알고 있어야 할 것 같다.
2-1. Room의 Version
-> Version은 데이터베이스의 구조(스키마) 변경을 관리하는 숫자 값이다.
-> 그래서, 데이터베이스의 테이블 구조나 컬럼이 변경될 때, Room에서 얘를 추적하고 적절한 마이그레이션 작업을 수행할 수 있도록 해야 한다.
2-2. Room의 Migration
-> 데이터베이스 스키마가 변경되고, Version이 올라가면서 기존 데이터를 유지하며 새로운 구조로 변환하는 과정이다.
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
-> 위에서 정의한 Database에서 User Entity만 있을 때의 version은 1이다.
-> 하지만 여기에 email이라는 컬럼을 User Entity에 추가한다면?
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "user_name") val name: String,
val email : String
val age: Int
)
@Database(entities = [User::class], version = 2)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
-> 일단 version을 2로 올려준다.
java.lang.IllegalStateException: A migration from version 1 to 2 was required but not found.
-> 이렇게만 끝내면 앱 크래쉬가 발생하면서 종료된다. 즉, Migration을 해줘야한다.
자동
-> 자동적으로 Migration을 선언하기 위해서 database에 autoMigrations 속성에 추가해주면된다.
-> '테이블에 새로운 컬럼 추가 & 새로운 테이블 추가 & 새로운 인덱스 추가' 시 사용할 수 있다.
@Database(
entities = [User::class],
version = 2,
autoMigrations = [
AutoMigration(from = 1, to = 2) // 자동 마이그레이션 적용
]
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
-> 이렇게 한다면 Migration 클래스를 만들지 않아도 자동으로 실행된다.
수동
-> Migration 객체를 만들어준다.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''")
}
}
-> 그 뒤 databaseBuilder에 Migration 객체를 추가해주면 된다.
val db = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_1_2) // Migration 추가
.build()
기존 데이터 버리기
-> 위에 Database 객체 생성할 때 사용했던 fallbackToDestructiveMigration()
-> 이걸 쓰면 Migration 설정하지 않고 데이터베이스 구조를 강제로 변경시키지만, 기존에 있던 데이터가 삭제된다!
'Android > Android Jetpack' 카테고리의 다른 글
| [Android] Jetpack - (3) Paging3 (1) | 2024.12.01 |
|---|---|
| [Android] Jetpack - (2) WorkManager (0) | 2024.07.18 |
| [Android] Jetpack - (1) ViewModel (0) | 2024.07.11 |