Room — 로컬 DB·DAO·관계·마이그레이션
Room은 안드로이드의 공식 로컬 데이터베이스 라이브러리다 — SQLite 위에 얇은 객체 매핑을 씌워, 컴파일 타임에 SQL을 검증하고 객체↔테이블 매핑·관찰 가능한 쿼리(Flow)·코루틴을 깔끔하게 제공한다. [andarch 04]의 오프라인 우선에서 *DB를 단일 출처(SSOT)*로 둘 때 그 DB가 보통 Room이다. (출처: Android — Save data with Room.)
세 조각 — Entity·DAO·Database
@Entity(tableName = "member")
data class MemberEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
@ColumnInfo(name = "created_at") val createdAt: Long,
)
@Dao
interface MemberDao {
@Query("SELECT * FROM member ORDER BY created_at DESC")
fun observeAll(): Flow<List<MemberEntity>> // 관찰 가능 — 데이터 바뀌면 자동 방출
@Query("SELECT * FROM member WHERE id = :id")
suspend fun findById(id: Long): MemberEntity? // suspend — 백그라운드에서
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertAll(items: List<MemberEntity>)
@Delete suspend fun delete(item: MemberEntity)
}
@Database(entities = [MemberEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun memberDao(): MemberDao
}
@Entity— 테이블 한 개 = 클래스 한 개.@PrimaryKey·@ColumnInfo·인덱스(indices).@Dao— 쿼리 메서드.@Query(컴파일 타임에 SQL·컬럼 검증!)·@Insert/@Update/@Delete.@Database— Entity·버전을 묶고 DAO를 노출.
관찰 가능 쿼리와 suspend — 코루틴/Flow 통합
Room의 강점은 코루틴·Flow 네이티브 통합이다.
Flow<...>반환 쿼리는 관찰 가능 — 그 테이블이 바뀌면 자동으로 새 결과를 방출한다. 그래서 오프라인 우선에서 DB를 구독하면 변경이 UI까지 자동으로 흐른다([andarch 04]).suspend함수는 백그라운드 스레드에서 안전하게 실행(메인 스레드 DB 접근은 금지 — ANR).
관계 — @Relation·@Embedded·외래키
여러 테이블의 관계는 *@Relation*으로 한 번에 조회한다(1:N·N:M). *@Embedded*는 객체를 같은 테이블 칼럼들로 펼치고, *@ForeignKey*로 참조 무결성을 건다.
data class MemberWithOrders(
@Embedded val member: MemberEntity,
@Relation(parentColumn = "id", entityColumn = "member_id")
val orders: List<OrderEntity> // Room이 두 쿼리로 묶어줌
)
@Transaction @Query("SELECT * FROM member")
fun observeMembersWithOrders(): Flow<List<MemberWithOrders>>
마이그레이션 — 스키마는 진화한다
앱이 업데이트되며 스키마가 바뀌면 version을 올리고 마이그레이션을 제공해야 한다 — 기존 사용자의 데이터를 깨지 않고 변환. 안 하면 IllegalStateException(또는 데이터 손실).
val MIGRATION_1_2 = Migration(1, 2) { db ->
db.execSQL("ALTER TABLE member ADD COLUMN email TEXT")
}
Room.databaseBuilder(ctx, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2)
.build()
단순 변경은 **자동 마이그레이션(@AutoMigration)**으로 처리하고, 복잡한 변환은 수동 Migration을 쓴다. *fallbackToDestructiveMigration(테이블 날리고 재생성)*은 개발 중에만 — 운영에서 쓰면 사용자 데이터가 사라진다.
TypeConverter — 비기본 타입 저장
SQLite가 모르는 타입(Date·enum·List)은 **@TypeConverter**로 기본 타입(Long·String)으로 변환해 저장한다.
class Converters {
@TypeConverter fun fromInstant(v: Instant?): Long? = v?.toEpochMilli()
@TypeConverter fun toInstant(v: Long?): Instant? = v?.let(Instant::ofEpochMilli)
}
정리 — 타입 안전한 로컬 SSOT
Room의 핵심 — @Entity/@Dao/@Database 세 조각, @Query를 컴파일 타임에 검증, Flow 반환으로 관찰 가능(변경 자동 방출)·suspend로 백그라운드, @Relation으로 관계 조회, 버전 올릴 땐 마이그레이션 필수(데이터 보존), 비기본 타입은 @TypeConverter. 오프라인 우선에서 Room을 SSOT로, Flow로 구독하면 데이터 변경이 화면까지 단방향으로 흐른다.
한 줄 요약 — Room=SQLite 위 객체 매핑(컴파일 타임 SQL 검증).
@Entity(테이블)·@Dao(@Query/@Insert...)·@Database(version).Flow반환=관찰 가능(자동 방출)·suspend=백그라운드. 관계는@Relation/@Embedded/@ForeignKey. 스키마 변경은 마이그레이션 필수(데이터 보존), 비기본 타입은@TypeConverter.
(출처: Android — Room / Migrating database / Relationships / Room with Flow & coroutines.)