Search
castle

Room DB

안드로이드 용 ORM

  • ORM Library
  • 의존성 추가
// build.gradle.kts (project)
plugins {
	id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false}

// build.gradle.kts (:app)
val nav_version = "2.9.3"

dependencies {
	// Room 런타임
    implementation("anddroidx.room:room-runtime:${nav_version}")
    // Room 컴파일러
    ksp("androidx.room:room-compiler:${nav_version}")
    // Kotlin 확장/코루틴 지
    implementation("androidx.room:room-ktx:${nav_version}")
}
  • java style
    • userDefiendDatabase::class.java : java style로의 변환이 필요할 수 있음
  • SQLite 이용
  • 인터페이스와 어노테이션 기반으로 추상화
    • 직접 SQL 작성 X
  • DAO(Data Access Object), Entity, Database 구성하여 사용

ORM(Object-Relational Mapping)

  • 객체(Class)와 관계형 데이터베이스의 데이터(Table)를 매핑, 반환하는 기술
  • 코드 → database 컨트롤

image.png

장점

  • 직관적, 가독성 좋음
  • SQL 오류 방지(컴파일 검증)
  • jetpack compose(StateFlow/LiveData/Flow)와 연동 가능
  • Migration 처리 간편
  • Kotlin Coroutine과 통합 용이
  • 기타
    • 객체지향적 접근 → 생산성 증가
    • 재사용 및 유지보수 용이성 증가
    • DBMS에 대한 종속성 감소

단점

  • 잘못 구현된 경우 속도 저하
  • 복잡한 Query는 Query DSL 등을 이용해 처리 필요

구성 요소

구성 요소 설명
Entity 테이블을 정의하는 데이터 클래스(SQLite 테이블과 대응)
DAO(Data Access Object) DB 접근 메서드를 정의한 인터페이스(SQL 쿼리 추상화)
Database RoomDatabase를 상속받아 DB 객체 생성 및 DAO 제공

Entity

  • tableName : table 이름 지정(defaut: ClassName)
속성 (Annotation) 설명
tableName 테이블 이름을 지정(default: ClassName)
indices 인텍스를 설정, 성능 향상 또는 중복 방지를 위해 사용.
ignredColums DB에 저장하지 않을 필드. 이름을 명시(@ignore와 유사한 역할)
PrimaryKeys primary key 설정
foreignKeys foreign key 설정

@Entity(
	tableName = "user"
	indices = [Index(value = ["email"], unique = true)],
	primaryKeys = ["uid"],
	foreignKeys = [ForeignKey(...)],
	ignoredColumns = ["tempData"]
)

data class User(
	val id: Int
	val fistName: String
	val email
	@ignore val tempData: String = ""
)
  • 필드 어노테이션
    • property 지정
    • @Entity : SQLite 테이블에 매핑되는 데이터 클래스에 붙임
    • @ignore : DB column으로 사용 X
속성 (Annotation) 설명 예시
@Entity 해당 클래스가 데이터베이스 테이블임을 선언합니다. 기본적으로 클래스 이름을 테이블 이름으로 사용하며, tableName 속성을 통해 변경할 수 있습니다. @Entity(tableName = "users")
@PrimaryKey 테이블의 기본 키(Primary Key)를 정의합니다. 해당 열은 각 행을 고유하게 식별하며, autoGenerate = true 속성을 통해 기본 키 값을 자동으로 생성하도록 할 수 있습니다. @PrimaryKey(autoGenerate = true) val id: Int
@ColumnInfo 필드 이름을 테이블의 컬럼 이름과 다르게 지정하거나, 컬럼의 다른 속성을 정의할 때 사용합니다. @ColumnInfo(name = "user_name") val name: String
@Embedded 복합 객체를 내부 컬럼으로 분해  
@NotNull not null  
@Ignore 해당 필드를 ROOM이 무시하도록 설정  
tableName 테이블 이름을 지정(default: ClassName)  
autoGenerate @PrimaryKey 속성과 함께 사용되어 기본 키 값을 자동으로 생성하도록 합니다. @PrimaryKey(autoGenerate = true) val id: Int
name 컬럼 이름 지정(default: 필드 이름)  
typeAffinity 컬럼의 데이터 타입을 지정  
Class의 변수 type이 아닌 SQLtype을 의미    
index 컬럼에 인덱스를 생성할지 여부를 지정  
collate 컬럼의 정렬 방식을 지정  

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    // @PrimaryKey(autoGenerate = true, val id: Int = 0)
    @ColumnInfo(name = "first_name")
    val firstName: String?,
    @ColumnInfo(name = "last_name")
    val lastName: String? = "",
)

DAO(Data Access Object)

  • Database CRUID 실행
  • interface지만 상속 및 구현 존재X
  • debug 어려움
    • compile 탐지, info 등 거의 없음
  • @Dao : SQL 쿼리를 추상화한 인터페이스 또는 추상 클래스의 어노테이션
  • 일반적으로 coroutine과의 연동을 위해 suspend 속성을 사용
  • SELECT 지원X
  • @Query : SELECT query 작성
    • :property : 동적 쿼리 매개변수(?) 지정
// @Dao 어노테이션은 이 인터페이스가 데이터 접근 객체임을 나타냅니다.
@Dao
interface UserDao {

