Android/Room

Room 데이터베이스 복습 (nowinandroid 를 예로 간단히 살펴보기)

노소래 2023. 1. 15. 02:55

이미지 출처 : https://levelup.gitconnected.com/using-room-in-jetpack-compose-d2b6b674d3a5

이번에 가벼운 사이트 프로젝트를 진행하는데, MVP 때는 서버 없이 작업하게 되었다. 그에 따라 로컬 데이터소스만을 활용해서 데이터를 관리해야해서 오랜만에 디비 셋업부터 여러 쿼리를 작성하게 되었다.  

그래서 앞으로 입문부터 쿼리 예시, 마이그레이션, 서버와 동기화 등의 주제로 포스팅하려하는데 오늘은 그 시작인 입문이다.

 

왜 로컬에 데이터를 저장해야하는 경우가 생기는지,

왜 Room 을 쓰면 좋은지,

구성요소는 어떻게되고,

기본적인 사용예시까지만 소개하려한다.


로컬에 데이터를 저장해야하는 경우 예시

대부분 인터넷이 끊긴 경우에도 유용한 정보를 보여주기 위함이다.

네이버 웹툰을 즐겨 사용하는 안드로이드 유저라면 지금 인터넷 연결을 해제하고 네이버 웹툰을 들어가보았을 때 

인터넷이 끊겼음에도 배너를 제외한 홈화면이 정상적으로 나오고 이미 봤던 작품들의 회차리스트가 나오는 것을 확인할 수 있다.

노션 앱도 일부 데이터가 캐싱되어 내가 저장한 정보들을 인터넷 없이도 확인할 수 있어 다행일 때가 있다.

카톡도 마찬가지.

 

그리고 앱 로컬에서만 필요한 정보를 실행이 끝나도 저장되어야하는 경우에 사용하는데 가벼운 데이터를 저장하는 것은 SharedPreference 나 DataStore 를 이용하면 편하다. 예를 들면 간단한 사운드 On/Off 같은 것이 되겠다.


Room 의 등장배경

안드로이드 운영체제에서는 경량의 내장 관계형 데이터베이스 관리 시스템인 SQLite 을 번들로 제공. 이는 안드로이드 프레임워크에 포함되어 과거에는 안드로이드 SDK 에서 SQLite 데이터베이스 관리 시스템에 접근하는 레이어를 구성하는 일련의 클래스들을 제공해서 관리할 수 있게 해주었다. 하지만 이제는 ViewModel 이나 LiveData 같은 새로운 아키텍쳐 가이드라인과 피쳐들을 이용할 수 있다. 이런 단점을 해소하기 위해 안드로이드 젯팩 아키텍쳐 컴포넌트에서 Room 라이브러리를 제공하는 것이다.

또한 아래와 같은 장점도 있다.

- SQLite 를 사용하면 생기는 자잘한 상용구들을 안 쓸 수 있다.

- 컴파일 타임에 쿼리의 유효성을 검사한다. 


구성요소

Database class

Room 데이터베이스 객체는 내부 SQLite 데이터베이스에 대한 인터페이스를 제공하는 친구이다.

앱의 데이터베이스에 대한 DAO 인스턴스를 제공한다.

즉, 데이터베이스를 보유하고 앱의 영구 데이터와의 연결을 위한 기본 액세스 포인트 역할을 한다.

앱은 단 하나의 Room 데이터베이스 객체를 가진다.

DAOs (Data access objects)

데이터 접근 객체는

데이터베이스의 데이터에 대해

insert, query, update, delete 하기 위해 사용할 수 있는 메서드들을 제공.

Entities

앱 데이터베이스의  테이블을 나타낸다.

테이블에 대한 스키마를 정의하는 클래스.

테이블 이름, 열 이름, 데이터 타입을 정의하고 어떤 열이 기본키인지 식별.

getter/setter 메서드를 포함

즉 넣을 때의 객체와 dao 에 의해 전달되는 데이터는 엔티티 클래스의 인스턴스


잠깐 구성요소에 대한 정리

공식문서 에 가보면 위 세 가지 구성요소를 다이어그램으로 표현한 사진이 있는데 대략 내가 이해한 바를 설명하면 다음과 같다.

Room Database 가 DAO 를 가져오고 / DAO 가 db 로 부터 Entities 를 가져오거나 저장한다.그리고 Entities 는 접근자를 제공한다.


예시 

예시는 공식문서가 질렸을테니 nowinandroid 프로젝트를 살펴보자.

패키지 com.google.samples.apps.nowinandroid.core.database  를 찾아가면 코드를 직접 확인할 수있다.

여기서 전체를 다 살피는 것보다 이러한 흐름이라는 예시를 보는 것이 더 효율적이라고 생각해서 일부만 담는다.

단순 사용법은 공식문서를 참고하면 더 편할 것 같다.


model 패키지에 들어가서 아래 코드를 보면

위 구성요소 Entities 에 대한 설명대로

@Entity 어노테이션이 붙은 클래스는 테이블 이름, 열 이름, 데이터 타입을 정의하고 어떤 열이 기본키인지 식별가능하게 하고 있다.

