[Android] Timer List 프로젝트 리팩토링 : DiffUtil과 BaseItem

ejkim·2025년 2월 25일

프로젝트에 관한 설명 및 코드는 아래 링크에 올려두었으니 참고 부탁드립니다.
📍프로젝트 설명 및 코드 바로가기

AutoDeleteDiffUtil : equals() is not implemented

object AutoDeleteDiffUtil: DiffUtil.ItemCallback<BaseItem>() {
    override fun areItemsTheSame(oldItem: BaseItem, newItem: BaseItem): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: BaseItem, newItem: BaseItem): Boolean {
        return oldItem == newItem
    }
}

AutoDeleteDiffUtil를 만드는 중 areContentsTheSame 함수의 == 에서 아래와 같은 에러가 발생하였습니다.

Suspicious equality check: equals() is not implemented in BaseItem

equals()BaseItem에서 구현되지 않았다는 동등성 검사 에러 메시지가 떠서 BaseItem 구현 부분을 확인해보기 전에.. DiffUtill에 대해 먼저 알아봅시다!

DiffUtil.ItemCallback의 두 가지 메서드

안드로이드에서 RecyclerViewDiffUtil을 사용할 때, 아이템이 동일한지 판단하는 두 가지 메서드가 있습니다.
1. areItemsTheSame(oldItem, newItem)
2. areContentsTheSame(oldItem, newItem)

하나씩 확인해보면,

1. 아이템 자체가 동일한지 검사하는 메서드 : areItemsTheSame(oldItem, newItem)

  • 같은 리스트의 두 개 아이템이 동일한 개체인지 확인
  • 유일한 ID(PK, 고유 식별자 등) 를 비교하는 것이 일반적

여기에서 ID는 데이터베이스에서 Primary Key 같은 역할을 하는데
즉, 두 아이템이 같은 id를 가지면 동일한 아이템이라고 판단 합니다.

  • id가 다르면 완전히 다른 아이템으로 간주 → false
  • id가 같으면 동일한 아이템으로 간주 → true
    📌 단, 아이템의 내용(name, time 등)은 다를 수도 있음 (이건 areContentsTheSame에서 처리)

2. 아이템의 내용이 동일한지 검사하는 메서드 : areContentsTheSame(oldItem, newItem)

  • areItemsTheSame에서 true가 나왔을 때만 호출됨
  • 두 개의 아이템이 같은 id를 가졌더라도, 내용이 달라졌는지 확인
  • 모든 속성을 비교해서 내용이 같으면 true, 다르면 false

예제 코드

override fun areContentsTheSame(oldItem: BaseItem, newItem: BaseItem): Boolean {
    return oldItem == newItem
}

여기서 BaseItem이 data class라면, == 연산자는 자동으로 equals()를 호출해서 모든 필드를 비교해줍니다.

따라서 data class가 아니고, equals()가 구현되어있지 않으면 위에서 발생한 에러가 나타나게 됩니다.

그럼 어떨 때 falsetrue를 반환할까요?

  • id는 같지만, 내용이 변경되었을 때 → false
  • id도 같고, 모든 내용이 완전히 동일할 때 → true

🚗 RecyclerView에서 DiffUtil 동작 과정

  • 리스트가 변경되었을 때, DiffUtil이 비교를 시작
  1. newList가 들어오면, oldList와 비교

  2. 각 아이템을 areItemsTheSame으로 비교
    false → 완전히 새로운 아이템으로 간주 → 뷰홀더 다시 생성
    true → 같은 아이템이므로 areContentsTheSame 비교

  3. areContentsTheSame을 확인
    false → 기존 아이템이 변경된 것으로 간주 → 뷰만 업데이트 (애니메이션 적용)
    true → 변경 사항 없음 → UI 업데이트 없음

BaseItem 구현 부분을 확인

interface BaseItem {
    val id: Int
    val name: String
}

data class OrdinaryItem(
    override val id: Int,
    override val name: String
) : BaseItem

data class AutoDeleteItem(
    override val id: Int,
    override val name: String,
    val time: Int = 0
): BaseItem

🚨문제가 되는 점

  1. BaseItem은 인터페이스라서 data class와는 달리 equals() 메서드가 자동으로 제공되지 않음.
  2. oldItem == newItem은 결국 두 객체가 같은 메모리 주소를 가리키는지를 비교할 뿐, 내부 값 비교를 하지 않음.
  3. 그래서 BaseItem을 상속받은 OrdinaryItemAutoDeleteItem이 각각 equals()를 자동으로 가지더라도, BaseItem 타입으로 비교할 때는 이를 사용할 수 없음.

🚀해결 방법

  1. BaseItem에 equals() 직접 정의
interface BaseItem {
    val id: Int
    val name: String

    override fun equals(other: Any?): Boolean
}
  1. 타입 캐스팅
override fun areContentsTheSame(oldItem: BaseItem, newItem: BaseItem): Boolean {
        return when {
            oldItem is OrdinaryItem && newItem is OrdinaryItem -> oldItem as OrdinaryItem == newItem as OrdinaryItem
            oldItem is AutoDeleteItem && newItem is AutoDeleteItem -> oldItem as AutoDeleteItem == newItem as AutoDeleteItem
            else -> false
        }
    }
  1. areContentsTheSame에서 idname을 직접 비교하기
override fun areContentsTheSame(oldItem: BaseItem, newItem: BaseItem): Boolean {
    return oldItem.id == newItem.id && oldItem.name == newItem.name
}
  1. sealed interface 이용하기
sealed interface BaseItem {
    val id: Int
    val name: String
}
  1. abstract class 이용하기
abstract class BaseItem(
    open val id: Int,
    open val name: String
)

총 5가지 방법을 찾았고, sealed interfaceabstract class의 차이점에 대해서도 찾아봤습니다.

🎯 sealed interface와 abstract class 차이점

특징sealed interfaceabstract class
다중 구현✅ 가능❌ 불가능 (단일 상속)
공통 로직 포함❌ 불가능✅ 가능 (메서드/속성 포함 가능)
생성자❌ 없음✅ 가능
타입 안정성✅ when에서 else 없이 사용 가능✅ when에서 else 없이 사용 가능
필드 강제❌ 선택적으로 구현해야 함✅ 강제됨 (open val 사용 가능)

결론

BaseItemAutoDeleteAdapter에서 두 개의 뷰홀더를 구현하기 위해 추가한 인터페이스입니다. 또한, ainViewModel에서도 when 문을 활용하기 때문에 보일러플레이트 코드를 줄일 수 있어 sealed interface를 추가 하였습니다.

profile
어떤 것이든 그것이 지닌 특별한 속성이나 가치를 간과해서는 안 된다.

0개의 댓글