    // @Query 어노테이션은 SQL 쿼리를 정의합니다. 모든 사용자를 가져옵니다.
    @Query("SELECT * FROM user")
    fun getAllUsers(): List<User> // User 객체의 리스트를 반환합니다.
    // 특정 Query 작성
    @Query("SELECT * FROM user WHERE id = :id")  // 동적 쿼리 매개변수
    fun getAllUsers(id: Int): Flow<List<User>> // User 객체의 리스트 조건절 사용.

    // @Insert 어노테이션은 User 객체를 데이터베이스에 삽입합니다.
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User)

    // @Update 어노테이션은 기존 User 객체를 업데이트합니다.
    @Update
    fun updateUser(user: User)

    // @Delete 어노테이션은 특정 User 객체를 삭제합니다.
    @Delete
    fun deleteUser(user: User)

    // 특정 ID로 사용자를 찾는 쿼리입니다.
    @Query("SELECT * FROM user WHERE uid = :userId")
    fun getUserById(userId: Int): User? // User 객체 또는 null을 반환합니다.
}
  • 주요 메서드 어노테이션
어노테이션 설명
@Insert 삽입, 하나 혹은 여려개의 Entity 삽입
@Update  
@Delete  
@Query  
onConflict CID 공통 속성, 충돌 발생 시 전략 설정(REPLACE, IGNORE, ABORT 등)

suspend

  • suspend
  • UI 스레드(Main Thred)에서 긴 작업을 하면 ANR 발생
  • ROOM DB의 쿼리도 I/O 작업 → UI 스레드에서 작동 시 앱이 멈출 수 있음
lifecycleScope.launch {
	val users = userDao.getAllUsers()  // suspend 함수 호출
	...
}

Database

  • @Database : ROOM DB 전체를 나타내는 추상 클래스에 붙음
    • RoomDatabase를 상속
  • Todo::class 주입 장식
    • 코틀린의 Kclass 타입을 의미, java의 Todo.class와 같은 개념
    • Room이 컴파일 타입에 엔티티의 메타데이터을 읽어 SQLite 테이블로 매핑
    • 이 데이터베이스에 포함될 엔티티 클래스를 Room에 알려주는 역할
@Database(
	entities = [User::class, Book::class],
	version = 1,
	exporSchema = true,
	autoMigrations = [AutoMigration(from =1, to 2)]
)

abstract class AppDatabase : RoomDatabase() {
	abstract fun userDao(): UserDao
}
  • 추상 클래스선언하는 이유
    1. DAO 구현체를 Room이 컴파일 타임에 자동 생성해 연결하기 위해
    1. Room이 내부적으로 DAO 구현체를 생성
    2. Room compiler(Annotation Processor)가 생성
    3. abstract fun daoMethod(): DaoClass : DAO 반환 메서드를 추상메서드로 선언
      1. DB 인스턴스 관리(싱글톤, 생명주기, 스레드 안전성)를 Room이 대신하기 위해
    4. Room이 실제 DB 인스턴스를 생성할 때 프록시(proxy)역할
    5. 프록시 객체가 실제 SQLite 쿼리 실행, 커넥션 관리, 트랙션 등을 실행
    6. TodoRoomDB.getDatabase(context).todoDao() : ViewModel에서 호출 → DAO 구현체 생성
      1. 공통 기능(RoomDatabase가 제공하는 기능)을 상속받아 사용하기 위해
    7. DB 연결 관리(SQLiteOpenHelper 대체)
    8. clearAllTables()
    9. 트랙잭션 지원
    10. migration 처리
    11. 일일히 SQLiteOpenHelper를 상속 받아 구현할 필요 없음
속성 설명
entities DB에 포함될 @Entity 클래스 목록을 지정. (테이블 역할)
version 데이터베이스 스키마 버전. 변경 시 마이그레이션 필요.
exportSchema 스키마를 JSON 파일로 내보낼지 여부 (기본 true). 보통 테스트/버전 관리 용도로 사용.
autoMigrations Room 2.4+ 지원. 자동 마이그레이션 규칙을 정의 (@AutoMigration).
views @DatabaseView로 정의된 뷰 클래스 목록을 지정. (쿼리 결과를 읽기 전용 엔티티처럼 다룸)
  • @volatile
    • 멀티스레드 환경에서 변수의 가시설을 보장
    • 여러 스레드가 동시에 INSTANCE를 읽거나 쓸 때, 캐시를 사용하지 않고 사용하도록 보장
    • 즉 최신 정보를 가져오도록 보장
    • @Volatile private var INSTANCE : 멀티스레드에서 최신 참조 보장.
  • synchronized(this)
    • 블록 안의 코드가 한 번에 하나의 스레드만 실행되도록 보장
    • 여러 스레드가 동시에 getInstance()를 호출해도 DB
  • getInstance(context) : 싱글턴 생성 + 재사용
  • entitiesUser 클래스가 테이블로 사용됨.
  • version → 1 → 이후 컬럼 변경/추가 시 2로 올리고 마이그레이션 필요.
  • exportSchematrue/schemas 폴더에 JSON으로 스키마 저장됨.
  • views/autoMigrations는 예제에서 생략했지만, 필요 시 추가 가능.
import androidx.room.Database
import androidx.room.RoomDatabase

// 1. Entity 정의
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val name: String,
    val age: Int
)

// 2. DAO 정의
// AppDatabase.kt
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(
    entities = [User::class],
    version = 1,
    exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                )
                    // 필요 시 개발 단계에서만 사용 (스키마 변경 시 데이터 파괴적 재생성)
                    .fallbackToDestructiveMigration()
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}
left
right

C

Contents