반응형

Room 은 SQLite 에 대한 추상화 레이어를 제공하여 개발자가 보다 편리하게 로컬 데이터베이스에 접근할 수 있게 해주는 Jetpack 라이브러리입니다.

 

많은 데이터를 처리하는 앱은 데이터를 로컬로 유지하여 많은 이점을 얻을 수 있는데요. 대표적으로 데이터를 로컬 디비에 캐싱하여 기기가 네트워크에 접근할 수 없거나 몇 번을 가져와도 동일한 데이터를 받는 경우에 서버로부터 데이터를 가져오지 않아도 앱에서 데이터를 활용할 수 있습니다.

 

또한 Room 은 rxjava 나 paging 라이브러리 등과 연동이 가능한 익스텐션 모듈도 있기 때문에 기존 프로젝트에 해당 라이브러리를 사용하고 있었다면 보다 편리하게 연동할 수도 있습니다.

 

이번 포스팅에서는 Room 에 대한 기본 사용법이 아닌, 다양한 커스텀 클래스나 리스트 등 자료구조 데이터를 어떻게 저장하고 가져올 수 있을지에 대해 다뤄보도록 하겠습니다.

 

미리 그 방법을 말씀드리자면, 데이터를 JSON 형태의 문자열로 Serialization(직렬화) 하여 저장하고, 꺼낼 때는 다시 해당 JSON 형태의 문자열을 원하는 형태의 클래스나 자료구조로 변환하도록 할 건데요. 이를 위해서는 직렬화 라이브러리를 사용하면 편리하기 때문에 okhttp, retrofit 등을 개발한 square 사의 Moshi 라는 라이브러리를 사용하도록 하겠습니다. (Moshi 에 대한 가이드는 여기를 참고하세요.)

 

gson, jackson, kotlinx.serialization 등 다른 직렬화 라이브러리를 사용하시더라도 방법의 차이만 있을 뿐 원리는 같으니 본인의 프로젝트에서 사용하는 다른 직렬화 라이브러리가 있다면 해당 라이브러리로도 충분히 적용 가능합니다.

 

예제 설정하기

먼저 오늘 함께 살펴볼 예제 코드를 정의하겠습니다.

 

@Entity
@JsonClass(generateAdapter = true)
data class ChampionInfo (
    @field:Json(name = "id") @PrimaryKey val id: String = "",
    @field:Json(name = "name") val name: String? = null,
    @field:Json(name = "image") val image: Image? = null,
    @field:Json(name = "tags") val tags: List<String>? = null,
    @field:Json(name = "skins") val skins: List<Skin>? = null
)

@JsonClass(generateAdapter = true)
data class Image(
    @field:Json(name = "filaName") val fileName: String
)

@JsonClass(generateAdapter = true)
data class Skin(
    @field:Json(name = "num") val num: Int,
    @field:Json(name = "name") val name: String
)

 

위 ChampionInfo 클래스는 하나의 Entity 로써, 하나의 테이블입니다. 그리고 그 안에 name, image, tags, skins 등 다양한 필드가 정의되어 있는데요. 커스텀 클래스인 Image 와 리스트 형태의 List<String>, 그리고 리스트 형태의 커스텀 클래스인 List<Skin> 에 대해 어떻게 디비에 저장하고 읽는지에 대해 살펴보겠습니다.

 

참고로 @Entity 어노테이션과 @PrimaryKey 어노테이션은 Room 에서 제공하는 어노테이션이고, @JsonClass 와 @field:Json 어노테이션은 Moshi 에서 제공하는 어노테이션입니다.

 

직렬화 / 역직렬화 활용하기

원리는 간단합니다.

예를 들어, fileName 으로 "A.png" 라는 값을 갖고 있는 Image 객체에 대해 아래와 같이 Json 문자열로 변환 후 그대로 로컬 디비에 ChampionInfo 엔티티의 image 필드에 저장하는 것입니다. 그리고 List 형태의 데이터 또한 마찬가지로 JsonArray 형태의 문자열로 변환 후 해당 문자열을 그대로 필드에 저장합니다.

 

ChampionInfo
id name image tags skins
100 Akali {"fileName" : "A.png"} ["Ninja", "Assassin"] [{"num": 0, "name": "default"}, {"num": 1, "name": "tinger Akali"}]

 

