Android 공식 문서는 SQLite API를 직접 사용하는 대신 Room을 사용하는 방식을 권장한다.
그렇다고 SQLite가 좋지 않은 데이터베이스라는 의미는 아니다.
Room 역시 내부적으로 SQLite를 사용한다.
Android App
↓
Room
↓
SQLite
따라서 질문은
Room을 사용할 것인가, SQLite를 사용할 것인가?
보다는
SQLite를 저수준 API로 직접 사용할 것인가, Room을 통해 사용할 것인가?
에 가깝다. 그렇다면 Room은 SQLite를 직접 사용할 때 발생하는 어떤 문제를 해결해주는 걸까?
1. 반복적인 저수준 코드를 줄인다
SQLite를 직접 사용하면 쿼리를 실행한 이후에도 직접 처리해야 하는 작업이 많다.
val cursor = db.rawQuery(
"SELECT * FROM user WHERE id = ?",
arrayOf(id.toString())
)
if (cursor.moveToFirst()) {
val name = cursor.getString(
cursor.getColumnIndexOrThrow("name")
)
val age = cursor.getInt(
cursor.getColumnIndexOrThrow("age")
)
val user = User(
id = id,
name = name,
age = age
)
}
cursor.close()
단순화하면 다음과 같다.
SQL 실행
↓
Cursor 처리
↓
컬럼 읽기
↓
타입 변환
↓
객체 생성
↓
리소스 정리
Room에서는 DAO를 통해 같은 의도를 표현할 수 있다.
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE id = :id")
suspend fun findById(id: Long): User?
}
SQL 자체는 여전히 개발자가 작성하지만, 반복적인 결과 처리와 객체 매핑은 Room이 담당한다.
2. SQL 오류를 빌드 과정에서 발견할 수 있다
Room의 중요한 장점 중 하나는 SQL 쿼리를 빌드 과정에서 검증할 수 있다는 점이다.
예를 들어 User 테이블에 다음 컬럼이 있다고 하자.
id
username
age
그런데 DAO에서 존재하지 않는 컬럼을 조회했다.
@Query("SELECT usernamee FROM User")
suspend fun getUserNames(): List<String>
SQLite API를 직접 사용하는 코드라면 이런 문제가 실제 쿼리를 실행하는 시점까지 발견되지 않을 수 있다.
코드 작성
↓
빌드 성공
↓
앱 실행
↓
해당 기능 실행
↓
SQLite 오류
Room은 Entity가 표현하는 스키마와 @Query에 작성한 SQL을 분석한다.
코드 작성
↓
빌드
↓
Room Compiler
↓
SQL 검증
↓
오류 발견
런타임에서 발견할 수 있는 데이터베이스 오류 일부를 더 이른 단계에서 발견할 수 있다는 점은 Room의 큰 장점이다.
3. DB Row를 Kotlin 객체로 매핑한다
SQLite의 조회 결과는 처음부터 Kotlin의 User 객체가 아니다.
DB에는 예를 들어 다음과 같이 데이터가 저장되어 있을 수 있다.
┌────┬────────┬─────┐
│ id │ name │ age │
├────┼────────┼─────┤
│ 1 │ 철수 │ 20 │
└────┴────────┴─────┘
SQLite를 직접 사용한다면 Cursor에서 각각의 값을 읽어 객체를 생성해야 한다.
val user = User(
id = cursor.getLong(...),
name = cursor.getString(...),
age = cursor.getInt(...)
)
Room에서는 Entity를 선언할 수 있다.
@Entity
data class User(
@PrimaryKey
val id: Long,
val name: String,
val age: Int
)
DAO에서는 객체 타입으로 직접 반환할 수 있다.
@Query("SELECT * FROM User")
suspend fun getUsers(): List<User>
개념적으로는 다음 과정이 자동화되는 것이다.
SQLite Row
↓
Room
↓
Kotlin Object
테이블과 쿼리가 많아질수록 이런 매핑 코드를 직접 작성하지 않아도 된다는 차이는 커진다.
4. DAO를 통해 DB 접근 코드를 구조화할 수 있다
Room은 데이터 접근 코드를 DAO(Data Access Object) 라는 명확한 단위로 나눌 수 있게 해준다.
@Dao
interface UserDao {
@Query("SELECT * FROM User")
suspend fun findAll(): List<User>
@Insert
suspend fun insert(user: User)
@Delete
suspend fun delete(user: User)
}
이렇게 하면 애플리케이션의 다른 코드가 Cursor나 SQLite의 세부 구현을 직접 알 필요가 줄어든다.
UI
↓
ViewModel
↓
Repository
↓
DAO
↓
Room
↓
SQLite
데이터 접근 책임이 DAO에 모이기 때문에 애플리케이션 구조도 더 명확하게 만들 수 있다.
5. Migration을 체계적으로 관리할 수 있다
앱의 데이터베이스 스키마는 시간이 지나면서 변한다. 처음에는 다음과 같은 테이블이 있었다고 하자.
User
id
name
새 버전에서는 age 컬럼이 필요해졌다.
User
id
name
age
개발 중이라면 DB를 삭제하고 다시 만들 수도 있다.
하지만 실제 사용자 기기에는 이미 기존 데이터가 존재한다.
따라서 기존 데이터를 유지하면서 스키마를 변경해야 한다.
DB Version 1
↓
Migration
↓
DB Version 2
예를 들어 다음과 같이 컬럼을 추가할 수 있다.
ALTER TABLE User
ADD COLUMN age INTEGER NOT NULL DEFAULT 0;
Room에서는 버전 사이의 Migration을 코드로 명시적으로 관리할 수 있다.
Room 버전에 따라 세부 API 형태는 달라질 수 있지만, 핵심은 동일하다.
기존 스키마에서 새로운 스키마로 어떻게 이동할 것인지 명시한다.
Room은 Migration 테스트를 위한 기능도 제공하기 때문에 앱 업데이트 과정에서 기존 사용자 데이터가 정상적으로 유지되는지 검증하기 좋다.
6. Room은 SQL을 숨기는 도구가 아니다
Room을 사용한다고 SQL을 몰라도 되는 것은 아니다.
Room에서도 SQL을 직접 작성하는 경우가 많다.
@Query(
"""
SELECT *
FROM User
WHERE age >= :age
ORDER BY name ASC
"""
)
suspend fun findAdults(
age: Int
): List<User>
Room은 SQL을 감추는 것보다,
개발자가 작성한 SQL을 Android 애플리케이션에서 더 안전하고 구조적으로 사용할 수 있도록 도와주는 계층
에 가깝다. 따라서 다음과 같은 SQL 개념은 여전히 중요하다.
SELECT
JOIN
WHERE
GROUP BY
ORDER BY
INDEX
TRANSACTION
FOREIGN KEY
Query Plan
예를 들어 조회 성능이 느리다면 결국 이런 질문을 해야 한다.
이 컬럼에 Index가 필요한가?
여러 테이블의 데이터를 합쳐야 한다면 다음을 이해해야 한다.
JOIN을 어떻게 작성해야 하는가?
여러 DB 작업을 하나의 작업 단위로 처리해야 한다면 Transaction이 필요하다.
7. Room과 SQLite의 차이를 정리하면
| 구분 | SQLite 직접 사용 | Room 사용 |
|---|---|---|
| 실제 DB | SQLite | SQLite |
| SQL 작성 | 직접 작성 | 직접 작성 |
| SQL 검증 | 주로 실행 시 발견 | 빌드 단계 검증 가능 |
| 결과 처리 | Cursor 직접 처리 | 객체 매핑 |
| DB 접근 구조 | 직접 구성 | DAO 사용 |
| 스키마 변경 | 직접 관리 | Migration 지원 |
| 저수준 코드 | 많음 | 상당 부분 감소 |
Room이 SQLite를 없애는 것은 아니다.
Room이 줄여주는 것은 SQLite 자체가 아니라,
SQLite를 Android 코드에서 직접 사용할 때 발생하는 반복 작업과 오류 가능성
이다.
8. SQLite는 공부하지 않아도 될까?
그렇지 않다.
오히려 SQLite와 SQL을 이해하고 있으면 Room을 훨씬 잘 사용할 수 있다.
Room을 사용한다고 다음 개념들이 사라지는 것은 아니다.
SELECT
JOIN
WHERE
GROUP BY
ORDER BY
INDEX
TRANSACTION
FOREIGN KEY
정규화
Query Plan
Room이 SQL을 대신 이해해주는 것이 아니라 개발자가 작성한 SQL을 더 안전하게 사용할 수 있도록 도와주는 것이기 때문이다.
9. Android 데이터베이스 공부 순서
개인적으로는 다음 순서로 공부하면 Room의 역할을 이해하기 좋다.
SQL 기본 문법
↓
SQLite 기본 개념
↓
Android에서 SQLite가 동작하는 방식
↓
Room
↓
Entity / DAO / Database
↓
Repository
↓
ViewModel
↓
Flow
↓
UI
처음부터 Room Annotation만 외우면 다음 코드가 왜 존재하는지 이해하기 어렵다.
@Entity
@Dao
@Query
@Database
반대로 SQLite를 직접 다뤄본 뒤 Room을 사용하면 Room이 어떤 반복 작업을 대신해주는지 더 명확하게 보인다.
결론
Room과 SQLite는 서로 경쟁하는 기술이 아니다.
Room
↓
SQLite
따라서
Room을 쓸까, SQLite를 쓸까?
보다는
SQLite를 직접 다룰까, Room을 통해 다룰까?
라고 질문하는 편이 더 정확하다.
Room은 SQLite 위에 다음과 같은 기능을 추가한다.
SQLite + SQL 검증 + 객체 매핑 + DAO + Migration = Room
대부분의 일반적인 Android 앱에서는 Room을 사용하는 편이 구조화와 유지보수 측면에서 유리하다.
하지만 Room 아래에서 실제 데이터를 저장하고 쿼리를 실행하는 것은 여전히 SQLite다.
Room 사용법만 외우기보다 SQL과 SQLite의 기본 개념을 함께 이해하는 것이 중요하다.
Room의 기본 구조가 처음이라면 다음 글부터 보는 것도 좋다.