Android Jetpack
안드로이드는 SQLite라는 경량화 데이터베이스를 기본적으로 제공한다. Room은 SQLite를 추상화하여 데이터베이스를 좀 더 쉽고 편리하게 사용할 수 있도록 해주는 라이브러리다.
대표적인 장점은 다음과 같다.
1. SQL 쿼리를 컴파일 시간에 검증하여 런타임 오류의 가능성을 줄여준다.
2. 보일러플레이트 코드를 줄여준다.
Entity
데이터베이스 내의 테이블을 나타내며 DB 스키마를 정의할 수 있다. 각 인스턴스는 하나의 데이터 행이다.
DAO (Data Access Object)
데이터베이스에 데이터 검색, 추가, 수정, 삭제 등의 다양한 작업을 수행하기 위한 쿼리를 보낼 수 있다. 쿼리를 보내기 위한 방법으로 CRUD(Create, Read, Update, Delete) 연산을 할 수 있는 메서드를 제공한다. SQL 쿼리를 이러한 메서드를 호출하는 것으로 간소화한다. 또한 DAO는 인터페이스나 추상 클래스로 정의되고, 구현체는 Room이 자동으로 생성하면서 컴파일 시간에 SQL 쿼리의 정확성을 검증한다.
Database
데이터베이스에 실질적으로 접근하는 진입점이다. 데이터베이스 자체를 나타내는 클래스라고 보면 된다.
데이터베이스와 연결된 데이터 항목인 entities 배열이 포함된 @Database 애너테이션이 있어야 한다.
RoomDatabase를 상속하는 추상 클래스여야 한다.
인자가 없고 DAO 클래스의 인스턴스를 반환하는 추상 메서드를 정의해야 한다.
아래 그림은 공식 문서에 있는 Room 아키텍처 다이어그램이다. 이를 통해 각 구성요소가 어떻게 동작하는지 살펴보자.
먼저 앱은 Database 인스턴스를 통해 데이터베이스에 접근한다. 그렇게 접근한 데이터베이스는 DAO를 통해 데이터를 조회하거나 조작한다. 이때 DAO 메서드는 SQL 쿼리를 추상화하며, Room은 이를 데이터베이스 작업으로 변환한다. Entity 클래스는 데이터베이스 테이블의 구조를 정의하며, DAO 메서드는 이 Entity를 사용하여 데이터를 처리한다.
1) 플러그인 추가
app 수준의 build.gradle에 KAPT 플러그인을 추가한다. KAPT는 코틀린에서 자바 애너테이션 프로세서를 사용할 수 있게 해주는 도구다. 데이터 바인딩, 의존성 주입 라이브러리(Dagger, Hilt 등), ORM 라이브러리(Room 등)와 같이 코드 생성이 필요한 라이브러리를 사용할 때 필요하다.
id("kotlin-kapt")
2) 라이브러리 의존성 추가
implementation("androidx.room:room-runtime:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")는 kapt(Kotlin Annotation Processing Tool)을 사용하여 Room 라이브러리의 컴파일러 플러그인을 프로젝트에 추가하기 위해 필요한 구문이다. Room은 데이터베이스 작업을 위한 추상 레이어를 제공하고, 이를 사용하기 위해서는 애너테이션이 필요하다. 해당 애너테이션을 room-compiler가 처리하여, 개발자가 정의한 Entity, Dao 인터페이스 등을 실제 실행 가능한 코드로 변환한다. 이 과정은 빌드 시간에 이루어지며, Room의 애너테이션을 사용한 코드에서 실제 데이터베이스 작업을 수행하는 코드를 자동으로 생성한다.
3) Entity 정의
해당 클래스 인스턴스가 테이블에서 하나의 행이라고 보면 된다. @Entity는 Room에서 제공하는 애너테이션이다. 테이블 이름을 user로 지정하고 있다. @PrimaryKey는 해당 필드를 테이블의 기본 키로 지정한다. autoGenerate = true는 기본 키 값이 자동으로 생성되도록 한다. @ColumnInfo(name = "first_name")은 테이블의 열 이름을 first_name로 지정한다. 해당 애너테이션을 사용하지 않으면 프로퍼티명(firstName)을 그대로 사용한다.
4) DAO 정의
앱이 데이터베이스에 접근하여 데이터를 관리할 수 있는 메서드를 정의한다. @Dao을 사용하여 해당 interface가 DAO인 것을 나타낸다. @Query는 SQL 쿼리를 보낼 수 있다. 인자에 SQL 문을 넣으면 된다. 데이터 삽입과 삭제 등과 같이 직관적인 애너테이션을 사용할 수도 있다.
5) Database 정의
데이터베이스를 정의하는 부분이다. @Database으로 해당 클래스가 Room 데이터베이스인 것을 나타낸다. entities에는 해당 데이터베이스에서 사용할 Entity 클래스를 배열로 할당한다. 현재는 하나뿐이다. version은 데이터베이스의 버전이고 스키마가 변경될 때 이 값을 증가시켜야 한다. userDao()는 Room에 의해 자동으로 런타임에 구현체가 생성된다.
앱이 단일 프로세스에서 실행되면 해당 클래스는 싱글톤으로 만든다. RoomDatabase 인스턴스가 리소스를 많이 쓰기 때문이다. 보통 안드로이드 앱은 단일 프로세스에서 실행된다. 하지만 만약 리소스 관리나 성능 최적화 목적으로 별도 프로세스로 분리하여 실행한다면 데이터베이스 빌더 호출 시 enableMultiInstanceInvalidation()을 포함해야 한다. 이 부분 다음 단계에서 추가로 살펴본다.
6) Database 생성 및 사용
데이터베이스 인스턴스를 만든다. 앞서 언급했던 enableMultiInstanceInvalidation()을 여기서 사용할 수 있다. Room.databaseBuilder(...).enableMultiInstanceInvalidation().build()와 같은 형태다. databaseBuilder 첫 번째 인자로 데이터베이스가 앱 생명주기를 따르도록 applicationContext를 넘겨주었다. 두 번째 인자로 앞서 정의했던 데이터베이스 클래스를 넘겨준다. 세 번째 인자는 생성될 데이터베이스 파일명이다.
안드로이드는 메인 스레드에서 데이터베이스에 접근할 수 없다. 따라서 별도의 워커 스레드에서 실행해야 한다. DAO에서 정의했던 메서드를 호출함으로써 데이터를 삽입하고 가져오는 작업을 간단하게 실행한다.
만약 코루틴을 사용하고 싶다면 추가적으로 필요한 라이브러리 의존성이 있다.
implementation("androidx.room:room-ktx:2.6.1")
그 후 UserDao 인터페이스의 메서드를 아래와 같이 suspend 함수로 변경하면 된다.
이제 코루틴을 사용해서 호출할 수 있다.
Dispatchers.IO를 명시적으로 지정하지 않아도 된다. 왜냐하면 Room이 코루틴을 지원하여 자동적으로 백그라운에서 동작하도록 하기 때문이다. 코틀린 코루틴의 일반적인 동작 방식과는 별개다.