기존에 ViewModel 작성과정에서 liveData를 다음과 같이 선언하곤 했다.
val responseLiveData : MutableLiveData<String> by lazy { MutableLiveData().also { loadResponse() } }
이렇게 선언해두면, LiveData의 observe 호출시 lazy에 의해 MutableLiveData가 생성된다.
이후, also에 따라 loadResponse 메소드가 그때 호출되서 자연스럽게 api나 유스케이스 호출 결과를 얻을 수 있었다.
ViewModel을 사용하는 이유가 무엇인가.
화면 회전시나 홈화면 이동시 onDestroy가 호출되기 때문에 Activity가 파괴됨에 따라 기존 로직 또는 데이터를 저장하기 위함이 아닌가.
근데 이렇게 데이터를 불러오는 로직을 onCreate에서 viewmodel의 메소드를 호출하는 식으로 작성한다면 화면 회전시마다 매번 메소드를 호출하지 않을까? 라는 생각이 들어서 이런식으로 작성했던 것 같다.
override fun onCreate(~) {
viewModel.loadResponse() // 화면 회전할때마다 매번 호출됨
viewModel.responseLiveData.observe(this) {
~
}
}
이렇게 된다면 맨 처음 앱 실행시 한번만 가져와도 되는 정보 (덜 중요한.. 등등)가 매번 화면이 회전될때 로드 요청이 들어오니 솔직히 낭비라고 생각이 들었다.
따라서 lazy로 선언해두고, 맨 처음 observe할때 프로퍼티에 접근하니까 그때 로드를 하면 되지 않을까? 라는 생각이 들어서 맨 처음 코드처럼 수정하게 되었다.
오늘 전까지 이런식으로 코드를 작성했다.
물론 작동은 잘했다. 코루틴의 작동 방식을 간과한 내 잘못이였지만 말이다.
class Main {
val resultProperty : Result<String> by lazy { Result.success("Hello world").also { loadResult() } }
fun loadResult() {
resultProperty.onSuccess {
println("!")
}
}
}
다음과 같은 코드에서 Main().resultProperty에 접근하는 순간 어떻게 될까?
프로퍼티에 접근하니 lazy 블럭에 의해 successful한 result를 반환할거고, also에 의해 loadResult도 호출되겠지?
Exception in thread "main" java.lang.StackOverflowError
at Main$resultProperty$2.invoke(Main.kt:6)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at Main.getResultProperty-d1pmJ48(Main.kt:6)
at Main.loadResult(Main.kt:9)
바로 스택 오버플로우가 터진다.
기존까지는 잘썼는데..? 왜 오늘따라 안되지 하고 한참을 생각한 결과 원인을 찾아냈다.
위 코드에서 loadResult를 코루틴이나 쓰레드로 감싸보자.
fun loadResult() {
thread { // CoroutineScope도 가능
resultProperty.onSuccess {
println("!")
}
}
}
결과 : !
이번에는 문제 없이 잘 돌아간다.
왜 그런걸까?
kotlin의 확장함수인 also는 it으로 자신에게 접근할 수 있는 블록을 제공하고 리턴값은 자기 자신이다.
그래서 보통 리턴하기 전에 also 블럭을 통해 최종적으로 처리하거나 덧붙일 작업을 하곤한다.
apply가 자신의 필드를 설정하는데 사용한다면 also는 다른 객체에게 자신을 요청할때 쓰는?
return userResponse.also { logger.info(it.name) }
문제는 이게 "리턴전"에 호출된다는 점이다.
위 코드에서는 뭐가 문제였을까?
바로 프로퍼티가 아직 초기화가 되지도 않은 상황에서 프로퍼티에 또 접근해서 lazy블럭이 무한 루프되었기 때문이다~
by lazy { Result.success("Hello world").also { loadResult() } }
lazy 블럭을 보면 Result.success(~)로 리턴할 결과값을 우선 반환한다.
근데, also 블럭을 통해 리턴하기전 한 작업을 호출한다. "리턴하기전"에
fun loadResult() {
resultProperty.onSuccess {
println("!")
}
}
load result는 resultProperty에 접근한다.
문제가 없어 보이는가?
위에도 말했다시피 lazy블럭에서 아직 리턴값이 오지 않았다.(also 블럭에 의해 호출됨) 따라서 프로퍼티인 resultProperty는 초기화 되지 않았다.
그래서 resultProperty가 초기화 되지 않았으므로 loadResult함수에서 다시 resultProperty의 lazy블럭을 부르게 된다.
그러니 무한루프로 터질 수 밖에.
쓰레드나 코루틴으로 호출한 메소드는 즉시 제어값이 메인쓰레드에게 반환된다.
보통 쓰레드를 처음 배울때 다음과 같은 코드의 호출 순서를 배우지 않는가
println(1)
thread {
println(2)
}
println(3)
결과
1
3
2
물론 jdk환경이나 pc 환경따라 결과가 달라질 수 있겠지만, 보통 1, 3, 2순서로 출력이 된다. thread나 코루틴은 특별한 처리를 하지 않는다면 즉시 해당블럭에서 벗어나 다음 플로우를 호출한다.
다시 그 코드를 보자.
fun loadResult() {
thread { // CoroutineScope도 가능
resultProperty.onSuccess {
println("!")
}
}
}
결과 : !
여기서는 thread에서 내부 프로퍼티에 접근한다.
그럼 어떻게 되겠는가. 메인쓰레드는 즉시 해당 블럭을 벗어나고, also 블럭에서 벗어났으니 기존 리턴값인 Result.succss값을 반환할것이다.
이후 접근하는 thread는 resultProperty가 초기화 되어 있으므로 문제없이 작동한다.
이런 문제점때문에, 이때까지 작성한 코드에 문제가 있었음을 인지하지 못했다.
val chartLiveData: MutableLiveData<MutableList<Chart>> by lazy {
MutableLiveData<MutableList<Chart>>().also { loadAllChartData() }
}
//최초로 한번 모든 차트 데이터를 불러오는 기능
private fun loadAllChartData() {
CoroutineScope(Dispatchers.IO).launch {
chartList = loadChartOrder().toEmptyChart() //order load
val date = Instant.now()
val heartRateJob = async { loadHeartRateChart(date) }
val sleepHourJob = async { loadSleepHourChart(date) }
val systolicJob = async { loadBloodPressureSystolicChart(date) }
val diastolicJob = async { loadBloodPressureDiastolicChart(date) }
// async - await를 이용해서 동시에 로드할 수 있도록 수행
heartRateJob.await()
sleepHourJob.await()
systolicJob.await()
diastolicJob.await()
chartLiveData.postValue(chartList) //위 job이 업데이트 하지 못할것 대비 (test)
}
}
예전에 작성했던 코드인데, 차트 정보를 가져올때 다음과 같이 작성하곤 했다.
여기서도 코루틴 스코프를 이용했기 때문에 문제는 일어나지 않았다.
val mdmStatusLiveData : MutableLiveData<Boolean> by lazy {
// lazy하게 가져오면서 로드시 mdm 정보 로드까지.
MutableLiveData<Boolean>().also { loadInitialMdmStatus() }
}
private fun loadInitialMdmStatus() {
CoroutineScope(Dispatchers.IO).launch {
val data = getMDMDataUseCase() ?: return@launch
mdmStatusLiveData.postValue(data.isEnabled)
}
}
}
여기서도 코루틴 스코프를 사용했기 때문에 문제가 일어나지 않았다.
// 액티비티에서 접근하는 LiveData. Mutable 하지 않게 제공
val locationLiveData : LiveData<UserLocation>
get() = _userLocationLiveData // get property이용해서 mutable한 livedata 제공
// 실질적으로 내부에서 관리되는 LiveData. Mutable함
private val _userLocationLiveData : MutableLiveData<UserLocation> by lazy { MutableLiveData().also { loadLocation() } }
// gps 정보 불러오는 메소드
fun loadLocation() {
val isEnabled = gpsEnabledUseCase() // 활성화 여부
if (!isEnabled) {
// 활성화 되지 않은경우
_gpsErrorLiveData.value = Event(GPSErrorType.NOT_ENABLED)
return
}
getLocationUseCase().onSuccess {
// 유저 정보 가져와지면
_userLocationLiveData.value = it
}.onFailure {
// 실패시
_gpsErrorLiveData.value = when (it) {
is IllegalStateException -> Event(GPSErrorType.NOT_ENABLED) // gps 비활성화
is NullPointerException -> Event(GPSErrorType.LOAD_FAIL) // 로드 실패
else -> Event(GPSErrorType.INTERNAL) // 내부 오류 - 펄미션 없음 등
}
}
}
이제 보이는가? loadLocation은 코루틴, 쓰레드등 없이 메인 쓰레드에서 정보를 가져오므로 모든 라인을 직접 처리한다.
이때 _userLocationLiveData 프로퍼티에 접근하는 순간 초기화가 되어 있지 않으므로 lazy 블럭이 다시한번 호출된다..ㅜㅜ
그래서 앱이 터져버렸다.
override fun onCreate(~) {
viewModel.loadLocation()
viewModel.liveData.observe~
}
val liveData by lazy { MutableLiveData().apply {
value = loadLocationOnce() } }
private fun loadLocationOnce() : Location {
return Location~
}
이렇게 한다면 apply에 의해 해당 메소드의 '결과값'이 리턴될 값에 value로 할당되니 프로퍼티를 중복해서 접근할 필요도 없어진다. 이후, 갱신이 필요할때 public한 함수로 접근하면 되겠다.
val liveData = MutableLiveData()
init {
liveData.value = "AWESOME"
}
근데 lazy를 사용하는 이유가 context등을 사용하는 서비스가 현재 context가 올바른 상태 (onResume)가 아닐때 초기화된다면 오류가 생겨서 사용하는건데 init블록과는 충돌이 생길 가능성이 있다.
NDM - Reader
ip 로드 부분은 vm에서만 사용되므로 apply블럭에서 string으로 불러와서 value 셋팅. PreferenceDataStore 접근은 그리 긴시간 소요되지 않아 runBlocking으로 블락.
val serverIPLiveData: MutableLiveData<String> by lazy {
MutableLiveData<String>().apply {
val ip = loadServerIP() ?: return@apply
value = ip
}
}
FT
// lazy로 지정되어 바로 로드가 아닌 참조시 로드
// 토큰값이 '설정'되어 있는지만 확인하는 livedata
val isTokenSetLiveData: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = loadTokenIsValid()
}
}
// 설정값 가져오는 livedata
val settingDataLiveData: MutableLiveData<SettingData> by lazy {
MutableLiveData<SettingData>().apply {
val setting = loadSetting() ?: return@apply
value = setting
}
}
얘도 이하 동일
정신 잘 차리고 버그없는 코드를 잘 짜보자..