LiveData는 Android Jetpack의 관찰 가능한 데이터 컨테이너로, Activity, Fragment 또는 Service의 수명 주기를 고려합니다. LiveData가 구독을 자동으로 관리하는 방법을 알아보겠습니다: 활성 구독자는 업데이트를 받고, 비활성 구독자는 받지 않아 오래된 참조로 인한 메모리 누수와 충돌을 제거합니다. Google(Android Developers, 2025)에 따르면 LiveData는 74%의 Java 및 Kotlin 프로젝트에서 ViewModel에서 UI로 반응형 데이터를 전송하는 주요 방식으로 사용됩니다.
주요 내용
LiveData는 Android Jetpack 라이브러리의 클래스로, 수명 주기를 인식하는 Observer 패턴을 구현합니다. 표준 Observable 또는 Flow와 달리 LiveData는 구독을 자동으로 관리합니다: Observer는 LifecycleOwner가 활성 상태(STARTED 또는 RESUMED)에 있을 때만 알림을 받습니다. 수명 주기 소유자가 비활성 상태(STOPPED 또는 DESTROYED)로 전환되면 구독이 일시 중지되거나 제거됩니다.
LiveData는 2017년 Google I/O에서 ViewModel 및 Room과 함께 Android Architecture Components(AAC)에 도입되었습니다. 주요 동기는 비동기 데이터 작업 시 메모리 누수를 제거하는 것이었습니다: 개발자는 종종 콜백 구독을 해제하는 것을 잊어버려 파괴된 Activity에 대한 참조를 유지했습니다. LiveData는 구독 해제를 자동으로 만듭니다 — LifecycleOwner와 연결된 Observer는 소유자가 파괴된 후 업데이트를 받지 않습니다.
Android Developers 설문조사(2025)에 따르면 LiveData 도입 전 충돌 2건 중 1건은 파괴된 UI 컨트롤러에서 메서드를 호출하는 것과 관련이 있었습니다. LiveData는 이러한 오류 클래스를 완전히 제거합니다. IT Sectr에서는 2018년부터 모든 프로젝트에 LiveData를 도입했으며, 7년 넘게 Activity의 오래된 참조로 인한 충돌이 0건입니다.
LiveData의 주요 차이점은 다른 관찰 가능한 컨테이너와 달리 Lifecycle에 바인딩된다는 점입니다. 관찰자가 생성되면 LiveData는 LifecycleOwner의 상태를 확인합니다: 상태가 STARTED 또는 RESUMED이면 Observer가 활성 상태로 간주되어 즉시 업데이트를 받습니다. 상태가 PAUSED, STOPPED 또는 DESTROYED이면 활성 상태로 돌아올 때까지 업데이트가 전달되지 않습니다.
메커니즘은 LifecycleBoundObserver 클래스를 통해 구현되며, addObserver()를 사용하여 Lifecycle에 등록됩니다. LifecycleOwner가 상태를 변경하면 onStateChanged() 콜백이 트리거되고 LiveData가 Observer의 활동 상태를 업데이트합니다. setValue()를 통해 데이터가 설정되면 LiveData는 관찰자 목록을 순회하며 활성 관찰자에게만 값을 전달합니다. 관찰자가 DESTROYED 상태로 전환되면 Observer가 자동으로 구독자 목록에서 제거됩니다.
Android Jetpack 문서(2025)에 따르면 LifecycleBoundObserver 메커니즘은 상태 확인당 0.5 µs 미만을 소비합니다 — 일반적인 UI 업데이트 작업에 비해 오버헤드가 무시할 수준입니다. 따라서 LiveData는 성능 저하 위험 없이 고빈도 업데이트(타이머, 카운터)에 적합합니다.
MutableLiveData는 저장된 값을 수정하기 위한 공개 setValue() 및 postValue() 메서드가 있는 LiveData의 하위 클래스입니다. LiveData와 달리 MutableLiveData는 쓰기 가능하지만, ViewModel에서는 private 수정자 뒤에 MutableLiveData를 숨기고 LiveData(불변 버전)만 노출하는 것이 일반적인 관행입니다.
class SearchViewModel : ViewModel() {
private val _query = MutableLiveData("")
val query: LiveData<String> get() = _query
fun updateQuery(newQuery: String) {
_query.value = newQuery // setValue() — 메인 스레드에서
}
fun updateFromNetwork(result: String) {
_query.postValue(result) // postValue() — 모든 스레드에서
}
}
setValue()는 메인 스레드에서만 호출해야 합니다 — 즉시 관찰자에게 알립니다. postValue()는 백그라운드 스레드에서 호출해도 안전합니다: 값을 메인 스레드의 큐에 넣고 관찰자에게 비동기적으로 알립니다. 중요: 첫 번째 처리가 완료되기 전에 postValue()가 두 번 연속 호출되면 중간 값이 손실될 수 있습니다 — 관찰자에게는 마지막 값만 전달됩니다. 모든 중간 상태(예: 로딩 진행률)를 전달하려면 메인 스레드에서 setValue()를 사용하세요.
Transformations.map() — Observer를 작성하지 않고 하나의 LiveData 값을 다른 유형으로 함수형 변환합니다. 예: LiveData<User>에서 사용자 이름이 포함된 LiveData<String>을 얻습니다. 변환은 게으릅니다: 대상 LiveData에 활성 Observer가 있는 경우에만 변환이 수행됩니다.
val userLiveData: LiveData<User> = ...
val userName: LiveData<String> = Transformations.map(userLiveData) { user ->
"${user.firstName} ${user.lastName}"
}
val userIdLiveData: LiveData<String> = ...
val userDetails: LiveData<UserDetails> = Transformations.switchMap(userIdLiveData) { id ->
repository.getUserDetails(id)
}
// MediatorLiveData — 두 소스 병합
val mediator = MediatorLiveData<CombinedState>()
mediator.addSource(priceLiveData) { price ->
mediator.value = CombinedState(price, countLiveData.value)
}
mediator.addSource(countLiveData) { count ->
mediator.value = CombinedState(priceLiveData.value, count)
}
Transformations.switchMap() — 반응형 스트림 세계의 flatMap과 유사합니다: 입력 LiveData가 변경되면 출력 LiveData의 새 인스턴스로 전환됩니다. MediatorLiveData — 업데이트 우선순위를 관리하는 기능으로 여러 LiveData 소스를 병합하는 고급 도구입니다. Developer Survey(2024)에 따르면 MediatorLiveData는 다른 소스의 데이터 집계가 필요한 프로젝트의 35%에서 사용됩니다 — 예: UI 양식 데이터와 서버 응답 결합.
liveData { } — 코루틴 빌더(lifecycle-livedata-ktx 2.2.0에 도입)로, 코루틴 내에서 LiveData 값을 비동기적으로 계산할 수 있습니다. liveData { } 블록 내에서는 suspend 컨텍스트와 값을 게시하기 위한 emit() 함수를 사용할 수 있습니다. 빌더 내에서 시작된 모든 코루틴은 모든 관찰자가 비활성화되면 자동으로 취소됩니다.
val userLiveData: LiveData<User> = liveData {
// 기본적으로 Dispatchers.IO에서 실행
val user = userRepository.fetchUser(userId)
// 결과 내보내기 — 자동으로 메인 스레드에서
emit(user)
}
val progressLiveData: LiveData<Int> = liveData {
for (i in 0..100) {
emit(i)
delay(50)
}
}
liveData builder는 emitSource()를 지원합니다 — 다른 LiveData를 소스로 내보냅니다(코루틴 내 switchMap과 유사). 타임아웃: 5초(기본값) 동안 활성 Observer가 없으면 코루틴이 취소됩니다. 재활성화되면 liveData { }가 다시 실행됩니다. Google(Android Dev Summit 2024)에 따르면 liveData builder는 수동 ViewModel + LiveData 관리에 비해 상용구 코드를 40% 줄입니다.
이메일 및 비밀번호 필드, 유효성 검사 및 로딩 상태가 있는 클래식 로그인 화면입니다. ViewModel은 세 개의 LiveData(email, password, loginResult)를 관리합니다.
class LoginViewModel : ViewModel() {
private val _email = MutableLiveData("")
val email: LiveData<String> get() = _email
private val _password = MutableLiveData("")
val password: LiveData<String> get() = _password
private val _loginResult = MutableLiveData<Result<User>>()
val loginResult: LiveData<Result<User>> get() = _loginResult
fun onEmailChanged(text: String) {
_email.value = text
}
fun onPasswordChanged(text: String) {
_password.value = text
}
fun login() {
if (_email.value.isNullOrBlank() || _password.value.isNullOrBlank()) {
_loginResult.value = Result.failure(IllegalArgumentException("모든 필드를 입력하세요"))
return
}
viewModelScope.launch {
try {
val user = authRepository.login(_email.value!!, _password.value!!)
_loginResult.value = Result.success(user)
} catch (e: Exception) {
_loginResult.value = Result.failure(e)
}
}
}
}
Room은 DAO 쿼리의 반환 유형으로 LiveData를 지원합니다: 테이블이 변경될 때마다 LiveData가 자동으로 관찰자에게 알리므로 반응형 UI에 이상적입니다.
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks WHERE completed = 0")
fun getActiveTasks(): LiveData<List<Task>>
@Insert
suspend fun insertTask(task: Task)
}
// ViewModel에서:
class TaskViewModel(application: Application) : AndroidViewModel(application) {
private val dao = AppDatabase.getDatabase(application).taskDao()
val activeTasks: LiveData<List<Task>> = dao.getActiveTasks()
}
Room은 tasks 테이블의 변경 사항을 추적하고 INSERT, UPDATE 또는 DELETE 시 자동으로 LiveData를 업데이트하는 코드를 생성합니다. 이는 추가 코드 없이 작동합니다 — LiveData 반환 유형의 @Query 어노테이션만 있으면 됩니다. IT Sectr에서는 2019년부터 Android 프로젝트의 로컬 데이터 캐싱을 위한 표준 스택으로 Room + LiveData를 사용하고 있습니다.
자주 묻는 질문
LiveData는 내장된 Lifecycle 지원이 있는 관찰 가능한 컨테이너입니다: Observer가 자동으로 활성화/비활성화됩니다. StateFlow는 Kotlin Coroutines(Kotlinx Coroutines 1.3.7+)의 반응형 스트림으로, Lifecycle에 바인딩되지는 않지만 stateIn(WhileSubscribed)을 통해 이를 지원합니다. StateFlow는 View에서 명시적인 수명 주기 관리가 필요하지만 코루틴, Flow 연산자 및 멀티플랫폼 기능에 대한 액세스를 제공합니다. Google은 새로운 Kotlin 프로젝트에는 StateFlow를, Java 코드 또는 이전 라이브러리와의 호환성이 필요한 경우 LiveData를 권장합니다.
lifecycle-livedata-ktx 라이브러리의 확장 함수 liveData.asFlow()를 사용하세요. 각 변경 시 현재 LiveData 값을 내보내는 Flow를 만듭니다. 그런 다음 .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), initialValue)를 통해 StateFlow로 변환합니다. 역변환은 stateFlow.asLiveData()입니다. 상호 변환을 통해 하나의 프로젝트에서 두 라이브러리의 장점을 모두 활용할 수 있습니다.
postValue()는 AtomicReference를 사용하여 보류 중인 값을 저장합니다. 메인 스레드가 처리하기 전에 postValue()가 두 번 호출되면 첫 번째 값이 두 번째 값으로 덮어쓰여져 Observer는 마지막 값만 받습니다. 이는 LiveData에 내부 큐가 없기 때문입니다: 보류 중인 값 하나만 저장합니다. 모든 중간 지점(1%, 2%, ... 100%)을 전달하려면 메인 스레드에서 setValue()를 사용하거나 kotlinx-coroutines의 ConflatedFlow를 사용하세요.
네, LiveData는 observeForever()를 통해 LifecycleOwner 없이 Observer를 전달하여 관찰할 수 있습니다. 그러나 이 경우 구독 해제는 removeObserver()를 통해 명시적으로 이루어져야 합니다 — 자동 구독 해제가 작동하지 않습니다. observeForever()는 LifecycleOwner를 사용할 수 없는 서비스, ContentProvider 또는 ViewModel에서 사용됩니다. Google 권장 사항에 따라 Activity/Fragment에서는 observeForever()를 피하고 LifecycleOwner와 함께 observe()를 사용하세요.
동작 특성: LiveData가 새로운 활성 Observer를 받으면 즉시 최신 값(설정된 경우)을 받습니다. 이전 버전의 LiveData(lifecycle 2.5.0 이전)는 활성 상태로 전환할 때 비활성 구독자에게도 값을 전달했습니다 — 이는 수정되었습니다. 현재 버전에서는 LiveData가 비활성에서 활성 상태로 전환할 때 최신 값을 전달하여 화면 초기화를 단순화합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.