Kotlin Flow와 StateFlow

Reactive Programming

최근 몇 년 동안 Reactive Programming은 애플리케이션 개발의 핵심 패러다임 중 하나가 되었습니다. 그렇다면 Reactive Programming이란 무엇일까요? 기본적으로 Reactive Programming은 데이터 흐름과 변화 전파에 중점을 둔 프로그래밍 패러다임입니다.

이 패러다임은 더 직관적이고 선언적인 방식으로 비동기 데이터 스트림을 작업하도록 해줍니다. 이는 애플리케이션의 여러 부분(예: UI 업데이트, 네트워크 요청, 사용자 입력 처리 등)에서 매우 유용하며, 이를 통해 복잡한 비동기 코드를 더욱 간결하고 이해하기 쉽게 만들 수 있습니다.

특히 안드로이드에서는 사용자 인터페이스와 데이터 상태가 사용자 입력이나 네트워크 응답과 같은 외부 이벤트에 반응해야하는 경우가 많기 때문에, Reactive Programming이 매우 중요합니다.

이처럼 Reactive Programming은 여러 가지 장점을 가지고 있지만, Kotlin Flow와 StateFlow는 이런 개념을 더욱 향상시키며, 안드로이드 개발에서 중요한 도구가 되었습니다. Flow는 Cold 스트림을 제공하며, 이를 통해 더욱 효율적인 데이터 처리가 가능해집니다. 반면, StateFlow는 상태 관리를 위한 강력한 도구로, 특히 상태가 시간에 따라 변화하는 UI를 구현하는데 유용합니다.


Kotlin Flow

Kotlin Flow는 Kotlin의 코루틴을 기반으로 만들어진 Reactive Streams 구현체입니다. Reactive Programming이란 데이터 흐름과 변경의 전파에 초점을 맞춘 프로그래밍 패러다임을 말합니다. 즉, 애플리케이션에서 발생하는 다양한 이벤트 스트림을 관리하고 이들 사이의 의존성을 선언적으로 작성하게 해주는 것이죠.

기본적으로 Flow는 다음과 같은 특징을 가집니다:

  1. 비동기적 연산: Flow는 데이터를 비동기적으로 처리하고 방출합니다. 이는 emit 함수를 통해 이루어집니다. emit은 값을 Flow에 방출하는 함수로, suspend 함수입니다. 즉, emit을 호출하는 코드는 자동으로 비동기적으로 실행되며, 필요에 따라 코루틴을 중단시킬 수 있습니다.
  2. Cold 스트림: Flow는 Cold 스트림의 특징을 가지고 있습니다. Cold 스트림은 구독자가 생겼을 때만 데이터를 방출하는 스트림입니다. 즉, Flow는 데이터를 필요에 따라 생성하고, 필요 없을 때는 생성하지 않습니다. 이는 리소스를 효율적으로 관리할 수 있게 해줍니다.
  3. Context 보존: Flow 연산자들은 컨텍스트 요소를 자동으로 보존합니다. 즉, 각 코루틴이 본래 시작된 스레드에서 실행되도록 보장합니다.

Flow를 만들기 위한 방법은 다양하며, 그 중 몇 가지를 살펴보면 다음과 같습니다:

  • flow 빌더: 가장 일반적인 Flow 생성 방법입니다. flow 블록 내에서 emit 함수를 통해 원하는 값을 방출할 수 있습니다.
val numberFlow = flow {
    for (num in 1..5) {
        delay(100) // 비동기로 대기
        emit(num) // 숫자를 방출
    }
}
  • asFlow: 다양한 컬렉션 혹은 시퀀스를 Flow로 쉽게 변환할 수 있습니다. asFlow 함수를 이용하면 이러한 변환이 가능합니다.
val list = listOf(1, 2, 3, 4, 5)
val flow = list.asFlow() // list를 Flow로 변환

Kotlin Flow는 비동기 데이터 스트림 처리에 강력하면서도 유연한 도구입니다. 그리고 이러한 기능들은 코루틴과 함께 작동하면서 코루틴의 강력함을 더욱 강조합니다.


Flow의 사용 예시

Flow의 생성

Flow를 생성하는 가장 간단한 방법 중 하나는 flow 빌더를 사용하는 것입니다. flow 내에서 emit 함수를 사용하여 값을 생성하고 이를 스트림에 추가할 수 있습니다.

다음은 Flow를 사용하여 1에서 5까지의 숫자를 방출하는 간단한 예시입니다:

val numberFlow = flow {
    for (i in 1..5) {
        delay(1000) // 1초 대기
        emit(i) // 숫자 방출
    }
}

Flow의 구독

Flow에 값을 방출했다면 이제 이 Flow를 구독하여 그 값을 처리할 차례입니다. collect 함수를 사용하면 Flow가 방출하는 값을 수집할 수 있습니다

