Android Room이란? 실제 앱에서 로컬 DB를 사용하는 이유

Google Drive 메타데이터 캐시로 이해하는 Room

Android 앱에서는 서버에서 받은 데이터를 화면에 보여주는 것만으로 끝나지 않는 경우가 많다.
서버에서 가져온 데이터를 기기 내부에 저장하고, 앱을 다시 실행했을 때 저장된 데이터를 먼저 보여주거나 인터넷 연결이 없는 상황에서도 이전 데이터를 사용해야 할 수 있다. 이때 사용할 수 있는 대표적인 방법이 로컬 데이터베이스(Local Database) 다.
Android에서는 SQLite를 사용할 수 있고, Jetpack에서는 SQLite를 보다 구조적으로 사용할 수 있도록 Room이라는 라이브러리를 제공한다.


1. Android에서 Room이란?

Room은 Android에서 SQLite 기반 로컬 데이터베이스를 사용할 수 있도록 도와주는 라이브러리다.
전체적인 관계를 단순화하면 다음과 같다.

Android App
    ↓
   Room
    ↓
 SQLite
    ↓
 DB File

Room이 SQLite를 대체하는 새로운 데이터베이스인 것은 아니다.
실제 데이터는 여전히 SQLite에 저장된다.
Room은 SQLite 위에서 다음과 같은 기능을 제공한다.

SQL 검증
객체 매핑
DAO
Migration
Database 구조화

즉,

Room은 SQLite를 Android 애플리케이션에서 더 편하고 안전하게 사용할 수 있도록 도와주는 추상화 계층이다.

Android 공식 문서에서도 SQLite API를 직접 사용하는 것보다 Room을 사용하는 방식을 권장한다.
Room이 SQLite 직접 사용보다 어떤 장점을 제공하는지는 Android에서 SQLite 대신 Room을 권장하는 이유에서 자세히 살펴본다.


2. SQLite를 직접 사용하면 어떤 작업이 필요할까?

SQLite를 직접 사용해서 사용자 한 명을 조회한다고 해보자.

val cursor = db.rawQuery(
    "SELECT * FROM user WHERE id = ?",
    arrayOf(id.toString())
)

if (cursor.moveToFirst()) {
    val nameIndex = cursor.getColumnIndexOrThrow("name")
    val ageIndex = cursor.getColumnIndexOrThrow("age")

    val name = cursor.getString(nameIndex)
    val age = cursor.getInt(ageIndex)

    val user = User(
        id = id,
        name = name,
        age = age
    )
}

cursor.close()

단순히 SQL만 실행하는 것으로 끝나지 않는다.

SQL 작성
   ↓
Cursor 받기
   ↓
컬럼 위치 찾기
   ↓
타입에 맞게 값 읽기
   ↓
Kotlin 객체로 변환
   ↓
Cursor 정리

Room에서는 같은 작업을 다음처럼 표현할 수 있다.

@Dao
interface UserDao {

    @Query("SELECT * FROM user WHERE id = :id")
    suspend fun findById(id: Long): User?
}

개발자는 어떤 데이터를 조회할지 선언하고, 조회 결과를 객체로 변환하는 반복 작업은 Room이 처리한다.
하지만 Room의 필요성을 제대로 이해하려면 먼저 로컬 데이터베이스 자체가 실제 앱에서 언제 필요한지 보는 것이 좋다.


3. 실제 앱에서는 언제 로컬 데이터베이스가 필요할까?

Google Drive의 파일 목록을 보여주는 Android 앱이 있다고 가정해보자.
이 글에서는 설명을 위해 이 앱을 Ariadne라고 부르겠다.
Ariadne는 Google Drive API를 호출해 사용자의 파일 목록을 가져온다.

Google Drive

파일 A
파일 B
파일 C

가장 단순한 구현은 앱을 실행할 때마다 Google Drive API를 호출하는 것이다.

앱 실행
   ↓
Google Drive API 호출
   ↓
파일 목록 수신
   ↓
화면 표시

하지만 모든 데이터를 매번 네트워크에서 가져오면 몇 가지 문제가 생긴다.

  • 앱을 실행할 때마다 네트워크 응답을 기다려야 한다.
  • 인터넷 연결이 없으면 이전에 본 파일 목록조차 확인하기 어렵다.
  • 동일한 파일 정보를 반복해서 요청하게 된다.
  • 파일 수가 많아질수록 API 호출 횟수와 로딩 시간이 증가할 수 있다.

이런 문제를 줄이기 위해 서버에서 받은 정보 일부를 Android 기기의 로컬 데이터베이스에 저장할 수 있다.


4. 파일 자체가 아니라 Metadata를 저장한다

Ariadne에서 Google Drive 파일의 실제 내용을 데이터베이스에 저장할 필요는 없다.
대신 파일을 식별하고 화면에 표시하는 데 필요한 Metadata를 저장할 수 있다.
예를 들면 다음과 같다.

