안드로이드 시스템·Compose 용어 사전
데이터@Entity · @Dao · @Database

Room

SQLite 위의 타입 안전 ORM. 컴파일 타임에 쿼리를 검증한다.

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다. @UpsertonConflict = 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건이 초 단위 → 밀리초 단위로 줄어드는 차이가 난다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Room — 로컬 DB·DAO·관계·마이그레이션