[Android] DiffUtil이 뭔지 설명해주시겠어요? 👀

Jay·2021년 6월 11일
1

Android

목록 보기
31/39
post-thumbnail

면접에서 참 많이 받게 되는 질문이다.

면접을 떠나서 안드로이드 개발자는 안드로이드 어플 개발을 하면서 RecyclerView를 정말 많이 만들어보고 다양한 adpater를 사용해본다.

수 많은 리스트의 데이터의 내용 중 변화가 생기게 된다면 우린 notifyDataChanged()를 통해 변화를 알려주게 된다.
그러나 notifyDataChanged()를 사용하면 새로운 item 인스턴스를 생성하기위해 비용이 많이 든다.

'RecyclerView 성능 개선' 이라고 검색만 해도 수많은 블로그 글들에서 notifyDataChaged()를 사용하지 말고 변경되는 특정 데이터에만 변경을 알려주자! 라고 한다.

🖐 그럼 변경이 단 한개가 아니고 100만개의 리스트 데이터중 7만개가 변경되었고 위치는 그때 그때 다르다고 하자.

우리가 7만개를 다 계산해서 notifyItemChanged()를 날려줄까?
여러 개니까 notifyItemRangeChanged(positionStart, itemCount)를 날려줘? 위치는 다 다른데?

저러한 연산들이 무조건 비효율적이란게 아니다! 상황에 따라 더 나을 수도 있지만 주로 DiffUtil의 성능이 좋다. (그 이유는 앞으로 설명하겠지만)

그럼 알아보자.


DiffUtil

두 목록의 차이를 계산하고 old item에서 new item으로 목록이 변환 할 때 업데이트 되는 작업 목록을 출력하는 유틸리티 클래스

  • RecycerView에 새로운 리스트가 들어올 경우, 해당 리스트를 탐지하고 자동으로 변경해주는 역할을한다.
  • Eugene W.Myers의 difference 알고리즘을 사용한다.
  • 목록 변환을 위한 최소의 업데이트 수를 계산.
  • O(N + D^2)의 시간 복잡도를 갖는다.
    N:추가/제거 된 항목 수, D : 스크립트 길이

그래서 이점은?

  • 변한 아이템을 탐지하고, 내부적으로 알아서 notify해주기에 아이템 변경에 대해 고려를 덜 할 수 있다.

더 나아가서 표현하자면

💡 DiffUtil을 사용하여 데이터 갱신을 위임하자.

Let's use it.

class SubjectDiffUtilCallback(
    private val oldTiles: List<Subject>,
    private val newTiles: List<Subject>
) : DiffUtil.Callback() {

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldSubjects[oldItemPosition] == newSubjects[newItemPosition]
    }

    override fun getOldListSize(): Int {
        return oldSubjects.size
    }

    override fun getNewListSize(): Int {
        return newSubjects.size
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldSubjects[oldItemPosition].number == newSubjects[newItemPosition].number
    }
}

예제는 DiffUtil.Callback에서 주로 쓰이는 4개의 메서드를 구현해놓았습니다.

DiffUtil 클래스 사용을 위해서 바뀌기 전 리스트와 바뀐 후 리스트 2가지를 모두 알고 있어야 한다.

원래 DiffUtil.Callback은 4개의 추상 메서드와 1개의 비추상 메서드로 구성되어있다.

4가지 추상 메서드

  • areItemsTheSame(oldPosition:Int, newPosition:Int) : 두 객체가 동일한 항목을 나타내는지 확인
  • getOldListSize() : 바뀌기 전 리스트의 크기를 리턴한다.
  • getNewListSize() : 바뀐 후 리스트의 크기를 리턴한다.
  • areContentsTheSame(oldPosition:Int, newPosition:Int) : 두 항목의 데이터가 같은지 확인한다.
    해당 메서드는 areItemsTheSame()에서 true 판별이 난 항목에 대해서만 검사를 수행한다.

1가지 비 추상 메서드

  • getChangePayload(oldPosition:Int, newPosition:Int) : areItemsTheSame == true && areContentsTheSame==false일 경우에 호출된다.

🔌 동작방식을 생각해보자.

  1. 우리가 DiffUtil.Callback()을 반환하는 커스텀 클래스를 생성해줘야한다. 위의 예시에서 SubjectDiffUtilCallback처럼!!
    그리고 반드시 areItemsTheSame()과 areContentsTheSame()를 포함한 4가지 추상메서드를 구현을 해줘야한다. 그래야 비교가 가능하다.

  2. 해당 부분을 adapter내에서 데이터를 setting하는 부분에 넣어준다.

  private fun calDiff(newTiles: MutableList<Tile>) {
        val subjectDiffUtilCallback = SubjectDiffUtilCallback(dataSet, newTiles)
        val diffResult: DiffUtil.DiffResult = DiffUtil.calculateDiff(tileDiffUtilCallback)
        diffResult.dispatchUpdatesTo(this)
    }

이런식이라고 생각해보자.
subjectDiffUtilCallback으로 콜백 결과를 받고 이를 DiffUtil.calculateDiff로 넣어서 다시 결과를 받고 이를 업데이트 요청하게 된다.
이는 adapter내에 정의된 메서드이며 adapter에게 변경을 알린다.

💢 조심할 점

  • 목록이 많으면 작업이 상당히 오래 걸릴 수 있기에 백그라운드 스레드에서 실행을 설정하고 DiffUtil.DiffResult를 가져와서 메인스레드에서 RecyclerView에 적용하는게 좋다.
  • 구현 제약으로 목록의 최대 크기는 2²⁶개로 제한 되어 있다고 한다.

Ref

profile
developer

0개의 댓글