파일 ID
파일 이름
파일 타입
수정 시간
부모 폴더
동기화 상태
...

데이터 흐름은 다음과 같이 바뀐다.

Google Drive
     │
     │ Metadata 동기화
     ▼
Android Local DB
     │
     │ 저장된 데이터 조회
     ▼
앱 화면

앱을 종료했다가 다시 실행해도 로컬 DB의 데이터는 유지된다.

앱 종료
   ↓
다시 실행
   ↓
Local DB 조회
   ↓
기존 파일 목록 먼저 표시

이후 네트워크 연결이 가능하면 Google Drive와 다시 동기화하면 된다.
따라서 이런 구조에서 로컬 DB는 단순한 영구 저장소만이 아니라,

Remote 데이터의 로컬 캐시이자 앱 화면이 읽을 데이터 소스

역할을 할 수 있다.


5. Ariadne에서 Room을 사용하면

Google Drive에서 받은 파일 Metadata를 다음과 같은 Entity로 표현할 수 있다.

@Entity(tableName = "cached_files")
data class CachedFileEntity(
    @PrimaryKey
    val externalId: String,
    val name: String
)

Entity는 데이터베이스에 저장할 데이터의 형태를 정의한다.
개념적으로는 다음과 같은 테이블과 대응한다.

cached_files

externalId | name
-----------|----------------
abc123     | photo.jpg
def456     | document.pdf

저장된 파일 목록을 조회하기 위한 DAO도 정의할 수 있다.

@Dao
interface StorageCacheDao {

    @Query("SELECT * FROM cached_files")
    fun observeFiles(): Flow<List<CachedFileEntity>>
}

Room의 주요 구성요소를 단순화하면 다음과 같다.

Entity
→ 어떤 데이터를 저장할 것인가

DAO
→ 데이터를 어떻게 조회하고 수정할 것인가

Database
→ 어떤 Entity와 DAO를 하나의 DB로 구성할 것인가

6. DAO는 인터페이스인데 어떻게 실행될까?

여기서 한 가지 의문이 생긴다.
우리가 작성한 것은 다음과 같은 인터페이스뿐이다.

@Dao
interface StorageCacheDao {

    @Query("SELECT * FROM cached_files")
    fun observeFiles(): Flow<List<CachedFileEntity>>
}

observeFiles() 안에는 SQLite를 직접 호출하는 코드가 없다.
그런데 실제 앱에서는 DAO를 사용할 수 있다.

val files = storageCacheDao.observeFiles()

실제로는 다음과 같은 작업이 필요하다.

SQL 실행
   ↓
SQLite 결과 읽기
   ↓
CachedFileEntity로 변환
   ↓
Flow<List<CachedFileEntity>> 반환

그렇다면 이 코드는 누가 작성하는 걸까?
우리가 직접 구현하지 않은 DAO의 구현 코드는 Room이 빌드 과정에서 생성한다.
Room 3에서는 이 코드 생성 과정에 KSP와 Room Compiler가 사용된다.
이 과정은 Room에서 KSP가 필요한 이유: Room Compiler와 코드 생성 원리에서 자세히 살펴본다.


7. Ariadne의 전체 구조

Room을 적용하면 Ariadne의 데이터 흐름은 대략 다음과 같은 구조가 될 수 있다.

                   ┌── Remote
                   │
                   │   Google Drive API
                   │          ↓
StorageRepository ─┤     StorageClient
                   │
                   └── Local
                          ↓
                         DAO
                          ↓
                         Room
                          ↓
                        SQLite

각 계층의 책임을 나누면 다음과 같다.

StorageClient
→ Google Drive와 통신

Room / DAO
→ Local Metadata 저장 및 조회

StorageRepository
→ Remote와 Local 데이터를 조합하고 동기화

이렇게 하면 UI가 Google Drive API를 직접 호출하거나 SQLite를 직접 다룰 필요가 없다.


정리

Room과 SQLite는 경쟁 관계가 아니다.

Room
 ↓
SQLite

Room은 SQLite 위에서 동작하면서 Android 애플리케이션에서 반복적으로 필요한 기능을 제공한다.
Ariadne처럼 Remote API에서 데이터를 가져오는 앱에서는 Room을 이용해 파일 Metadata를 로컬에 저장하고, 앱을 실행했을 때 저장된 데이터를 먼저 보여주는 구조를 만들 수 있다. Room을 처음 공부할 때는 Annotation 자체를 외우기보다 다음 관계를 먼저 이해하는 것이 좋다.

Remote API
    ↓
Repository
    ↓
Local Database
    ↓
Room
    ↓
SQLite

다음 글에서는 Room이 SQLite API 직접 사용에 비해 구체적으로 어떤 장점을 제공하는지 살펴본다.


참고