GlobalScope.launch {
    numberFlow.collect { value ->
        println(value)
    }
}

Flow 연산자

Flow는 다양한 연산자를 제공합니다. 이 연산자들은 각각의 값을 변환하거나 필터링하는 등의 동작을 수행할 수 있습니다.

  • map: 각 값을 특정 함수를 통해 변환합니다.
  • filter: 특정 조건에 맞는 값만을 통과시킵니다.
  • transform: 더 복잡한 변환 로직을 수행할 수 있습니다.

다음은 이 연산자들을 활용한 예시입니다:

val transformedFlow = numberFlow
    .filter { it % 2 == 0 } // 짝수만 통과
    .map { it * it } // 제곱 연산

GlobalScope.launch {
    transformedFlow.collect { value ->
        println(value) // 4, 16 출력 (2의 제곱, 4의 제곱)
    }
}

Flow의 예외 처리

Flow에서 발생하는 예외는 catch 연산자를 통해 처리할 수 있습니다. catch 블록 내에서 발생한 예외를 처리하는 로직을 작성하면 됩니다

val failingFlow = flow {
    for (i in 1..5) {
        if (i == 3) throw RuntimeException("Something went wrong")
        emit(i)
    }
}

GlobalScope.launch {
    failingFlow
        .catch { e -> println("Caught exception: $e") }
        .collect { value -> println(value) }
}

이 예제에서는 Flow가 3을 방출하려고 할 때 예외를 발생시킵니다. catch 연산자가 이 예외를 잡아내고, 해당 예외에 대한 메시지를 출력합니다.

이렇게 Flow는 비동기적인 작업을 쉽게 처리할 수 있게 해주는 강력한 도구입니다.


StateFlow와 SharedFlow

물론입니다. StateFlow와 SharedFlow에 대한 개념, 차이점 및 상태 관리에 대해 다루겠습니다.

StateFlow와 SharedFlow의 개념과 차이점

StateFlowSharedFlow는 Kotlin Coroutines 라이브러리의 일부로, 특정 상태를 표현하거나 여러 수신자에게 값을 공유하는 역할을 합니다. 둘 다 ‘Hot’ Flow로, 값을 수집하는 수신자가 없더라도 값을 방출할 수 있습니다.

StateFlow는 이름에서 알 수 있듯이 상태를 나타내는 데 사용되는 Flow입니다. 각 StateFlow는 항상 현재 상태 값을 가지며, 수신자가 연결되면 즉시 현재 상태를 수신합니다.

SharedFlow는 여러 수신자에게 동일한 값을 방출하는 데 사용됩니다. SharedFlowStateFlow와 달리 현재 상태 개념이 없으며, 값은 명시적으로 방출되어야 합니다.

StateFlow와 LiveData의 비교

StateFlow는 Android Jetpack의 LiveData와 비슷한 역할을 합니다. 둘 다 옵저버 패턴을 사용하여 데이터를 UI 컴포넌트에 바인딩합니다. 하지만 StateFlow는 더 일반적인 코루틴 기반의 접근 방식을 사용하고, LiveData는 Android 생명주기에 특화된 API를 제공합니다.

StateFlow를 사용한 상태 관리

StateFlow는 상태 관리에 매우 유용합니다. ViewModel에서 StateFlow를 사용하여 UI 상태를 나타낼 수 있으며, 이를 UI 컴포넌트가 수집하여 화면을 업데이트하는 방식으로 작동합니다.

다음은 간단한 ViewModel에서 StateFlow를 사용하는 예시입니다

class MyViewModel : ViewModel() {
    private val _state = MutableStateFlow("Initial state")
    val state: StateFlow<String> get() = _state

    fun updateState(newState: String) {
        _state.value = newState
    }
}

// In your UI component
myViewModel.state.collect { state ->
    // Update UI based on the new state
}

위 예제에서, MutableStateFlow는 내부 상태를 가집니다. 외부에서는 읽기 전용 StateFlow를 통해 이 상태를 관찰하고, 필요에 따라 updateState 메소드를 통해 상태를 변경합니다. 이렇게 하면 ViewModel의 상태를 안전하게 캡슐화하고 관리할 수 있습니다.

이상으로 StateFlow와 SharedFlow에 대한 기본적인 소개를 마치겠습니다. 둘은 상당히 유사하면서도 각기 다른 사용 사례를 가지고 있습니다. 따라서 적절한 상황에서 적절한 Flow를 선택하는 것이 중요합니다.


StateFlow의 사용 예시

StateFlow는 애플리케이션의 상태를 관리하는 데 매우 효과적입니다. 다음은 간단한 상태를 관리하는 코드 예제입니다

class UserViewModel : ViewModel() {
    // MutableStateFlow를 private로 선언하여 외부에서 변경 불가능하게 합니다.
    private val _user = MutableStateFlow<User?>(null)
    // StateFlow를 통해 외부에서 상태를 관찰할 수 있습니다.
    val user: StateFlow<User?> = _user

