[AAC] Room 입문

개요

Lifecyle입문편에 이어 가벼운(?) ORM인 Room의 입문편을 다룹니다.
입문편에서는 최대한 간결하게 Room을 이해하고 기초적인 사용법을 익히는데 목표를 두고 있습니다.
Room은 AAC에 포함된 ORM으로 최대한 추상적인 구조를 지양하고 최대한 날SQL에 가까운 느낌으로 쓸 수 있게 제작되어있습니다. 단지 Room사용 시 비동기를 사용하면 LiveData와 연결되는지라 이 글에서는 최대한 심플한 예제로 설명하겠습니다.

하나의 테이블을 나타내는 엔티티작성

Room에서 Entity는 하나의 테이블 스키마를 의미하는 추상객체이자 레코드의 인스턴스이기도 합니다. 종합적인 설명은 여기에 있습니다. 간단히 DB에 Items이라는 테이블을 하나 정의한다고 생각해보죠.

create table Items(
    id int NOT NULL,
    name varchar(255) NOT NULL,
    description varchar(255),
    price int,
    PRIMARY KEY (id)
)

이걸 그대로 엔티티로 바꾸는 방법은 아래와 같습니다.

@Entity(tableName = "Items")
public class ItemEntity{
    @PrimaryKey
    public int id;
    public String name;
    public String description;
    public int price;
}

애노태이션만 외운다면 굉장히 직관적이고 쉽습니다. 엔티티의 애노태이션은 다음과 같은 기능을 전부 제공합니다.

  1. 복합키 – @Entity(primaryKeys = {“firstName”, “lastName”})
  2. 객체 필드명과 실제 테이블 필드명이 다를 때의 처리 – @ColumnInfo(name = “first_name”)String firstName
  3. 무시할 객체의 필드 – @Ignore Bitmap picture
  4. 인덱스설정 – @Entity(indices = {@Index(“name”), @Index(value = {“last_name”, “address”})})
  5. 외래키 설정 – @Entity(foreignKeys = @ForeignKey(entity = User.class, parentColumns = “id”, childColumns = “user_id”))
  6. 중첩엔티티로 표현하기 – @Embedded OtherEntity data

사실 SQLLite로 할 수 있는 거의 모든 기능을 표현할 수 있습니다. Room에서는 우선 엔티티를 만들어야 합니다.

쿼리를 의미하는 DAO

테이블을 만들었으니 이제 이 테이블에 질의를 해야겠죠. 이러한 질의를 하는 메소드를 소유한 객체를 DAO(다오)라고 합니다. 엔티티처럼 애노태이션으로 정의합니다.
위에서 만든 Items에 질의하는 DAO를 하나 정의해보죠.

@Dao
public interface ItemDao{
    @Query("SELECT * FROM items")
    LiveData<List<ItemEntity>> loadAll();

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertAll(List<ItemEntity> items);

    @Query("select * from items where id = :id")
    LiveData<ItemEntity> load(int id);

    @Query("select * from items where id = :id")
    ItemEntity itemById(int id);
}

몇 가지 파악하기 쉬운 특성이 보입니다.

  1. @Dao – 이 애노태이션의 선언으로 ItemDao인터페이스는 Dao타입이 됩니다
  2. interface – 구상클래스가 아니라 인터페이스로 선언합니다.
  3. @Query – 실제 쿼리를 작성합니다. 만약 쿼리에 들어갈 인자가 있다면 이는 메소드의 인자와 일치시키는 식으로 작성합니다.
  4. List<?> – 여러 개의 레코드를 입력받을 땐 List<>로 받습니다. 또한 쿼리의 결과가 여러 개의 레코드인 경우도 List<>로 출력합니다.
  5. 동기/비동기 – 동기인 경우는 직접 엔티티나 엔티티리스트형을 반환하지만 비동기로 처리하고 싶다면 LiveData로 감쌉니다.

Dao의 전체적인 안내는 여기입니다.
Dao는 가장 기저인 @Query외에도 보다 추상화된 고수준의 @Update, @Insert, @Delete등의 CRUD 애노태이션을 별도 제공할 뿐 아니라 조인 등에 대응하여 엔티티와 다른 레코드 형태가 결과로 반환되는 경우 중첩 클래스를 이용해 간단히 처리할 수 있습니다. 예를 들어 위의 ItemDao에 다음과 같은 쿼리가 붙었다고 해보죠.

