SQLite 위의 타입 안전 ORM. 세 조각으로 이뤄진다.
@Entity(tableName = "users", indices = [Index("email", unique = true)])
data class UserEntity(
@PrimaryKey val id: Long,
val name: String,
val email: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY name")
fun observeAll(): Flow<List<UserEntity>> // 변경 시 자동 재방출
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getById(id: Long): UserEntity?
@Upsert suspend fun upsertAll(users: List<UserEntity>)
@Delete suspend fun delete(user: UserEntity)
}
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
컴파일 타임 검증이 핵심 가치
@Query 문자열을 빌드 시점에 파싱한다
- 테이블/컬럼 오타 → 빌드 실패
- 반환 타입이 안 맞으면 → 빌드 실패
- :id 파라미터가 없으면 → 빌드 실패
생 SQLite 라면 이 전부가 런타임 크래시다
Flow 반환이 SSOT를 만든다
Flow<List<UserEntity>> 를 반환하면
- 그 테이블이 바뀔 때마다 Room 이 알아서 다시 방출한다
- 어디서 INSERT 했든 화면이 자동으로 갱신된다
- 오프라인 우선 구조의 토대
무효화는 테이블 단위다 (행 단위가 아니다)
- 큰 테이블에 잦은 쓰기가 있으면 과도한 재조회가 생길 수 있다
suspend가 강제된다
메인 스레드에서 부르면
- IllegalStateException: Cannot access database on the main thread
(allowMainThreadQueries() 로 끌 수 있지만 테스트 외에는 쓰지 않는다)
트랜잭션
@Transaction
suspend fun replaceAll(users: List<UserEntity>) {
deleteAll()
upsertAll(users)
}
@Transaction 없이 두 개를 나눠 부르면
- 중간에 화면이 조회해 '빈 목록' 을 보게 된다 (깜빡임) @Relation 을 쓰는 쿼리에도 @Transaction 이 필요하다 (여러 쿼리로 쪼개지므로)
면접 함정
- ❌ "Room이 스레드를 알아서 옮겨 준다" →
suspend/Flow면 그렇다. 동기 함수는 직접 IO로 옮긴다. - ❌ "@Insert가 중복을 알아서 갱신한다" → 기본은 ABORT다.
@Upsert나onConflict = REPLACE를 쓴다.
인덱스가 없으면 느려진다
@Entity(
tableName = "posts",
indices = [Index("userId"), Index(value = ["userId", "createdAt"])]
)
data class PostEntity(...)
Room 은 인덱스를 자동으로 만들지 않는다 (PrimaryKey 제외) WHERE · ORDER BY · JOIN 에 쓰는 컬럼에 직접 건다
확인하는 법
- @Query("EXPLAIN QUERY PLAN SELECT ...")
- "SCAN posts" 가 보이면 인덱스를 안 타는 것이다
Paging 3 연동
@Query("SELECT * FROM posts ORDER BY createdAt DESC")
fun pagingSource(): PagingSource<Int, PostEntity>
Room 이 PagingSource 를 직접 생성해 준다 데이터가 바뀌면 무효화 신호를 보내 Paging 이 알아서 다시 로드한다
대량 삽입
// ❌ 1,000번의 개별 트랜잭션 — 매우 느리다
users.forEach { dao.insert(it) }
// ✅ 한 트랜잭션
@Insert suspend fun insertAll(users: List<UserEntity>)
SQLite 는 트랜잭션마다 디스크 동기화를 한다 1,000건이 초 단위 → 밀리초 단위로 줄어드는 차이가 난다