DiffUtil 내부 코드

hegleB·2024년 3월 14일
0
post-thumbnail

기존의 RecyclerView

RecyclerView에서 아이템 변경이 잦을 경우, 전통적으로 notifyDataSetChanged()를 통해 모든 아이템을 업데이트하는 방식이 사용되었다. 이 방법은 아이템이 많아질수록 비효율적이고 공식 문서에서도 notifyDataSetChanged()는 최후의 수단으로 사용하라고 명시되어 있다. notify 메서드를 사용하여 item을 관리해도 상관은 없지만, 수동으로 position을 관리해야 하는 번거로움이 있다. 이러한 문제를 해결하기 위해 Google은 DiffUtil이라는 유틸리티 클래스를 도입했다.

DiffUtil

두 리스트 사이의 차이를 찾고 업데이트 해야 할 항목을 반환하는 기능으로, RecyclerView 어댑터의 업데이트를 효율적으로 알린다. 이 과정에서 Eugene W. Myers의 알고리즘을 활용해 최소한의 업데이트 횟수를 결정한다.

ListAdapter

ListAdapter의 코드를 살펴보면 다음과 같다.

public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {
       
		// ...
    public void submitList(@Nullable List<T> list) {
        mDiffer.submitList(list);
    }

    public void submitList(@Nullable List<T> list, @Nullable final Runnable commitCallback) {
        mDiffer.submitList(list, commitCallback);
    }

submitList 메서드를 통해 표시할 list를 설정한다. 만약 list가 이미 표시되어 있는 경우 background thread에서 diff가 계산되어 main thread에서 Adapter.notifyItem 이벤트를 디스패치한다. submitList 메서드가 2개가 있는데 하나는 list를 인자로 받고 나머지 하나는 list, commitCallback 인자를 받는다.

list, commitCallback을 인자로 받는 submitList 메서드는 list의 차이를 비교하고 최소한의 업데이트로 RecyclerView를 갱신한 뒤, 업데이트가 완료되면 추가적인 작업을 수행하고 싶을 때 사용한다. 예를 들어 사용자가 소셜 미디어 앱에서 새로운 게시물을 작성한 후 목록 화면으로 돌아왔을 때, 새로운 게시물이 목록의 최상단에 추가되어야 하고, 사용자는 자동으로 목록의 맨 위로 스크롤되어 최신 게시물을 볼 수 있어야 한다. 이러한 경우에 사용한다.

public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {
    final AsyncListDiffer<T> mDiffer;
   
    protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
        mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
                new AsyncDifferConfig.Builder<>(diffCallback).build());
        mDiffer.addListListener(mListener);
    }

    protected ListAdapter(@NonNull AsyncDifferConfig<T> config) {
        mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), config);
        mDiffer.addListListener(mListener);
    }
    // ...

ListAdapter를 만들기 위해서는 AdapterListUpdateCallback과 DiffUtil.ItemCallback 클래스가 필요하다. AdapterListUpdateCallback는 notify 메서드 호출을 담당하는 클래스로, ListAdapter 인스턴스 자체를 인자로 받는다. 이는 실제로 어댑터에 데이터 변경을 알리기 위해 필요한 작업을 수행한다.

DiffUtil.ItemCallback는 리스트 간의 차이점을 계산하는 데 사용되는 추상 클래스이다. 이 클래스는 두 아이템이 같은지(areItemsTheSame), 내용이 같은지(areContentsTheSame)를 판단하고, 아이템 변경의 특정 페이로드(getChangePayload)를 제공할 수 있다. 이를 통해, DiffUtilCallback 같은 구체적인 차이점 계산 로직을 구현할 수 있습니다.

마지막으로, 이 DiffUtil.ItemCallback을 이용하여 AsyncDifferConfig의 builder() 메서드를 통해 AsyncListDiffer 객체를 생성한다. AsyncListDiffer는 비동기적으로 리스트의 차이점을 계산하고, 계산된 결과에 따라 어댑터에 데이터 변경을 알리는 역할을 한다. 이 과정을 통해, 리스트 데이터가 업데이트 될 때마다 효율적으로 UI를 갱신할 수 있게 돤다.

AsyncListDiffer

public class AsyncListDiffer<T> {
    // ...
    
    @Nullable
    private List<T> mList;

    @NonNull
    public List<T> getCurrentList() {
        return mReadOnlyList;
    }

    @SuppressWarnings("WeakerAccess")
    public void submitList(@Nullable final List<T> newList) {
        submitList(newList, null);
    }

    @SuppressWarnings("WeakerAccess")
    public void submitList(@Nullable final List<T> newList,
            @Nullable final Runnable commitCallback) {

        // ...

        final List<T> oldList = mList;
        mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {
                final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
                    @Override
                    public int getOldListSize() {
                        return oldList.size();
                    }

                    @Override
                    public int getNewListSize() {
                        return newList.size();
                    }

                    @Override
                    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                      // ...
                    }

                    @Override
                    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                      // ...
                    }