@Dao
public interface ItemDao{
    @Query("select price from items where id = :id")
    ? price(int id);
}

이 경우 쿼리의 결과는 price만 반환하기 때문에 기존의 ItemEntity를 사용할 수는 없습니다. 대신 중첩 클래스를 이용합니다.

@Dao
public interface ItemDao{
    @Query("select price from items where id = :id")
    Price price(int id);
   
    static class Price{
      public int price;
    }
}

Dao는 비동기 출력에 대해 AAC에 포함된 LiveData로 반환할 수 있을 뿐 아니라 표준 Rx를 준수하는 Flowable로 반환할 수도 있습니다.

이들을 모아 하나의 데이터베이스를 정의함

엔티티와 Dao라는 재료가 모였으면 이제 데이터베이스를 정의할 수 있습니다. 데이터베이스는 단지 애노태이션만으로는 정의할 수 없고 반드시 RoomDatabase를 상속 받아야 합니다.

@Database(entities = {ItemEntity.class}, version = 1)
public abstract class ItemsDB extends RoomDatabase{
  public abstract ItemDao itemDao();
}

위 클래스에서 몇 가지 짚어보죠.

  1. @Database – 이 애노태이션으로 버전과 연관된 여러 개의 엔티티를 기술합니다. 이는 즉 이 디비에 저 테이블들을 만들라는 뜻입니다.
  2. abstract class – Dao와 마찬가지로 추상클래스입니다.
  3. abstract ItemDao – DAO를 얻는 메소드를 추상메소드로 선언합니다. 필요한 만큼의 Dao를 만들어 추상메소드로 게터를 만들어가면 됩니다.

버전별 디비마이그레이션 방법데이터베이스를 만드는 추가 사항은 각 링크를 참고하세요.

이 데이터베이스 추상클래스를 만들었다고 Room을 쓸 수 있게 된 건 아닙니다.

RoomDatabase.Builder로 디비 실체화

여태 준비한 모든 재료는 결국 ItemsDB에 집결 되어있습니다. 이 클래스를 빌더에게 전달하면 실체화된 RoomDatabase를 얻을 수 있습니다.

ItemDB itemDB = (ItemDB)Room.databaseBuilder(getContext(), ItemDB.class, "ItemDB").build();

빌더로 실체를 생성할 때는 애플리케이션 컨텍스트가 필요합니다. 이렇게 ItemDB의 구상인스턴스를 얻게 되면 Dao의 게터메소드들을 이용해서 Dao를 쓸 수 있으므로 실질적인 테이블작업을 할 수 있게 되는 것입니다. 이를 4단계로 알기 쉽게 작성해보죠.

//1. 데이터베이스를 얻는다.
ItemDB itemDB = (ItemDB)Room.databaseBuilder(getContext(), ItemDB.class, "ItemDB").build();

//2. Dao를 얻는다.
ItemDao dao = itemDB.itemDao();

//3. Dao의 메소드로 쿼리를 실행한 결과를 받는다.
ItemEntity item = dao.itemById(1);

//4. 반환된 엔티티를 사용한다.
Log.i("test", item.id + ":" + item.name);

ORM이라고는 하지만 정말 필요한 구조물 외에 군더더기가 거의 없는 게 Room의 장점입니다. LiveData로 반환되는 경우는 비동기이므로 수신리스너를 지정하여 값을 얻어야 합니다.

//Dao의 반환이 LiveData인 경우다!
LiveData<List<ItemEntity>> result = dao.loadAll();

//result를 구독하자.
result.observer(this, items->{
  if(items == null) return;
  for(ItemEntiry item:items) Log.i("test", item.id + ":" + item.name);
});

이렇듯 LiveData로 출력하면 비동기가 되어 옵져버로 등록된 Observer는 백그라운드 쓰레드에서 실행됩니다(따라서 핸들러 통하지 않고 걍 UI업데이트 하면 망..)

결론

이상으로 초 간단하게 제로에서 Room을 통한 데이터베이스 사용을 살펴보았습니다. 더 어렵고 복잡한 주제도 이미 본문에 링크가 다 걸려 있으므로 한 바퀴 이해하셨다면 어렵지 않게 정복하실 수 있을거라 생각합니다.