LiveData 및 MutableLiveData 가 안드로이드 플랫폼에서의 MVVM Architecture 구현에 큰 역할을 해왔다는 사실에는 누구도 부정할 수 없습니다. 구현이 복잡하고 보일러 플레이트 코드가 많은 Observer Pattern 을 대체하여 간편하게 사용할 수 있기 때문입니다.
StateFlow 와 SharedFlow 가 추가되면서 그 입지가 많이 줄었지만, 그래도 여전히 수많은 안드로이드 앱에 사용되고 있습니다.
저는 개발 공부를 할 때 다른 개발자들의 코드를 많이 참고하는 편입니다.
그 개발자들을 선택하는 기준은 두 가지인데,
- 유명한 개발자
- 좋은 품질의 코드를 개발할 수 있는 환경에 있는 개발자
가 그 것입니다.
그러한 개발자들의 여러 코드를 쭉 살펴보면, 보통 소속 집단이나 성향에 따라 코드가 천차만별입니다. 하지만 공통적으로 사용하는 패턴이나 비슷한 부분들이 분명히 있는데, 오늘은 LiveData 를 선언하는 방식에서의 공통점을 발견했고, 이에 대해 기록하고자 합니다.
잘못된 방식 먼저 소개하고, 옳은 방식을 설명해 보겠습니다.
잘못된 방식
class MainViewModel {
val numData = MutableLiveData<Int>()
}
ViewModel 에 단순히 MutableLiveData() 만 구현해두는 방식입니다. 단 하나만 구현하기 때문에 가독성은 굉장히 높다고 할 수 있습니다. 깔끔하긴 하지만, 이렇게 선언을 해두면 Activity 에서 값을 변경할 수 있어, 문제 발생의 소지가 있습니다.
또한, 아래와 같은 방식으로 직접적인 접근이 가능하여 Elegant 하지 못한 느낌도 듭니다.
viewModel.numData.observe(this@MainActivity) {
// todo
}
그런데, 이 방식을 디컴파일 하면 아래와 같습니다.
public final class MainViewModel {
@NotNull
private final MutableLiveData numData = new MutableLiveData();
@NotNull
public final MutableLiveData getNumData() {
return this.numData;
}
}
getter 메서드가 생성되었고, 객체에 private 을 선언하지 않았음에도 불구하고 자동으로 객체를 은닉시킵니다. 언어 차원에서 그렇게 해주지만, 그래도 우리는 명시적으로 private 을 선언해주는 게 좋긴 합니다.
아무튼, 코틀린으로 필드를 작성하면 getter 및 setter 를 만들어줍니다. (물론 위 예시는 var 이 아닌 val 이기 때문에 getter 만 생성) 그래서, viewModel.numData 와 같은 방식으로 접근하는 것은 사실 getter 메서드를 사용하는 것과 같습니다.
즉, 위 코드는 아래 코드와 정확히 같습니다.
class MainViewModel {
private val numData = MutableLiveData<Int>()
fun getNumData() = numData
}
디컴파일 해봅시다.
public final class MainViewModel {
private final MutableLiveData numData = new MutableLiveData();
@NotNull
public final MutableLiveData getNumData() {
return this.numData;
}
}
아무튼 위 방식에는 문제가 있습니다. View 에서 ViewModel 에 대한 직접적인 조작이 '가능'하다는 점입니다. "직접 조작 안 하면 되는 거 아니냐?" 할 수 있지만, 애초에 그러한 가능성을 제거하는 것이 중요합니다.
그렇다면, 그 가능성을 제거한 코드를 작성해 보겠습니다. 그럼 다음과 같을 것입니다.
class MainViewModel {
private val _numData = MutableLiveData<Int>()
val numData: LiveData<Int> = _numData
}
이렇게 작성하게 되면 _numData 는 private 으로 은닉되어 있고, numData 를 통해 외부에서 접근할 수 있기 때문에, 외부로부터 상태에 대한 관찰은 가능하지만 값에 대한 변경이 불가능합니다. 디컴파일 해봅시다.
public final class MainViewModel {
private final MutableLiveData _numData = new MutableLiveData();
@NotNull
private final LiveData numData;
@NotNull
public final LiveData getNumData() {
return this.numData;
}
public MainViewModel() {
this.numData = (LiveData)this._numData;
}
}
_numData 에 대한 은닉과 접근을 위해 numData 라는 새로운 객체가 생성됩니다. 이는 불필요한 메모리를 사용하는 것이기 때문에 효율이 좋다고 말할 수 없습니다.
이러한 경우를 위해 코틀린에서는 get() 키워드를 제공하고 있으며, 이를 통해 MutableLiveData 의 객체만 생성하고 접근 경로만 새롭게 구성할 수 있습니다.
[올바른 방식]
class MainViewModel {
private val _numData = MutableLiveData<Int>()
val numData: LiveData<Int>
get() = _numData
}
프로퍼티는 처음과 같이 구현하지만 numData 의 타입만을 지정해주고 get() 키워드를 삽입하여 _numData 를 할당해 줍니다.
public final class MainViewModel {
private final MutableLiveData _numData = new MutableLiveData();
@NotNull
public final LiveData getNumData() {
return (LiveData)this._numData;
}
}
바이트 코드로 변환하면 이렇게 됩니다. 더 없이 깔끔하고 효율적입니다. _numData 는 private 을 통해 외부로부터의 직접적인 접근이 불가능하게 하고, 이를 LiveData 타입으로 전달받을 수 있도록 합니다.
get() =
get() { }
get 키워드는 두 가지 방식을 지원합니다.
첫째로는 이때까지 기술했던 것과 같이 단순한 할당입니다.
둘째가 특이한데, 블럭에 특정한 액션이 가능합니다. map{} 과 같은 기능을 한다고 볼 수 있습니다.
예시는 다음과 같습니다.
class MainViewModel {
private val justInt = 5
val sumInt: Int
get() {
return justInt + 5
}
init {
print(sumInt)
}
}
최근 코틀린 학습을 시작했는데, 굉장히 매력적인 언어인 것 같습니다.
'Android > Tech' 카테고리의 다른 글
[Dagger-Hilt] ActivityScope 와 ActivityRetainedScope 의 차이 (0) | 2022.10.21 |
---|---|
Coroutine Dispatcher 란 무엇인가? (0) | 2022.10.19 |
Fragment Lifecycle 과 UX (feat.Navigation Component) (0) | 2022.06.03 |
[XML] Expandable Layout 라이브러리 없이 구현하기. (0) | 2022.04.29 |
자주 쓰는 Intent Flag (0) | 2022.04.21 |