본문 바로가기

안드로이드/코루틴

[안드로이드 Coroutine Flow] 코틀린 Flow에 대해서


728x90

안드로이드 앱 개발에 있어서 비동기 프로그래밍은 중요한 부분을 차지합니다. UI가 반응성 있게 동작하도록 하기 위해서 네트워크 요청, 데이터베이스 작업 등 시간이 걸리는 작업을 메인 스레드에서 분리해서 처리하게 됩니다. 이러한 작업을 리액티브 프로그래밍으로 처리하면 코드 관리 측면에서 유리해지는데, 안드로이드 코틀린 Coroutine의 Flow는 데이터 스트림으로써 리액티브 프로그래밍을 지원합니다.

1. Kotlin Flow 소개

코틀린 Flow는 코루틴을 기반으로 한 리액티브 프로그래밍을 가능하게 하며, 시간에 따라 여러 값을 방출할 수 있는 콜드 스트림(cold stream)입니다. 콜드 스트림은 아래와 같은 특징이 있습니다.

  • 데이터를 내부에서 생성
  • 소비자가 구독할 때, 데이터를 생성
  • 하나의 생산자에 하나의 소비자만 존재

즉, 코루틴에서 이러한 데이터 스트림을 구현할 때 Flow를 사용하게 되며, 데이터 스트림은 아래의 구성으로 역할이 나뉘게 됩니다.

Kotlin Flow Data Stream
Kotlin Flow Data Stream

  • 생산자 (Producer)
  • 중간 연산자 (Intermediary)
  • 소비자 (Consumer)

생산자, 중간 연산자, 소비자 3가지의 구성으로 나뉘게 됩니다. 단어 그대로 생산자는 데이터를 생산하는 역할을 하고 있으며, 중간 연산자는 UI에서 바로 사용할 수 있는 데이터 형태로 변환해 주는 역할을 합니다. 마지막으로 소비자는 중간 연산자가 전달해 준 데이터를 받아서 UI 등의 작업을 처리하는 형태입니다. 각각의 역할에 대해 더 자세히 알아보겠습니다.

2. Consumer (생산자)

생산자는 데이터를 생성하는 역할을 합니다. 코루틴 Flow에서는 flow { } 블록을 통해 Flow를 만들 수 있고 블록 내부에서 emit() 함수를 사용하여 데이터를 생성합니다.

생산자가 가져오는 데이터는 보통 DataSource로부터 가져옵니다. 즉, API를 통한 외부 데이터(Remote Data Source)와 DB(Local Data Source) 데이터입니다.

그런데 이번 포스팅에서는 Flow를 이해하기 위해 간단한 예제로 설명을 진행하겠습니다. 아래 예제는 숫자를 1씩 증가시킨 데이터를 발행하는 코드입니다.

fun getNumberFlow(): Flow<Int> = flow {
    var num = 1
    while (true) {
        emit(num++)
        delay(1000)
    }
}

3. Intermediary (중간 연산자)

생산자가 생성한 데이터를 사용하기 좋게 데이터를 변환하는 역할을 수행합니다. 즉, 서버로부터 받은 데이터를 UI에서 사용하는 형태로 변환하여 UI 레이어에서 필요한 데이터만을 가져오기 위함입니다. 중간 연산자는 데이터 변형을 위한 map, 데이터 필터링을 위한 filter 등을 많이 사용합니다.

// 생산자가 생성한 데이터에 *2 연산을 수행
getNumberFlow().map { number -> 
    number * 2
}

// 생산자가 생성한 데이터 중 1 이상의 데이터만 가져옴
getNumberFlow().filter { number ->
    number >= 1
}

4. Consumer (소비자)

마지막으로 소비자입니다. 소비자는 중간 연산자가 변환한 데이터를 실제로 받는 영역으로써 전달받은 데이터를 소비할 수 있습니다. 즉, UI 레이어에서 데이터를 소비하고, 적절한 UI를 표시하는 역할을 합니다. 코루틴을 사용해서 비동기 작업을 실행하는 방법은 GlobalScrope.launch { }를 사용하기도 하지만, 안드로이드에서는 생명주기를 고려해서 lifecycleScope나 viewModelScope를 사용합니다.

viewModelScope.launch { 
    getNumberFlow().collect { number ->
        // 상태 관련 변수 Update
    }
}


네트워크 요청을 통해 데이터를 받아온다면 아래와 같은 형태로 Flow를 사용할 수 있습니다.

fun fetchUserData(): Flow<User> = flow {
    val user = api.fetchUser() // 네트워크 요청으로 사용자 데이터를 가져옴
    emit(user) // 가져온 데이터를 발행
}.flowOn(Dispatchers.IO) // IO 스레드에서 실행

lifecycleScope.launch {
    fetchUserData().collect { user ->
        // UI 업데이트
    }
}

5. 결론

이번 포스팅에서는 안드로이드 코틀린 Coroutine과 Flow의 기본적인 사용방법에 대해 알아보았습니다. 리액티브 프로그래밍 방식을 통해 비동기 처리를 간단하게 할 수 있으며, 결과적으로 코드의 가독성과 유지보수성을 크게 높여줍니다. 또한, 클린 아키텍처 패턴을 함께 사용하면 각 계층 간의 의존성을 최소화하면서 개발 과정을 더욱 쉽게 만들어주는 것 같습니다.