본문 바로가기

Android/Troubleshooting

Data Class의 equals와 hashCode를 사용해 DB 갱신하기

 

Resume때마다 서버로 부터 새로 데이터를 받아와서 DB에 저장하는데, 이때 DB의 데이터를 전부 삭제(;;)하고 새로운 데이터로 DB를 다시 작성하는 방식이었다.

비둘기가 거꾸로 날아도 날긴 나는거라고, 이런 코드가 2년간 유지되어 왔다는 사실도 놀랍지만 resume때 마다 10초 이상 정지된 화면을 바라보며 대기를 하는데 아무런 불만없이 사용해 주던 사용자분들에게도 미안했다.

 

그러던 중, 얼마전 훑어보았던 드로이드 나이츠 2020 발표 내용에 있던 "Data Class의 equals/hashCode" 내용을 응용해서 문제를 해결할 수 있지 않을까라는 생각들었다.

 

https://speakerdeck.com/taehwandev/amado-effective-han-kotlin?slide=52

 

아마도 Effective 한 Kotlin

드로이드 나이츠 발표 영상 - https://www.youtube.com/watch?v=tna95Q7bnNg 효과적인 Kotlin 사용을 위해 알아볼 내용 - 가독성을 위한 - Property - Scope function - Sequence - Coroutines 블로그 글 https://thdev.tech 발표자료

speakerdeck.com

 

다행히도 해당 방법을 사용함으로써, Room에 저장되어 있는 기존 값의 데이터만 교체하는 방식으로 간편하게 문제를 해결할 수 있었다.

suspend fun databaseRefresh(dataList: TotalDataList) = withContext(Dispatchers.IO) {
    val originalData = ArrayList<InvoiceListData>().apply {
            addAll(dataList.MyDataList)
        }
    val datalList = RoomDB.getInstance().DataDao().getAllData()

    dataList.forEach { newItem ->
        // room에서 기존 데이터와 같은 인덱스 키를 가진 첫 번째 항목 찾기
        val currentItem = databaseList.firstOrNull {
            it.tracking_no == newItem.tracking_no
        }

        if (currentItem == null) {
        // 만약 currentItem이 null이면, 동일한 항목이 없는 것이므로 Insert() 수행
            Timber.e("inserting new item into Room: $newItem")
            dao.insert(newItem)
        } else if (currentItem.hashCode() != newItem.hashCode()) {
            // 변경된 데이터 업데이트
            // DB에 저장된 데이터와 새로운 데이터의 hashCode가 다르면 데이터가 변경된 것으로, 해당 필드를 업데이트
            Timber.e("updating item in room\nbefore: $currentItem \nafter: $newItem")
            dao.update(newItem)
        } else {
            Timber.e("Not Changed Data")
        }
    }
}

 

변수 명이나, 함수 명, DB 명칭 등은 보안을 위해 변경되어 있음을 명시한다.

이런식으로 DB의 데이터를 전부 가져와서 비교하는 이유는, 현재 작업 중인 어플리케이션의 DB에 저장되는 데이터의 양이 많지 않아서 한번에 메모리에 로드하는 것이 좀더 효율적이라 생각되었기 때문이다.

 

만약 데이터가 많으면 한번에 로드하는 것이 OutOfMemoryError를 일으킬 수 있어, 하나씩 가져오도록 하면 된다. 

그런데 데이터를 하나씩 가져오다보면 DB 쿼리가 반복적으로 실행되어 디스크 I/O에 작업이 누적되고 오히려 시간이 더 걸릴 수 있다. 이런 문제는 페이지네이션(pagination)같은 기술로 부분 로드로 커버 가능하다.

 

정답은 없고, 앱의 환경에 따라 적절한 방식을 선택하면 된다. 그냥 얘는 이렇게 해결했구나 하고 참고 바란다.

 

 

아래는 함께 작성한 DAO

// RoomDB내의 모든 데이터를 가져오기
@Query("SELECT * FROM MyListData")
suspend fun getAllInvoices(): List<MyListData>

// 업데이트 작업 수행
@Update
suspend fun update(vararg data: MyListData)

@Update 어노테이션을 사용하면 Room은 내부적으로 객체의 primaryKey를 사용하여 적절한 레코드를 찾아 업데이트를 수행한다. 객체에 변경된 필드가 있으면 해당 필드만 업데이트하고, 변경되지 않은 필드는 그래도 유지한다.

 

 

추가적으로, 이런 검색을 통해 데이터를 비교하는 작업을 수행할 때는 인덱싱(Indexing)의 적용을 고려해 볼 수 있다.

인덱싱은 데이터베이스 테이블을 쿼리하거나 액세스할 때마다, DB 테이블의 모든 행을 검색하지 않고도 데이터를 빠르게 찾을 수 있도록 인덱스를 추가하는 프로세스다.

@Entity(indices = [Index(value = ["phone_no"], unique = true)])
data class MyListData(
    @PrimaryKey val id: Int,
    // ... 다른 필드들
    @SerializedName("phone_no")
    val phoneNo: String
    // ... 다른 필드들
)

인덱스는 DB 스키마에 부여하는 것이다.

@Entity 어노테이션이 적용된 데이터 클래스 내에서 indices 속성을 통해 정의한다.

Index 어노테이션은 phone_no 필드에 대해 인덱스를 생성하도록 Room에 지시하고, Unique=true를 통해 해당 필드의 값이 고유함을 나타낸다.

 

본인은 인덱싱을 더해 좀더 빠른 검색이 가능하도록 코드를 수정했다.

hashCode 비교와 인덱싱을 통해 보통 10000ms이상 소요되던(;;;) Resume시간을 500~1100ms까지 줄일 수 있었다.

DB 갱신에 많은 시간이 소요된다면 한번 쯤 시도해 봐도 좋은 방법이라 생각된다.

 


Ref.

 

코틀린 data class에서 자동으로 처리하는 equals와 hashCode를 알아보자. |

I’m an Android Developer.

thdev.tech

 

 

아마도 Effective 한 Kotlin

드로이드 나이츠 발표 영상 - https://www.youtube.com/watch?v=tna95Q7bnNg 효과적인 Kotlin 사용을 위해 알아볼 내용 - 가독성을 위한 - Property - Scope function - Sequence - Coroutines 블로그 글 https://thdev.tech 발표자료

speakerdeck.com

 

 

Exploring Room Architecture component — The Extras

Exploring extra features of Room library like Transaction, Indexing and TypeConverter

proandroiddev.com