개발/아키텍처

[Architecture] DataResource

도넛의용기 2025. 9. 11. 18:54

1. 개요

안드로이드 개발에서 데이터 상태를 일관성 있게 관리하는 방법은 애플리케이션의 안정성과 유지보수성을 높이는 데 중요한 역할을 한다. 이 글에서는 Flow와 DataResource를 활용하여 상태를 관리하는 방법에 대해 알아보도록 하겠다.

 

2. Datresource란?

DataResource는 데이터 상태를 나타내는 추상적 개념으로 Success, Loading, Error 세 가지 상태를 포함한다.

이러한 상태를 한 곳에서 집중적으로 관리하면 상태 변화가 한 곳에서 관리되므로 코드의 가독성과 유지보수성이 향상되고 레이어 간 상태 관리가 단순화된다.

또한 상태 변화가 명확하므로 테스트 작성이 용이하며 데이터 처리와 UI 상태 관리가 명확히 분리된다.

 

예시

3. Flow를 활용하기

 

Flow는 비동기 데이터 스트림을 처리할때 데이터 상태 관리에 좋다.

일반적으로 Flow는 데이터 로드 과정에서 Loading에서 Success 또는 Error로 흐름을 따르거나 (Loading -> Error or Success)

Loading에서 여러 번 Success로 이어지는 경우가 있다. (Loading -> Success -> Success->....)

이러한 흐름을 처리하기 위해 2개 이상의 DataResource가 필요할 수 있다.

 

이를 쉽게 처리하기 위해서 Flow를 사용할 수 있다.

Flow는 cold flow와 hot flow 로 나눌 수 있는데 밑에서 그 내용에 대해서 알아보자!

 

4. Cold Flow??

구독자가 데이터를 요청할 때만 데이터를 emit하며 데이터 소스와 구독자 간의 관계가 1:1이다. 구독자가 없으면 데이터를 로드하지 않아 리소스를 절약할 수 있다. 데이터베이스 또는 원격 API 호출에 적합하다.

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.launch

// 콜드 플로우 정의
fun coldFlowExample(): Flow<Int> = flow {
    println("Flow 시작")
    for (i in 1..5) {
        emit(i) // 데이터를 emit
        println("emit: $i")
    }
}

fun main() = runBlocking {
    println("콜드 플로우 구독 시작")
    val flow = coldFlowExample()

    launch {
        flow.collect { value ->
            println("구독자 1: $value")
        }
    }

    launch {
        flow.collect { value ->
            println("구독자 2: $value")
        }
    }
}
콜드 플로우 구독 시작
Flow 시작
emit: 1
구독자 1: 1
Flow 시작
emit: 1
구독자 2: 1
emit: 2
구독자 1: 2
Flow 시작
emit: 2
구독자 2: 2
...

 

5. Hot Flow??

구독자 여부와 상관없이 데이터를 emit하며 여러 구독자가 데이터를 동시에 받을 수 있다. SharedFlow와 StateFlow가 대표적이며 상태를 유지하거나 실시간 데이터 스트림과 UI에 적합하다.

import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.launch

fun main() = runBlocking {
    // 핫 플로우 정의
    val sharedFlow = MutableSharedFlow<Int>() // SharedFlow 생성

    // 데이터 emit 작업
    launch {
        for (i in 1..5) {
            println("emit: $i")
            sharedFlow.emit(i) // 데이터를 emit
            kotlinx.coroutines.delay(500)
        }
    }

    // 구독자 1
    launch {
        sharedFlow.asSharedFlow().collect { value ->
            println("구독자 1: $value")
        }
    }

    // 구독자 2
    launch {
        sharedFlow.asSharedFlow().collect { value ->
            println("구독자 2: $value")
        }
    }
}
emit: 1
구독자 1: 1
구독자 2: 1
emit: 2
구독자 1: 2
구독자 2: 2
emit: 3
구독자 1: 3
구독자 2: 3

6. Flow를 활용한 DataResource

sealed class DataResource<T> {
    data class Success<T>(val data: T) : DataResource<T>()
    class Loading<T> : DataResource<T>()
    data class Error<T>(val exception: Throwable) : DataResource<T>()
}

데이터 리소스 정의

 

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.delay

// DataSource 구현
class DataSource {
    fun fetchData(): Flow<DataResource<List<String>>> = flow {
        emit(DataResource.Loading()) // 데이터 로드 시작
        try {
            delay(1000) // 데이터 로드 시뮬레이션 (1초 대기)
            val data = listOf("Item1", "Item2", "Item3") // 성공적으로 로드된 데이터
            emit(DataResource.Success(data)) // 성공 상태로 emit
        } catch (e: Exception) {
            emit(DataResource.Error(e)) // 에러 상태로 emit
        }
    }
}

플로우를 활용한 DataSource

 

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope

class MainViewModel(private val dataSource: DataSource) : ViewModel() {
    private val _dataState = MutableStateFlow<DataResource<List<String>>>(DataResource.Loading())
    val dataState: StateFlow<DataResource<List<String>>> get() = _dataState

    fun loadData() {
        viewModelScope.launch {
            dataSource.fetchData().collect { resource ->
                _dataState.value = resource // 상태 업데이트
            }
        }
    }
}

뷰모델에서의 사용

 

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
    val dataState by viewModel.dataState.collectAsState()

    when (dataState) {
        is DataResource.Loading -> {
            // 로딩 상태 UI 표시
            println("Loading...")
        }
        is DataResource.Success -> {
            // 성공 상태 UI 표시
            val data = (dataState as DataResource.Success<List<String>>).data
            println("Success: $data")
        }
        is DataResource.Error -> {
            // 에러 상태 UI 표시
            val exception = (dataState as DataResource.Error<List<String>>).exception
            println("Error: ${exception.message}")
        }
    }
}

UI상태 구독

 

 

6. 클린 아키텍처 레이어에서의 데이터 리소스

Data Layer

데이터 레이어에서는 API 호출 및 DB와의 상호작용하는데, 상태를 DataResource로 감싸서 Domain으로 전달하는 역할 수행을 중점으로 코드를 작성하면 된다.

Domain Layer
도메인 레이어에서는 데이터가 변경될 때마다 Flow를 통해 DataResource의 상태를 업데이트 하는 역할로 작성한다.

 

Presentation
이전에 LiveData를 사용했던것과 같이 ViewModel을 통해 Flow 상태를 전파하고 구독하도록 관리하는데, 이를 DataResource로 통합하여 관리한다.