
Android ViewModel에서 MutableStateFlow를 직접 노출하는 것은 앱 아키텍처, 데이터 무결성, 그리고 코드의 전체적인 유지 보수성과 관련된 여러 문제를 야기할 수 있습니다. 주요 우려 사항은 다음과 같습니다.
이러한 이유들로 인해, MutableStateFlow와 같은 가변 상태는 외부에 노출되지 않도록 하고, StateFlow와 같은 불변 상태로 노출하는 것이 권장됩니다.
MutableStateFlow를 노출하는 주요 문제는 객체지향 프로그래밍의 캡슐화 원칙을 깨뜨린다는 점입니다. 가변 컴포넌트를 외부에 노출하면 외부 클래스가 상태를 직접 수정할 수 있게 되어, 예측할 수 없는 앱 동작이나 추적하기 어려운 버그가 발생할 수 있으며, ViewModel이 자신의 상태를 관리하고 제어해야 한다는 책임을 위반하게 됩니다.
외부 클래스가 상태를 직접 수정할 수 있을 때, 데이터 무결성을 유지하는 것이 어려워집니다. ViewModel은 더 이상 상태 전환의 유효성을 보장할 수 없게 되어, 앱 내에서 불법적이거나 일관성이 없는 상태가 발생할 수 있습니다. 이는 상태 관리를 복잡하게 만들고, 오류 발생 가능성을 높입니다.
ViewModel 외부에서 상태를 직접 수정할 수 있도록 허용하면 코드베이스가 더 복잡해질 수 있습니다. 상태 변화가 어디에서 어떻게 시작되는지를 추적하기 어려워져, 코드 베이스를 이해하고 유지보수하는 것이 어려워집니다. 이로 인해 디버깅도 더 어려워질 수 있는데, 앱이 특정 상태에 도달한 경로가 명확하지 않기 때문입니다.
MutableStateFlow는 스레드 안전하지만, 앱의 여러 부분이 동시에 상태를 업데이트할 수 있을 때 동시성 관리는 더 복잡해집니다. 신중한 조정이 없으면 경쟁 조건 (race conditions)이나 기타 동시성 문제가 발생하여 앱의 동작을 불안정해질 수 있습니다.
ViewModel의 내부 상태가 외부에서 수정될 수 있을 때, 이를 테스트하는 것이 더 어려워집니다. 테스트에서 ViewModel의 상태를 예측하고 제어하는 것이 어려워지며, 이로 인해 테스트의 신뢰성이 떨어지고 복잡성이 증가할 수 있습니다.
가변 상태를 직접 노출하면 앱의 아키텍처에서 각 계층 간의 경계가 흐려질 수 있습니다. ViewModel의 역할은 UI가 관찰하고 반응할 수 있는 데이터를 제공하고 처리하는 것이지, 어디서나 변경할 수 있는 가변 데이터 소스를 제공하는 것이 아닙니다. 이는 명확한 관심사의 분리를 방해하여, 아키텍처를 이해하고 따르기 어렵게 만들 수 있습니다.
상태가 외부에서 수정될 때, 옵저버들이 변경 사항을 어떻게, 그리고 언제 통지 받는지를 제어하기 어려워집니다. 이로 인해 불필요한 UI 업데이트가 발생하거나, 상태가 변경되었지만 옵저버가 적절하게 통지받지 못하는 문제가 발생할 수 있습니다. 아래는 가변 상태를 노출하는 것이 나쁜 관행임을 보여주는 예시입니다.
class RatesViewModel constructor(
private val ratesRepository: RatesRepository,
) : ViewModel() {
val state = MutableStateFlow(RatesUiState(isLoading = true))
}
이러한 문제를 완화하기 위해, 일반적으로 ViewModel에서 상태를 StateFlow 또는 LiveData를 사용하여 읽기 전용으로 노출하는 것이 권장됩니다. 이 접근 방식은 캡슐화를 유지하면서 ViewModel이 상태를 더 효과적으로 관리할 수 있도록 합니다. 상태 변경은 ViewModel 내의 잘 정의된 메서드를 통해 이루어질 수 있으며, 이 메서드는 필요에 따라 변경 사항을 검증하고 처리할 수 있습니다. 이를 통해 데이터 무결성을 보장하고, 테스트를 단순화 하며, 명확한 아키텍처를 유지할 수 있습니다.
class RatesViewModel constructor(
private val ratesRepository: RatesRepository,
) : ViewModel() {
private val _state = MutableStateFlow(RatesUiState(isLoading = true))
val state: StateFlow<RatesUiState>
get() = _state.asStateFlow()
}
위의 예시에서,
ViewModel 내부에 업데이트를 할 수 있는 private 상태를 가지고 있으며, 이를 내부적으로 업데이트할 수 있습니다. 그런 다음 asStateFlow() 확장 함수를 사용하여 불변 상태를 외부에 노출합니다.
kotlin에서, 특히 Android 개발의 맥락에서 MutableStateFlow를 사용하는 것은 시간이 지남에 따라 변경될 수 있는 데이터를 반응형으로 처리하는 방법을 제공합니다. MutableStateFlow로 표현된 상태를 업데이트해야 할 때, 사용할 수 있는 몇 가지 접근 방식이 있습니다. 이제 이러한 방법들을 살펴보고, 왜! update{}를 사용하는 것이 권장되는 방법인지 알아보겠습니다.
mutableStateFlow.value = mutableStateFlow.value.copy()
해당 방법은 MutableStateFlow의 값을 직접 설정하여 원하는 변경 사항이 반영된 현재 상태의 복사본을 생성하는 것입니다. 이 접근 방식은 간단하며, 간단한 상태 업데이트에 잘 작동합니다. 그러나 이 방법은 원자적이지 않다는 문제가 있습니다. 즉, 여러 스레드가 동시에 상태를 업데이트하면 경쟁 조건(race conditions)이 발생할 수 있습니다.
mutableStateFlow.emit(newState())
emit()을 사용하면 새로운 상태를 MutableStateFlow로 전송할 수 있습니다. emit()은 스레드 안정성을 보장하며, 동시에 업데이트할 때 사용할 수 있습니다. 하지만 emit()은 서스펜딩 함수이기 때문에 코루틴 내에서 호출해야 하여, 상태가 소비될 때까지 기다려야 하는 상황에 적합합니다. 이 방법은 더 유연할 수 있지만, 동기 코드 블록 내에서 또는 코루틴 외부에서 사용할 때 복잡성이 증가할 수 있습니다.
mutableStateFlow.update { it.copy(// state modification here) }
왜? update{}가 선호되는지 설명하겠습니다.
직접 할당과 emit()도 특정한 사용 사례에 유용할 수 있지만, update{}는 MutableStateFlow 값을 업데이트하는 스레드 안전하고 원자적인 방법을 제공하도록 설계되었습니다. 이는 동시성 환경에서 일관되고 안전한 상태 업데이트를 보장해야 하는 대부분의 시나리오에서 뛰어난 선택이 됩니다.
다음과 같은 data class가 있습니다.
data class User(val name: String, val age: Int)
val userStateFlow = MutableStateFlow(User(name = "John", age = 30))
만약 User의 age를 업데이트 하려고 합니다.
userStateFlow.update { currentUser ->
currentUser.copy(age = currentUser.age + 1)
}
이 코드는 userStateFlow의 현재 상태를 원자적으로 업데이트하며, 나이를 1 증가시킵니다.
지금까지 효율적인 앱 개발에 필수적인 고급 기술들을 살펴보았습니다. ViewModel에서 가변 상태를 직접 노출하는 함정과 그에 따른 위험성에 대해 논의했습니다. 이러한 문제를 해결하기 위해 읽기 전용 상태를 사용하고, update{} 함수를 활용하여 안전한 상태 업데이트를 수행하는 방법을 추진했습니다. 이를 통해 코드베이스를 더욱 견고하고 유지 보수 가능하게 만들 수 있습니다.