                    @Nullable
                    @Override
                    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
                      // ...
                    }
                });

                mMainThreadExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        if (mMaxScheduledGeneration == runGeneration) {
                            latchList(newList, result, commitCallback);
                        }
                    }
                });
            }
        });
    }

    void latchList(
            @NonNull List<T> newList,
            @NonNull DiffUtil.DiffResult diffResult,
            @Nullable Runnable commitCallback) {
        final List<T> previousList = mReadOnlyList;
        mList = newList;
        // notify last, after list is updated
        mReadOnlyList = Collections.unmodifiableList(newList);
        diffResult.dispatchUpdatesTo(mUpdateCallback);
        onCurrentListChanged(previousList, commitCallback);
    }

AsyncListDiffer를 분석해보면, submitList() 메서드가 background thread에서 DiffUtil.calculateDiff()를 호출해 두 리스트 사이의 차이를 계산하고, 메인 스레드에서 latchList() 메서드로 새 리스트를 업데이트하는 방식으로 되어 있다는 것을 알 수 있다.

public final class AdapterListUpdateCallback implements ListUpdateCallback {
    @NonNull
    private final RecyclerView.Adapter mAdapter;
		
    public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
        mAdapter = adapter;
    }

    /** {@inheritDoc} */
    @Override
    public void onInserted(int position, int count) {
        mAdapter.notifyItemRangeInserted(position, count);
    }

    /** {@inheritDoc} */
    @Override
    public void onRemoved(int position, int count) {
        mAdapter.notifyItemRangeRemoved(position, count);
    }

    /** {@inheritDoc} */
    @Override
    public void onMoved(int fromPosition, int toPosition) {
        mAdapter.notifyItemMoved(fromPosition, toPosition);
    }

    /** {@inheritDoc} */
    @Override
    public void onChanged(int position, int count, Object payload) {
        mAdapter.notifyItemRangeChanged(position, count, payload);
    }
}

latchList() 메서드에서 diffResult.dispatchUpdatesTo(mUpdateCallback) 코드를 통해, diffResult의 결과에 따라 onInserted(), onRemoved(), onMoved(), onChanged() 같은 메서드들이 호출되는 과정을 확인할 수 있다.

따라서 AsyncListDiffer를 통해 개발자들이 notify를 수동으로 관리하는 번거로움을 해소 시켜주고 있다.

그렇다면 DiffUtil를 사용하는 것이 항상 좋을까?

DiffUtil의 시간 복잡도는 최악의 경우 O(N^2)이 될 수 있다. 여기서 N은 리스트 항목의 수이다. 이는 DiffUtil이 두 리스트 사이의 최소 변경 사항을 찾기 위해 각 항목 간의 차이를 계산할 때 발생한다. 리스트의 크기가 크고 항목 간의 차이가 많을수록, 즉 더 많은 비교가 필요할 경우 계산 시간이 길어질 수 있다.

그러나 실제 성능은 데이터의 특성과 변경되는 항목의 양에 따라 달라질 수 있다. 예를 들어, 리스트에 많은 수의 항목이 있지만 변경되는 항목이 적은 경우, DiffUtil은 이를 효율적으로 처리할 수 있다. DiffUtil은 변경 사항을 계산하기 위해 Eugene W. Myers의 차이 알고리즘과 같은 최적화된 알고리즘을 사용하여, 실제 사용 사례에서는 O(N^2)에 가까운 시간 복잡도를 가지지 않을 수 있다.

그러나 대량의 데이터를 처리하거나, 데이터 변경이 매우 빈번할 경우, DiffUtil의 사용은 성능 저하를 초래할 수 있으므로, 이러한 상황에 대한 추가적인 최적화 전략을 고려해야 한다. 예를 들어, 미리 계산된 차이 정보를 사용하거나, 페이징을 사용할 수 있다.

결론

DiffUtil이 등장하기 전까지 개발자들은 notify 메서드를 직접 호출해 데이터 변경을 관리해야 했다. DiffUtil을 통해 AsyncListDiffer가 백그라운드에서 리스트 차이점을 계산한 후, 계산이 끝나면 메인 스레드에서 적절한 notify 메서드를 자동으로 호출하게 되어, 개발자의 부담을 대폭 감소시켰다. 그러나 모든 상황에서 DiffUtil의 사용이 이점만을 가져다주는 것은 아니며, 특히 대량의 데이터를 다루거나 변경이 자주 발생하는 경우에는, 페이징이나 미리 계산된 데이터를 활용하는 것이 바람직하다.

https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil
https://developer.android.com/reference/androidx/recyclerview/widget/AsyncListDiffer
https://cliearl.github.io/posts/android/recyclerview-listadapter/

profile
성장하는 개발자

0개의 댓글

관련 채용 정보