Room은 Android Jetpack의 ORM 라이브러리로, Android에서 로컬 데이터베이스 작업을 위해 SQLite 위에 추상화 계층을 제공합니다. Android Developers, 2025의 공식 문서에 따르면, Room은 컴파일 타임에 어노테이션을 기반으로 DAO 구현을 자동 생성하여 SQLiteOpenHelper를 직접 사용하는 것과 비교해 약 70%의 보일러플레이트 코드를 제거합니다. 이 라이브러리는 컴파일 단계에서 SQL 쿼리 유효성 검사를 수행하여 장치에서 애플리케이션을 실행하기 전에 구문 오류를 발견할 수 있게 합니다.
핵심 요점
Room은 Android 플랫폼에서 로컬 SQLite 데이터베이스 작업을 단순화하기 위해 Google이 만든 Android Jetpack의 ORM 라이브러리입니다. 데이터 스키마를 설명하기 위한 어노테이션을 제공하고 컴파일 타임에 DAO 인터페이스 구현을 자동 생성합니다. SQLiteOpenHelper를 직접 사용하는 것과 달리, Room은 개발자를 데이터베이스 연결 생성, 열기 및 관리에 필요한 상당한 양의 보일러플레이트 코드 작성에서 해방시킵니다.
이 라이브러리는 Google I/O 2017에서 Android 아키텍처 구성 요소의 일부로 발표되었습니다. 그 이후로 Room은 로컬 데이터 저장의 사실상 표준이 되었으며, Android용 GreenDAO 및 Realm과 같은 솔루션을 인기에서 앞질렀습니다. Google에 따르면, 이 라이브러리는 기기에서 로컬 데이터로 작업하는 Google Play에 게시된 애플리케이션의 60% 이상에서 사용됩니다.
핵심 기능은 어노테이션 프로세서를 사용한 컴파일 타임 SQL 쿼리 검증입니다. 개발자가 SQL 명령에서 오류를 범한 경우, 예를 들어 존재하지 않는 열 이름을 지정한 경우, 애플리케이션이 설치되기 전에 빌드가 오류와 함께 실패합니다. 이는 SQLiteOpenHelper 접근 방식과 근본적으로 다릅니다. SQLiteOpenHelper에서는 이러한 오류가 런타임에만, 종종 프로덕션 환경에서 발견됩니다.
SQLite는 TEXT, INTEGER, REAL, BLOB 및 NULL의 다섯 가지 데이터 유형만 지원합니다. 그러나 Java와 Kotlin에서는 Date, List, Enum 및 사용자 정의 객체와 같은 복잡한 유형이 사용됩니다. 이를 저장하기 위해 Room은 TypeConverters 메커니즘을 제공합니다. 이는 복잡한 유형을 SQLite가 이해할 수 있는 기본 유형으로 변환하는 정적 메서드입니다. 예를 들어, Date 객체는 Long(타임스탬프)으로, List<String>은 Gson 또는 Moshi를 통해 JSON 문자열로 변환됩니다.
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
val db = Room
.databaseBuilder(context, AppDatabase::class.java, "app-db")
.build()
컨버터를 선언하려면 정적 메서드에 @TypeConverter 어노테이션을 추가하고 데이터베이스 수준에서 @TypeConverters 어노테이션에 컨버터 클래스를 지정하기만 하면 됩니다. Room은 각 SQL 쿼리에서 해당 유형을 읽고 쓸 때 자동으로 컨버터를 적용하므로 변환 메서드를 수동으로 호출할 필요가 없습니다.
Room은 Entity, DAO 및 Database의 세 가지 주요 구성 요소로 구성됩니다. 각 구성 요소는 엄격하게 정의된 역할을 수행하며 해당 어노테이션으로 주석 처리됩니다. 함께, 이들은 애플리케이션의 비즈니스 로직을 SQLite 구현 세부 사항으로부터 격리하는 완전한 데이터 액세스 계층을 형성합니다.
Entity는 데이터베이스에서 하나의 테이블 구조를 설명하는 데이터 클래스입니다. 클래스의 각 필드는 테이블 열에 해당하며, 데이터베이스의 각 행은 클래스의 한 인스턴스에 해당합니다. @Entity 어노테이션은 Room에 해당 클래스가 테이블임을 알립니다. @PrimaryKey 어노테이션이 있는 필드는 기본 키를 정의하며, 자동 증가 또는 복합 키가 될 수 있습니다. @ForeignKey는 테이블 간의 관계에 사용되어 데이터베이스 수준에서 데이터 무결성을 보장합니다.
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "full_name")
val name: String,
val age: Int,
val email: String
)
DAO(Data Access Object)는 데이터 작업(삽입, 읽기, 업데이트 및 삭제)을 선언하는 인터페이스 또는 추상 클래스입니다. 각 작업은 @Insert, @Query, @Update 또는 @Delete로 주석 처리됩니다. Room은 컴파일 타임에 이 인터페이스의 구현을 자동 생성합니다. @Query 어노테이션은 특히 가치가 있습니다. SQL 쿼리를 문자열로 받아들이고 빌드 타임에 그 정확성을 검증합니다.
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User): Long
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
@Delete
suspend fun delete(user: User)
}
Database는 RoomDatabase를 확장하는 추상 클래스로, 데이터베이스의 진입점 역할을 합니다. 모든 Entities 목록을 포함하고 DAO를 얻기 위한 추상 메서드를 제공합니다. 클래스는 @Database로 주석 처리되며, 스키마 버전과 엔터티 목록을 지정합니다. 데이터베이스 인스턴스는 애플리케이션 컨텍스트, 파일 이름 및 Database 클래스를 사용하여 Room.databaseBuilder를 통해 생성됩니다.
Room은 SQLite를 대체하는 것이 아니라 추상화 계층으로 그 위에서 작동합니다. 내부 아키텍처에는 어노테이션 프로세서, 코드 생성기 및 연결 풀이 포함됩니다. 컴파일 타임에 어노테이션 프로세서는 Entity, DAO 및 Database 클래스를 분석한 다음 _Impl 접미사가 있는 구현 클래스를 생성합니다. 생성된 모든 클래스는 빌드 패키지에 배치되며 개발자에게 직접 표시되지 않습니다.
컴파일 타임 코드 생성은 Room의 핵심 메커니즘입니다. 각 DAO 인터페이스에 대해 주석 처리된 모든 메서드의 전체 구현이 포함된 클래스가 생성됩니다. @Query 어노테이션의 SQL 쿼리는 정확성에 대해 검증됩니다. 프로세서는 열 이름을 Entity 필드와 일치시키고 SQL 구문을 확인합니다. 오류가 발견되면 빌드가 명확한 메시지와 함께 중단됩니다. 이는 오류가 런타임에만 나타나는 원시 SQLiteOpenHelper를 사용할 때는 불가능합니다.
생성 프로세스는 세 단계로 구성됩니다. 첫 번째 — 스키마 검증: 프로세서는 @Database에 나열된 모든 클래스가 유효한 Entity인지 확인합니다. 두 번째 — DAO 본문 생성: 각 메서드에 대해 준비된 쿼리를 실행하는 내부 RoomSQLiteQuery 객체를 사용한 구현이 생성됩니다. 세 번째 — Database_Impl 클래스 생성: 데이터베이스 생성 및 열기와 모든 DAO 객체 초기화를 처리합니다.
class UserDao_Impl(private val __db: RoomDatabase) : UserDao {
private val __insertionAdapter = __db
.createInsertionAdapter(User::class, 0)
override suspend fun insert(user: User): Long {
__db.assertNotSuspendingTransaction()
return __db.runInTransaction {
__insertionAdapter.insertAndReturnId(user)
}
}
}
Room은 데이터베이스 작업을 위한 별도의 스레드 풀을 생성하지 않습니다. 기본적으로 쿼리는 한 가지 제한과 함께 호출 스레드에서 실행됩니다. 읽기와 쓰기는 스레드를 차단합니다. 비동기 작업을 위해 Room은 Kotlin 코루틴과 suspend 함수를 통해, LiveData와 반환 값을 통해, Flow와 반응형 래퍼를 통해 통합됩니다. 이는 개발자에게 특정 작업에 적합한 아키텍처 솔루션을 선택할 수 있는 유연성을 제공합니다.
Room을 사용하여 메모 저장 애플리케이션을 만드는 실제 예제를 살펴보겠습니다. 이 애플리케이션에는 id, title, content 및 timestamp 필드가 있는 하나의 Note 테이블이 포함되어 있습니다. 사용자는 메모를 추가, 보기 및 삭제할 수 있습니다. 비동기 작업에는 코루틴이 사용됩니다.
Android 프로젝트에 Room을 통합하려면 모듈 수준의 build.gradle 파일에 종속성을 추가합니다. Room에는 세 가지 구성 요소가 필요합니다: 런타임 라이브러리, kapt 어노테이션 프로세서 및 선택적 코루틴 지원. 라이브러리 버전은 손쉬운 업데이트를 위해 room_version 변수에 지정됩니다. Room 2.4.0부터 더 빠른 빌드 속도를 제공하는 kapt의 대안으로 KSP가 지원됩니다.
dependencies {
def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// 선택 사항: 테스트
testImplementation "androidx.room:room-testing:$room_version"
}
종속성을 설정한 후 Note Entity, NoteDao 인터페이스 및 AppDatabase 클래스의 세 가지 파일을 만듭니다. Note Entity에는 @PrimaryKey 및 @ColumnInfo 어노테이션이 있는 필드가 포함됩니다. DAO는 삽입, 목록 가져오기 및 삭제를 위한 메서드를 제공합니다. Database는 @Database 어노테이션을 통해 Entity와 DAO를 연결합니다.
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val content: String,
@ColumnInfo(name = "created_at")
val timestamp: Long = System.currentTimeMillis()
)
@Dao
interface NoteDao {
@Insert
suspend fun insert(note: Note)
@Query("SELECT * FROM notes ORDER BY created_at DESC")
fun getAllNotes(): Flow<List<Note>>
@Delete
suspend fun delete(note: Note)
}
AppDatabase 파일은 RoomDatabase를 확장하는 추상 클래스로 선언됩니다. @Database 어노테이션은 현재 버전의 모든 Entities와 스키마 버전 번호를 지정합니다. 인스턴스를 얻기 위해 애플리케이션 컨텍스트를 사용하여 Room.databaseBuilder의 build 메서드를 통해 싱글톤 패턴이 사용됩니다. 데이터베이스 인스턴스를 캐싱하면 메모리 누수를 일으킬 수 있는 여러 생성을 방지합니다.
마이그레이션은 Room에서 기존 데이터를 잃지 않고 애플리케이션을 업데이트할 때 데이터베이스 스키마를 변경하기 위한 메커니즘입니다. 사용자가 수정된 Entities가 포함된 새 버전을 설치하면 Room은 버전 불일치를 감지하고 지정된 마이그레이션 단계를 실행합니다. 마이그레이션이 없으면 데이터베이스가 삭제되고 다시 생성되어 사용자가 저장한 모든 데이터가 손실됩니다.
마이그레이션은 Migration 클래스로 설명되며, 데이터베이스의 시작 및 끝 버전을 받습니다. migrate 메서드 내에서 스키마를 변경하기 위해 ALTER TABLE 또는 CREATE TABLE SQL 쿼리가 실행됩니다. Room은 스키마 변경을 자동으로 감지할 수 없습니다. 개발자는 각 Entity 변경에 대해 수동으로 마이그레이션을 작성해야 합니다. Room 2.4.0부터 자동 마이그레이션 생성을 위한 실험적 autoMigrations 기능을 사용할 수 있습니다.
autoMigrations 기능을 사용하면 Room이 Entity 버전 간의 차이를 기반으로 마이그레이션을 자동 생성할 수 있습니다. 사용하려면 @Database에 @AutoMigration 어노테이션을 추가하고 JSON으로 스키마 내보내기를 활성화하기만 하면 됩니다. Room은 인접 버전의 스키마를 비교하고 필요한 ALTER 쿼리를 생성합니다. 그러나 autoMigrations는 하위 호환되는 변경만 지원합니다: 열 추가, 인덱스 생성 및 호환 가능한 변환으로 유형 변경.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE users ADD COLUMN phone TEXT"
)
}
}
val db = Room
.databaseBuilder(context, AppDatabase::class.java, "app-db")
.addMigrations(MIGRATION_1_2)
.build()
열 이름 변경이나 테이블 병합과 같은 복잡한 변경에는 중간 테이블을 사용한 수동 마이그레이션이 필요합니다. 일반적인 시나리오: 이전 스키마로 임시 테이블을 만들고, 변환을 적용하여 이전 테이블에서 새 테이블로 데이터를 복사하고, 이전 테이블을 삭제하고 임시 테이블의 이름을 변경합니다. Room은 모든 마이그레이션이 단일 트랜잭션에서 실행되도록 보장하며, 오류가 발생하면 변경 사항이 완전히 롤백됩니다.
자주 묻는 질문
Room은 어노테이션과 컴파일 타임 SQL 검증을 통해 ORM 추상화를 제공하는 반면, SQLiteOpenHelper는 모든 쿼리를 수동으로 작성하고 연결을 관리해야 합니다. Room은 CRUD 작업을 위한 코드를 자동 생성하고 LiveData 및 Flow를 포함한 Android 아키텍처 구성 요소와 통합됩니다.
Room은 모든 Java 기본 유형(Int, Long, Boolean, Float, Double)과 String, ByteArray 및 Date를 지원합니다. List 또는 Enum과 같은 복잡한 유형의 경우 TypeConverters가 사용됩니다. 이는 비표준 유형을 SQLite 호환 형식으로 변환하는 정적 변환 메서드입니다.
네, Room은 코루틴 없이 동기 호출을 지원하지만, 실행되는 스레드를 차단합니다. 비동기 작업의 경우 코루틴 대신 LiveData 또는 RxJava를 사용할 수 있습니다. Google은 새 프로젝트에서 비동기 데이터 액세스의 기본 방법으로 코루틴을 사용할 것을 권장합니다.
Room이 데이터베이스 버전 불일치를 감지하고 적절한 마이그레이션을 찾지 못하면, 기본적으로 오류 설명과 함께 IllegalStateException이 발생합니다. 개발자는 fallbackToDestructiveMigration 메서드로 이 동작을 재정의할 수 있으며, 이는 기존 데이터베이스를 삭제하고 새 데이터베이스를 생성하여 모든 데이터가 손실됩니다.
Room은 @Embedded 어노테이션을 사용한 중첩 객체와 @Relation 어노테이션을 사용한 관계 클래스를 통해 관계를 지원합니다. 테이블 조인이 포함된 복잡한 쿼리의 경우 사용자 정의 POJO 클래스가 사용되며, 해당 필드는 SQL JOIN 문과 함께 @Query의 결과로 채워집니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.