Android에서 ROOM이란 무엇일까?
Room은 쉽게말해서 AAC(Android Architecture Components ), 스마트폰 내장 DB에 데이터를 저장하기 위해 사용하는 라이브러리이다.
Room은 ORM(Object Relational Mapping)라이브러리로서 DB 데이터를 Java 또는 코틀린 객체로 매핑해준다.
Room은 SQLite를 내부적으로 사용하고 있지만, DB를 구조적으로 분리하여 데이터 접근의 편의성을 높여주고 유지보수에 편리하다.
또한, 다양한 Annotation을 통해 컴파일시 코드들을 자동으로 만들어주며 LiveData, RxJava와 같은 Observation 형태를 지원하고 MVP, MVVM과 같은 아키텍쳐 패턴에 쉽게 활용할 수 있도록 되어있다.
SQLite vs ROOM
- SQLite는 쿼리에 대한 에러를 컴파일 타임에 확인할 수 없지만, ROOM은 컴파일 타임에 SQL에 대한 유효성 검사를 할 수 있다.
- SQLite는 schema가 변경될 경우, SQL 쿼리를 수동으로 업데이트 해야하지만, ROOM은 쉽게 해결할 수 있다.
- SQLite는 Java 데이터 객체를 변경하기 위해 많은 boilerplate code를 사용해야 하지만, ROOM은 boilerplate code없이 ORM 라이브러리를 통해 매핑 가능하다. (boilerplate code: 수정하지 않거나, 최소한의 수정만을 거쳐 여러 곳에 필수적으로 사용되는 코드)
- ROOM은 LiveData와 RxJava를 위한 Observation으로 생성하여 동작할 수 있지만, SQLite는 할 수 없다.
Room 구조 & Annotation
ROOM은 아래와 같이 3가지 구성 요소(Database, Entity, Dao)가 있다.
Database
: 데이터베이스의 전체적인 소유자 역할, DB 생성 및 버전 관리.
RecyclerView의 어댑터 같은 느낌으로 Entity만큼 정의된 Dao 객체들을 반환할 수 있는 함수들을 가지고 있는 추상 클래스 형태로 정의.
RooM DB에서 DAO를 가져와서 객체를 통해 데이터를 CRUD함.
: class가 Database임을 알려주는 어노테이션.
Annotation
@Database
: class가 Database임을 알려주는 어노테이션.
- entities
: 이 DB에 어떤 테이블들이 있는 지 명시. - version
: Scheme가 바뀔 때 이 version도 바뀌어야 함. - exportSchema
: Room의 Schema 구조를 폴더로 Export 할 수 있음. 데이터베이스의 버전 히스토리를 기록할 수 있다는 점에서 true로 설정하는 것이 좋다.
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
// 위의 파일 생성 후, 아래 코드로 DB instance 가져올 수 있다.
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "database-name").build();
Entity
: DB 내의 Table, 즉 DB에 저장할 데이터 형식으로 class의 변수들이 컬럼(column)이 되어 table이 된다.
Annotation
@Entity(tableName = StudentEntry.TABLE_NAME)
: Table 이름을 선언한다. (기본적으로 entity class 이름을 database table 이름으로 인식)
@PrimaryKey
: 각 entity 는 1개의 primary key 를 가져야 한다.
@ColumnInfo
: Table 내 column 을 변수와 매칭
@Entity
public class User {
@PrimaryKey
public int uid;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
}
Dao
: 데이터베이스에 접근하여 수행할 작업을 메소드 형태로 정의 (SQL 쿼리 지정 가능)
Annotation
@insert
: Entity set 삽입. @Entity로 정의된 class만 인자로 받거나, 그 class의 collection 또는 array 만 인자로 받을 수 있다.
인자가 하나인 경우 long type 의 return (insert 된 row Id)을 받을 수 있고, 여러 개인 경우 long[], List 을 받을 수 있다.
"onConflict = OnConflictStrategy.REPLACE" option 으로 update 와 동일한 기능을 할 수 있다.
@update
: Entity set 업데이트. Return 값으로 업데이트된 행 수를 받을 수 있다.
@delete
: Entity set 삭제. Return 값으로 삭제된 행 수를 받을 수 있다.
@query
: @Query를 사용하여 DB를 조회할 수 있다.
Compile time 에 return 되는 object 의 field 와 sql 결과로 나오는 column 의 이름이 맞는지 확인하여 일부가 match되면 warning, match 되는게 없다면 error를 보낸다.
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
Room 사용법
1. Gradle 설정
dependencies {
...
def room_version = "2.2.5"
// Room components
implementation "androidx.room:room-runtime:2.2.5"
annotationProcessor "androidx.room:room-compiler:2.2.5"
androidTestImplementation "androidx.room:room-testing:2.2.5"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
...
}
장황하게 gradle을 늘어놓았지만 코틀린 코드를 사용할 것이 아니라면 아래 코드만 설정해도 무방하다.
implementation 'androidx.room:room-runtime:2.2.6'
annotationProcessor 'androidx.room:room-compiler:2.2.6'
2. Entity Class 생성
Room에서는 class로 Table을 명시할 수 있다. 따라서 데이터 모델인 Entity를 만들어야 한다.
Annotation으로 Entity를 추가해서 class를 만든 후, 해당 class에 들어갈 Entity들을 정의해준다.
여기서 각 Entity는 PrimaryKey가 반드시 필요한데, autoGenerate=true를 하면 키가 자동으로 생성된다.
나는 지금 당이 필요하므로 먹고 싶은 dessert class를 만들어보겠다.
@Entity
public class Dessert {
@PrimaryKey(autoGenerate = true)
public int id;
@ColumnInfo(name="dessertName")
public String dessertName;
@ColumnInfo(name="origin")
public String origin;
@ColumnInfo(name="amount")
public long amount;
- @Ignore 를 이용하면 해당 필드는 들어가지않는다.
- Entity 어노테이션에서 foreinKey인자를 통해 테이블간 관계 설정 가능
- Relation 어노테이션을통해 선언된 테이블 관계에서 매칭되는 외래키를 설정해줄 수 있습니다.
3. DAO 생성
DB에 접근해 query, insert, delete등을 수행할 메소드를 포함하여 만든다.
@Dao
public interface DessertDao {
@Query("SELECT * FROM Dessert")
List<Dessert> getAll();
@Query("SELECT * FROM Dessert WHERE id IN (:dessertIds)")
List<Dessert> loadAllByIds(int[] dessertIds);
@Insert
void insertAll(Dessert... desserts);
@Delete
void delete(Dessert dessert);
}
일반적으로 위처럼 생성할 수 있다.
그런데 리턴값을 RxJava에서 사용할 수 있는 Observe 형태로도 리턴할수도 있다.
하지만 Rx를 사용할 경우, 오탐 에러를 피하기위해 Observable형태로 데이터로 리턴받기보다는 되도록 Flowable, Completable, Single을 사용하는게 좋다.
4. Database
데이터베이스의 정의는 @Database로 Database 클래스임을 명시하며, RoomDatabase을 상속받아 abstract로 선언되어야 하며 abstract의 Dao를 가지고 있다.
Room 객체는 많은 리소스를 소모하기 때문에 Singleton 패턴으로 정의해야 한다.
@Database(entities = {Dessert.class}, version = 1)
public abstract class DessertDB extends RoomDatabase {
private static DessertDB INSTANCE = null;
public abstract DessertDao dessertDao();
public static DessertDB getInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
DessertDB.class, "dessert.db").build();
}
return INSTANCE;
}
public static void destroyInstance() {
INSTANCE = null;
}
}
기본적인 삽입/수정/삭제 외에 다른 기능을 가진 메서드를 만들고 싶다면 @Query 어노테이션을 붙이고 그 안에 어떤 동작을 할 건지 sql 문법으로 작성을 해주면 된다.
5. Activity에서 Room접근(사용)
private DessertDB dessertDB = null;
// onCreate()
// DB 생성
dessertDB = DessertDB.getInstance(this);
// main thread에서 DB 접근 불가하므로 data를 읽고 사용할 thread 사용 필요
class InsertRunnable implements Runnable {
@Override
public void run(){
}
}
InsertRunnable insertRunnble = new InsertRunnable();
Thread t = new Thread(insertRunnable);
t.start();
// 버튼 사용하여 실행할 경우
InsertRunnable insert = new InsertRunnable();
Thread addThread = new Thread(insert);
addThread.start(); // 삽입
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
startActivity(intent);
finish();
// DB 닫기
@Override
protected void onDestroy() {
super.onDestroy();
DessertDB.destroyInstance();
}
참조