그럼 하나하나 살펴보도록 하겠습니다.

 

@ProvidedTypeConverter, @TypeConverter 활용하기

Room 에서 제공하는 @ProvidedTypeConverter 를 사용하여 타입 컨버터를 정의하고 @TypeConverter 를 통해 컨버팅 할 대상들을 어떻게 변환해 줄지 함수로 정의합니다.

 

이때, 직렬화 - 역직렬화에 Moshi 에서 생성해준 Adapter 를 사용하는데요. 각자 편의에 맞게 gson, jackson 등 다른 직렬화 라이브러리를 여기서 적용하시면 됩니다.

@ProvidedTypeConverter
class ImageTypeConverter(
    private val moshi: Moshi
) {

    @TypeConverter
    fun fromString(value: String): Image? {
        val adapter: JsonAdapter<Image> = moshi.adapter(Image::class.java)
        return adapter.fromJson(value)
    }

    @TypeConverter
    fun fromImage(type: Image): String {
        val adapter: JsonAdapter<Image> = moshi.adapter(Image::class.java)
        return adapter.toJson(type)
    }
}

Image 클래스를 직렬화/역직렬화 하는 컨버터

 

@ProvidedTypeConverter
class StringListTypeConverter(
    private val moshi: Moshi
) {

    @TypeConverter
    fun fromString(value: String): List<String>? {
        val listType = Types.newParameterizedType(List::class.java, String::class.java)
        val adapter: JsonAdapter<List<String>> = moshi.adapter(listType)
        return adapter.fromJson(value)
    }

    @TypeConverter
    fun fromImage(type: List<String>): String {
        val listType = Types.newParameterizedType(List::class.java, String::class.java)
        val adapter: JsonAdapter<List<String>> = moshi.adapter(listType)
        return adapter.toJson(type)
    }
}

List<String> 형태의 데이터를 직렬화/역직렬화 하는 컨버터

 

@ProvidedTypeConverter
class SkinListTypeConverter(
    private val moshi: Moshi
) {

    @TypeConverter
    fun fromString(value: String): List<Skin>? {
        val listType = Types.newParameterizedType(List::class.java, Skin::class.java)
        val adapter: JsonAdapter<List<Skin>> = moshi.adapter(listType)
        return adapter.fromJson(value)
    }

    @TypeConverter
    fun fromImage(type: List<Skin>): String {
        val listType = Types.newParameterizedType(List::class.java, Skin::class.java)
        val adapter: JsonAdapter<List<Skin>> = moshi.adapter(listType)
        return adapter.toJson(type)
    }
}

List<Skin> 형태의 데이터를 직렬화/역직렬화 하는 컨버터

 

위와 같이 컨버터들을 모두 준비했다면, 이제 아래와 같이 본인이 정의한 RoomDataBase 구현 객체를 생성하는 단계에서 빌더에 컨버터를 등록해주면 됩니다.

 

val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory()) // 코틀린에서 JsonAdapter 를 생성하기 위한 팩토리 객체
        .build()
        
val appDatabase = Room
        .databaseBuilder(context, AppDatabase::class.java, "YOUR_DB_NAME") // AppDatabase::class.java 대신 본인이 정의한 RoomDatabase 의 하위 클래스를 설정해주면 됩니다.
        .addTypeConverter(ImageTypeConverter(moshi))  // image 를 위한 컨버터
        .addTypeConverter(StringListTypeConverter(moshi))  // List<String> 을 위한 컨버터
        .addTypeConverter(SkinListTypeConverter(moshi))  // List<Skin> 을 위한 컨버터
        .build()

위 예제를 실제로 사용하여 Hilt 와 함께 적용한 샘플 프로젝트가 궁금하다면 제가 작성한 샘플 프로젝트의 Github 을 참고하시면 되겠습니다.

도움이 되었다면 저장소에 Star 도 꾹 눌러주시면 감사하겠습니다 :)

반응형
  • 코틀린 2021.11.10 16:48

    안녕하세요! 글 정말 감사히 잘 봤습니다. 혹시 moshi 어노테이션들에대한 설명도 가능하신가요?

    • Favicon of https://readystory.tistory.com BlogIcon Ready Kim 2021.11.11 01:23 신고

      네 조만간 관련해서 포스팅하도록 하겠습니다 :)

반응형