-
Notifications
You must be signed in to change notification settings - Fork 0
유지보수하기 쉽게 DiffUtil 사용해보기 2
sieunju edited this page Feb 20, 2022
·
2 revisions
- DiffUtil 를 각자 페이지 단위로 관리를 하게 되면 DiffUtil 의 간섭은 없지만, 중복된 아이템과 관리하는 측면에서 어려움이 있습니다.
- 공통 DiffUtil을 관리 할때 ‘충돌'이 자주 발생하는 부분에 대해서 좀더 깔끔하게 처리할수 있게 하여 ‘충돌' 이슈와 한곳에서 DiffUtil를 관리하도록 합니다.
- 이전 시간에 DiffUtil 과 공통 어댑터를 사용하여 유지보수하기 쉽게 할수 있도록 하는 방안을 고민해봤습니다. 또한, 여러사람과 협업시 자주 충돌이 발생하는 공통 DiffUtil 에 대해서 각자 따로따로 관리하면서 최대한 ‘충돌'에 대해서 대비를 하자는 취지 였습니다.
- 하지만, 페이지 단위로 DiffUtil 를 관리 하게 되면 뎁스가 있는 RecyclerView 에서는 ViewHolder 안에서서 DiffUtil 들을 처리하는 로직이 들어가게 됩니다... 오히려 관리하기 힘든? 구조로 되기 때문에 한번더 고민을 하게 되었습니다.
- Inline 함수와 reified 를 잘 활용 하여 DiffUtil 에서 사용되는 주요 함수인
‘
areItemsTheSame
’ 및 ‘areContentsTheSame'
에서 성능 이슈를 최소화 했습니다. - Kotlin 으로 봤을때는 ‘깔끔' 한 코드지만 실상 Java 로 디컴파일 할때 성능상 더 안좋은 코드인 경우가 더러 있어서 몇가지 함수를 구성하고 중간 중간 디컴파일 하면서 성능 이슈를 최소화 하는데 초점을 맞췄습니다.
- DiffUtil 를 보통 사용하게 되면 아래 처럼 사용하게 됩니다. 코틀린으로 개발하다가 자주 디컴파일 해야겠다는 생각이 강하게 들었습니다.
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
val oldItem = oldList[oldPos]
val newItem = newList[newPos]
return if (oldItem is Model1 && newItem is Model1) {
oldItem.id == newItem.id
} else if (oldItem is Model2 && newItem is Model2) {
oldItem.id == newItem.id
} else {
false
}
}
// Java Decompiled..
Object oldItem = this.oldList.get(oldPos);
Object newItem = this.newList.get(newPos);
return oldItem instanceof Model1 && newItem instanceof Model1 ?
((Model1)oldItem).getId() == ((Model1)newItem).getId() :
(oldItem instanceof Model2 && newItem instanceof Model2 ? ((Model2)oldItem).getId() == ((Model2)newItem).getId() :
...? WTF 😭
- DiffUtil 에 else if 문을 사용하다 보면 줄 정렬이 복잡하게 되는 경향이 있습니다. 다른 사람과 협업을 하다 보면 else if 의 특징(?) 때문에 잦은 충돌이 발생 하게 되는데 이때 Merge 를 잘하면 되겠지만, 이러한 점은 많은 리스크를 발생하게 됩니다. 그래서 최대한 switch 문을 사용해서 이를 해결하고 자바로 디컴파일 했을때 위에 처럼 이상하게 되는 이슈를 막는데 초점을 두었습니다.
- 그래서 스위치 문과 inline 함수와 고차 함수를 사용하여 1차적으로 개선해봤습니다.
inline fun <reified R : Any> compareSame2(
old: R,
new: Any,
function: (R, R) -> Boolean
): Boolean {
return if (new is R) {
function(old, new)
} else {
false
}
}
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
return when (val oldItem = oldList[oldPos]) {
is Model1 -> {
compareSame2(oldItem, newList[newPos]) { old, new ->
old.id == new.id
}
}
is Model2 -> {
compareSame2(oldItem, newList[newPos]) { old, new ->
old.id == new.id
}
}
else -> false
// Java Decompiled..👍
Object oldItem = this.oldList.get(oldPos);
if (oldItem instanceof Model1) {
var4 = Companion;
new$iv = this.newList.get(newPos);
$i$f$compareSame2 = false;
if (new$iv instanceof Model1) {
Model1 var7 = (Model1)new$iv;
Model1 old = (Model1)oldItem;
var9 = false;
var10000 = old.getId() == var7.getId();
} else {
var10000 = false;
}
} else if (oldItem instanceof Model2) {
var4 = Companion;
new$iv = this.newList.get(newPos);
$i$f$compareSame2 = false;
if (new$iv instanceof Model2) {
Model2 var10 = (Model2)new$iv;
Model2 old = (Model2)oldItem;
var9 = false;
var10000 = old.getId() == var10.getId();
} else {
var10000 = false;
}
}
- 간단한 설명을 하자면 switch 문에서 oldItem 을 instanceof 체크 하고 그 이후에 newItem 을 instanceof 처리 하면서 결국 (oldItem is Model1 && newItem is Model1) 로직을 switch 문으로 변경하고 ‘깃 충돌' 이 발생할 요소를 최대한 줄여봤습니다.
- 하지만 결국 이것도 처음에 else if 로 처리 되기 때문에 성능 적으로 이슈가 있을것으로 판단이 되어 찐 switch 문으로 하기위한 2차 개선안을 구성해봤습니다.
companion object {
val diffMap: HashMap<Any, String> by lazy { HashMap() }
}
private fun getSimpleNameMap(obj: Any): String {
if (!diffMap.containsKey(obj)) {
diffMap[obj] = obj::class.java.simpleName
}
return diffMap[obj] ?: ""
}
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
return when (getSimpleNameMap(oldList[oldPos])) {
"Model1" -> {
IsDiffUtil.compareSame<Model1>(oldList[oldPos], newList[newPos]) { old, new ->
old.id == new.id
}
}
"Model2" -> {
IsDiffUtil.compareSame<Model2>(oldList[oldPos], newList[newPos]) { old, new ->
old.id == new.id
}
}
else -> false
// Java Decompiled
switch(var3.hashCode()) {
case -1984932280:
if (var3.equals("Model1")) {
var4 = IsDiffUtil.Companion;
old$iv = this.oldList.get(oldPos);
new$iv = this.newList.get(newPos);
$i$f$compareSame = false;
if (old$iv instanceof Model1 && new$iv instanceof Model1) {
Model1 var220 = (Model1)new$iv;
Model1 old = (Model1)old$iv;
var10 = false;
var10000 = old.getId() == var220.getId();
} else {
var10000 = false;
}
return var10000;
}
break;
case -1984932279:
if (var3.equals("Model2")) {
var4 = IsDiffUtil.Companion;
old$iv = this.oldList.get(oldPos);
new$iv = this.newList.get(newPos);
$i$f$compareSame = false;
if (old$iv instanceof Model2 && new$iv instanceof Model2) {
Model2 var218 = (Model2)new$iv;
Model2 old = (Model2)old$iv;
var10 = false;
var10000 = old.getId() == var218.getId();
} else {
var10000 = false;
}
return var10000;
}
break;
- 확실히 switch 문 답게 해쉬코드로 처리하는 장점이 있지만, case 영역에 ‘Model’ Literal 된 것들과 데이터 모델 클래스 명은 일치해야 하고 난독화 처리할때 유의 해야 한다는 단점이 있습니다.
- 랜덤으로 데이터 모델을 만들고, 데이터 모델 개수는 110개로 구성했습니다.
- 실제로 테스트를 해보니 기존에 레거시한 방법은 확실히 느렸습니다. 그리고 생각보다 1차 개선방법이 더 좋았던게 특이 했습니다. 2차같은 경우는 심지어 맵에 저장해서 여러군데에 사용할때 캐싱 처리기능을 넣었는데도 불구하고 1차 개선 방법 보단 아주 조금 느렸습니다. 오히려 맵을 통해 가져오는게 이슈였을지도 모릅니다. 하지만, 2차 개선 방법은 뭔가 모델 클래스 명이 변경될때와 관리하는 측면에서 이슈가 있습니다.
- DiffUtil.calculateDiff() 처리할때 걸리는 평균 시간을 각각에 맞게 MS 으로 표현한 캡처 화면
- 1000개 랜덤 리스트 1000번 반복
- 3000개 랜덤 리스트 300번 반복
- 좀더 나은 방향은 없는지 고민하고 새로운 사실들을 알았던 점이 좋았습니다.
- 그리고 코틀린으로 개발하면서 자주 Java 로 디컴파일 하면서 내 의도대로 구성이 되어 있는지 확인을 해야겠다는 생각이 들었습니다.