@PrimaryKey 어노테이션으로는 기본키를 식별하고 

@Entity(
    tableName = "news_resources"
)
data class NewsResourceEntity(
    @PrimaryKey
    val id: String,
    val title: String,
    val content: String,
    val url: String,
    @ColumnInfo(name = "header_image_url")
    val headerImageUrl: String?,
    @ColumnInfo(name = "publish_date")
    val publishDate: Instant,
    val type: NewsResourceType,
)

이번에는 DAO 를 살펴보자

DAO 는 앞서 말했듯 한국말로 데이터 접근 객체이고, 그래서 여기서는 NewsResourceEntity 에 접근하게 해주는 DAO 이다.

 

아래 코드는 Room 에 입문하는데 혼란을 줄만한 쿼리는 제거하고 가져왔다.

interface 로 만들어서 Room 이 구현할 수 있게 한다.

@Dao 어노테이션이 붙은 것을 확인할 수 있고

@Query 뿐만 아니라 @Update @Delete 등을 제공해 적은 코드로도 원하는 연산을 수행할 수 있다는 것을 볼 수 있다.

 

@Dao
interface NewsResourceDao {
    @Transaction
    @Query(
        value = """
            SELECT * FROM news_resources
            ORDER BY publish_date DESC
    """
    )
    fun getNewsResources(): Flow<List<PopulatedNewsResource>>

    @Update
    suspend fun updateNewsResources(entities: List<NewsResourceEntity>)

    @Query(
        value = """
            DELETE FROM news_resources
            WHERE id in (:ids)
        """
    )
    suspend fun deleteNewsResources(ids: List<String>)
}

(아마 아시겠지만) 구현체가 궁금하다면 커맨드 좌클릭 또는 왼쪽 구현 아이콘을 좌클릭해서 확인할 수 있다.


마지막으로 RoomDatabase 클래스이다. 

@Database 어노테이션을 붙이고

구현해야하기 때문에 abstract  class 로 선언해야한다.

아래 코드 역시 당장은 불필요한 부분을 제거했다.

@Database(
    entities = [
        NewsResourceEntity::class,
        NewsResourceTopicCrossRef::class,
        TopicEntity::class,
    ]
)
abstract class NiaDatabase : RoomDatabase() {
    abstract fun topicDao(): TopicDao
    abstract fun newsResourceDao(): NewsResourceDao
}

Dao 와 마찬가지로

NewsResourceDao 가 선언되어있는 것을 확인할 수 있고 

CREATE TABLE ~  로 테이블 만드는 것도 확인할 수 있다!

(TopicDao 는 블로그로 안긁어왔지만 NewsResourceDao 와 같은 형태로 있다! Entity도!)

public final class NiaDatabase_Impl extends NiaDatabase {
  private volatile TopicDao _topicDao;

  private volatile NewsResourceDao _newsResourceDao;

  @Override
  protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
    final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(12) {
      @Override
      public void createAllTables(SupportSQLiteDatabase _db) {
        _db.execSQL("CREATE TABLE IF NOT EXISTS `news_resources` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `url` TEXT NOT NULL, `header_image_url` TEXT, `publish_date` INTEGER NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`))");
        _db.execSQL("CREATE TABLE IF NOT EXISTS `news_resources_topics` (`news_resource_id` TEXT NOT NULL, `topic_id` TEXT NOT NULL, PRIMARY KEY(`news_resource_id`, `topic_id`), FOREIGN KEY(`news_resource_id`) REFERENCES `news_resources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`topic_id`) REFERENCES `topics`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
        _db.execSQL("CREATE INDEX IF NOT EXISTS `index_news_resources_topics_news_resource_id` ON `news_resources_topics` (`news_resource_id`)");
        _db.execSQL("CREATE INDEX IF NOT EXISTS `index_news_resources_topics_topic_id` ON `news_resources_topics` (`topic_id`)");
        _db.execSQL("CREATE TABLE IF NOT EXISTS `topics` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `longDescription` TEXT NOT NULL DEFAULT '', `url` TEXT NOT NULL DEFAULT '', `imageUrl` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))");
        _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
        _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f83b94b22ba0a0ce640922a3475e7c3e')");
      }
      ...
   }
 }
 ...
}

 


마무리

여기서 입문은 일단락해야할 것 같다.

이외 추가적으로 여기서 설명거나 나오진 않았지만 nowinandroid 의 core.database 패키지에 있는 다른 어노테이션에 대해 알아보거나 트랜잭션이나, 외부키, 일대다 단순화(by @Relation, @Embedded) 와 같은 추가적 팁을 얻고 싶다면 공식문서에 달려있는 여기나 관련 공식문서들을(ex. 여기) 참고해보면 좋을 것 같다.

이번에 사이드 프로젝트를 지속성 있는 로컬데이터가 필수이다.

또 MVP 에서는 서버가 없어서 완전 메인이다.

그래서 Room 관련은 계속 공부하고,

따라서 블로그에는 조금씩 시리즈로 작성하게 될 것 같다.

 

출처: 디벨로퍼스 공식문서, nowinandroid, 핵심만 골라 배우는 젯팩 컴포즈 + 구글링..!