    suspend fun loadUser(userId: String) {
        val user = userRepository.loadUser(userId) // userRepository는 아마 UserRepository와 같은 인터페이스를 구현한 클래스입니다.
        _user.value = user
    }
}

// Activity or Fragment에서 StateFlow 관찰하기
lifecycleScope.launchWhenStarted {
    viewModel.user.collect { user ->
        // UI를 업데이트합니다.
    }
}

위의 코드는 UserViewModel에서 사용자 정보를 불러오고 상태를 관리하는 간단한 예제입니다. loadUser를 통해 사용자 정보를 불러오고 _user에 할당하면, StateFlow를 통해 이 상태를 관찰하고 있는 UI는 즉시 업데이트됩니다.

SharedFlow의 활용 방법

SharedFlow는 여러 컴포넌트에서 동일한 이벤트를 관찰할 때 사용할 수 있습니다. 이벤트가 발생하면 이를 수집하는 모든 컴포넌트에게 방출됩니다.

class EventViewModel : ViewModel() {
    // MutableSharedFlow를 private로 선언하여 외부에서 변경 불가능하게 합니다.
    private val _events = MutableSharedFlow<String>()
    // SharedFlow를 통해 외부에서 이벤트를 관찰할 수 있습니다.
    val events: SharedFlow<String> = _events

    fun sendEvent(event: String) {
        viewModelScope.launch {
            _events.emit(event)
        }
    }
}

// Activity or Fragment에서 SharedFlow 관찰하기
lifecycleScope.launchWhenStarted {
    viewModel.events.collect { event ->
        // 이벤트에 따라 UI를 업데이트합니다.
    }
}

이 예제에서는 EventViewModel이 이벤트를 생성하고, 이를 관찰하고 있는 여러 컴포넌트에게 이벤트를 방출합니다. 이러한 방식은 여러 UI 컴포넌트가 동일한 이벤트를 독립적으로 처리해야 하는 경우에 유용합니다.

이상으로 StateFlow와 SharedFlow의 사용 예시를 마무리하겠습니다. 두 Flow 모두 데이터 흐름을 관리하는 데 있어 강력한 도구이며, 앱의 복잡성을 줄이고 더 효율적인 코드를 작성하는 데 도움을 줍니다. 이를 통해 애플리케이션의 상태를 더욱 안정적이고 예측 가능하게 관리할 수 있습니다.


이 글을 마치며

안드로이드 개발에서의 비동기 작업은 불가피하며, 이를 효율적으로 관리하려면 적절한 도구를 사용해야 합니다. Kotlin의 Flow와 StateFlow는 이러한 비동기 작업을 간결하고 효율적으로 처리할 수 있는 강력한 도구입니다.

Flow는 Cold Stream을 사용하여 데이터를 생성하고 처리하는 데 필요한 시점에서만 작동합니다. 이렇게 하면 불필요한 연산을 방지하고 리소스를 효율적으로 활용할 수 있습니다. 또한, 다양한 연산자를 통해 데이터를 손쉽게 변환하고 필터링할 수 있습니다.

한편, StateFlow는 상태 관리에 특화되어 있습니다. LiveData와 비슷하지만, 더 강력한 기능을 제공하며 백그라운드에서도 안전하게 동작한다는 장점이 있습니다. 상태의 변경을 관찰하고 이에 따라 UI를 업데이트하는 것이 중요한 안드로이드 개발에서 StateFlow는 필수적인 도구가 될 수 있습니다.

마지막으로, SharedFlow는 여러 구독자가 동일한 데이터 스트림을 관찰할 수 있게 해주어, 여러 컴포넌트에서 동일한 이벤트를 독립적으로 처리하는 등의 상황에서 유용합니다.

이처럼 Flow와 StateFlow는 각기 다른 용도와 장점을 가지고 있으며, 이 두 가지를 적절하게 활용하면 비동기 코드를 보다 간결하고 효율적으로 작성할 수 있습니다. 이를 통해 복잡한 비동기 로직을 쉽게 관리하고, 앱의 성능을 최적화하는 데 큰 도움이 됩니다. Flow와 StateFlow의 활용은 안드로이드 개발의 효율성을 크게 높일 수 있으며, 이들의 중요성은 계속해서 증가하고 있습니다.

안드로이드 개발에 투입하는 시간과 노력을 효율적으로 활용하려면, Flow와 StateFlow를 사용하는 방법에 대해 깊이 이해하고 적용하는 것이 중요합니다. 이 포스트를 통해 Flow와 StateFlow의 기본 원리와 활용 방법에 대해 알아보았습니다. 이 지식을 바탕으로, 여러분들이 복잡한 비동기 작업을 효율적으로 관리하는 데 성공하길 바랍니다.

Leave a Comment