데이터 흐름(Data flow)을 이해해 보는 데 있어 필요한 것은? 짝퉁 개발자처럼 논하기

데이터 흐름(Data flow)을 이해해 보는 데 있어 필요한 것은? 짝퉁 개발자처럼 논하기



개인 광고 영역

제미나이에게 개발에서 데이터 흐름이란?를 알려달라고 했다.

개발에서 데이터 흐름은 어떤 시스템이나 소프트웨어에서 데이터가 생성되고, 변환되며, 저장되고, 전송되는 과정을 의미합니다. 마치 물이 강을 따라 흐르듯이, 데이터는 시스템 내에서 특정한 경로를 따라 이동하며 가치를 창출합니다.

위키백과도 한번 확인해 보았다.

위키 백과 데이터 흐름 - 링크

데이터 흐름(Data flow, 데이터 플로)란 하나의 작업을 수행하기 위하여 실행되는 각각의 세부 작업들 사이에서 자료가 입력되고 출력되는 모습을 의미한다.

결국 같은 말이다.

우리가 매우 흔하게 사용하는 데이터 흐름을 가볍게 이해하는 표현으로 서문을 작성해 보았다.

이 글에서 데이터 다양한 데이터 흐름을 이해하는 데 도움이 될만한 내용을 정리해 본 글인데, 실제 함수 위주이니 참고만 한다고 생각하길

이 글에서는

  • 함수의 blocking vs nonblocking
  • Observer pattern + stream
  • UDF(unidirectional data flow)
  • 매우 주관적으로 작성한 글이다.
  • 데이터 흐름(Data flow)에 대한 새로운 형태를 만드는 짝퉁 설명이니 재미로 읽기를

함수의 blocking vs nonblocking

함수에는 blocking과 nonblocking으로 구분된다.

fun main() {
    println("main")
    someA()
    println("end)
}

fun someA() {
    println("run some a")
}

이 함수의 결과는 다음과 같다.

main
run some a
end

이유는 간단하다. blocking이기 때문이다.

그럼 아래의 코드는?

fun main() {
    println("main")
    someA()
    println("end")
}

fun someA() = CoroutinesScope().launch {
    println("run coroutines)
}

이 함수의 결과는 다음과 같을 수 있다.

main
end
run coroutines

이는 nonblocking이니 가능한 결과이지만 end 전에 coroutines이 실행되어 순서대로 나올 순 있다.

여기서의 흐름은 처음 예제는 명확히 순서를 보장한다는 점이고, 후자는 비동기가 필요하기에 순서의 보장이 필요 없는 경우를 말한다.

데이터 흐름에서 가장 중요한 부분은 비동기라고 할 수 있다.


그럼 아래의 코드는 blocking, nonblocking 중 어느 것일까?

아래 링크에 포함되어 있는 코드를 그대로 가져왔다.

coroutineScope - link

fun main() {
    CoroutinesScope().launch {
        println("main")
        showSomeData()
        println("end")
    }
}

suspend fun showSomeData() = coroutineScope {
    val data = async(Dispatchers.IO) { // <- extension on current scope
     ... load some UI data for the Main thread ...
    }

    withContext(Dispatchers.Main) {
        doSomeWork()
        val result = data.await()
        display(result)
    }
}
  • main 함수는 blocking
  • main 함수의 CoroutinesScope().launch의 시작점은 nonblocking
  • launch { } 안의 내용은 blocking
  • showSomeData()의 async 시작 부분은 nonblocking
  • showSomeData()의 async 내부는 blocking
  • showSomeData()의 withContext 부분은 blocking

이 코드는 blocking과 nonblocking이 매우 많이 뒤섞여있다.

이런 코드가 아주 흔한 일이다. 여기서 동기와 비동기 부분을 명확히 이해해야 데이터 흐름을 빠르게 파악할 수 있다.

이 코드는 이 글의 핵심은 아니지만 동기와 비동기를 이해하는 데 있어 중요한 코드이며, coroutines은 일반적인 함수의 사용만큼이나 쉽다는 점이다.


Observer pattern + stream

다음 문장은 어떤 부분을 설명하는 걸까?

물이 흐르고 있다. 이 흐르는 물에 새로운 물줄기를 추가했다.

이 설명은 개발에서 HotFlow/HotObserve에 대한 설명일 수 있지만 내가 적었으니 맞다.

여기서 중요한 부분은 무엇일까?

바로 흐름(flow)이다. Observer pattern에서 데이터 흐름을 설명하는 쉬운 방법 중 하나이다.

물이 흐른다는 표현을 적었으니 흐르는구나를 알 수 있고, 지속적인 흐름을 의미할 수 있다.

반대로 흐르지 않는 경우도 있는데 어떻게 설명해 볼 수 있을까?

나는 얼음이다. 땡 해주기 전에는 움직일 수 없다.


Coroutines flow는?

Asynchronous Flow - link

A suspending function asynchronously returns a single value, but how can we return multiple asynchronously computed values? This is where Kotlin Flows come in.

코루틴 flow는 단일 값을 한 번씩 호출하여 사용할 수 있는 suspend 대신 지속적인 흐름을 가지기 위한 개념을 포함한다. 바로 Observer + stream을 포함한다.


HotFlow/ColdFlow?

서문에 적은 설명은 오류를 가질 수 있지만 HotFlow/ColdFlow를 각각 설명하기 쉬운 주제라고 생각하여 필자가 주로 설명하는 방식이다.

HotFlow는 두 가지가 존재하는데

  • StateFlow
  • SharedFlow

사용법이 다를 뿐 둘 다 HotFlow이다.

ColdFlow는

  • flow {}
  • flowOf()

flow의 시작점이다.


추가로 - O 어떤 걸로 구성되어 있을까?

이 글에서는 상세한 내용을 알아보기 위해서 적는 글은 아니니 가볍게 가볍게 어떤 구성으로 이루어져 있을까만 적어본다.

얼음이거나 물이 흐르거나로 표현할 수 있었던 이유는 흐르는 물 사이에 새로운 물줄기를 만들거나 얼음을 깨어 새로운 데이터 흐름을 추가할 수 있다는 소리인데

요즘 안드로이드에서는 잘 사용하지 않는 ReactiveX 문서에는 여전히

ReactiveX is a combination of the best ideas from
the Observer pattern, the Iterator pattern, and functional programming

이라고 표현하고 있다.

이 데이터 흐름을 이해하기 위해서는 결국 Observer patternIterator pattern 만 알아도 충분히 이해할 수 있다는 이야기다.

그럼 안드로이드 개발에서 상태의 기억을 가지는 3가지 나열해 보면 아래와 같다.

  • RxJava - Subject 패턴들 4가지가 있으나 상황에 따라 다른 사용을 가짐
  • StateFlow
  • LiveData

이들은 모두 데이터의 제공과 이를 소비하는 패턴으로 만들어져있다.

이때 중요한 부분은 불변으로 데이터를 소비할 수 있도록 만들어주는 데 있다. 불변과 equals/hashCode의 중요성을 각각 확인할 수 있는 관련 글 2개를 링크로 추가한다.

추가로 RxJava를 제외한 Flow와 LiveData는 Android에서 라이프 사이클에 따른 처리가 잘 되어있는 반면 RxJava는 직접 처리해야 할 부분이 많고 현재는 레거시로 취급되니 궁금하신 분은 RxJava 관련 문서를 참고하시길


UDF(unidirectional data flow)

아키텍처를 적극 사용하는 현재는 데이터 흐름이 복잡할 수밖에 없다. 이를 가장 쉽게 설명할 수 있는 부분이 바로 UDF이다.

Architecting your Compose UI - UDF 부분 참고 - link

UDF는 단방향 데이터 플로우인데, 위에서 설명한 blocking, nonblocking 역시 단방향 플로우를 가진다.

  • A 함수를 실행
  • B 함수의 처리
  • 다시 A 함수로 돌아와 이어가기

데이터 흐름상 단방향이다.

Observer pattern + stream에서는?

  • A 함수를 실행
  • B 함수에 구독을 요청하고, stream으로 데이터 흐름을 전달 받는 대기
  • A 함수로 돌아와 A 함수는 끝나고, Stream의 데이터 흐름을 대기

동기와 비동기가 적절하게 포함되어 있는 형태이다.


아키텍처에서의 UDF

UDF는 데이터 흐름을 쉽게 이해하는 데 이를 아주 쉽게 설명한 설명이다.

모든 흐름은 함수의 호출과 그 함수 안에서 새로운 함수의 호출 또는 구독으로 이루어진다. 이를 설명하는 가장 쉬운 방법이 UDF 인 것이다.

그럼 아래의 코드에 대해서 UDF로 설명해 보자.

@Composable
fun Screen(viewModel: SomeViewModel) {
    val someUiState by viewModel.someUiState.collectAsStateWithLifecycle()
    
    Screen(
        someUiState = someUiState,
        onClick = { viewModel.fatchSome() },
    )
}

@Composable
fun Screen(
    someUiState: SomeUiState,
    onClick: () -> Unit,
) {
    Button(
        onClick = onClick,
    )
}

class SomeViewModel(
    private val someRepository: SomeRepository,
) {
    private val _uiState = MutableStateFlow(SomeUiState.Default)
    val uiState = _uiState.asStateFlow()

    init {
        someRepository.flowSome()
            .map { it.toState() }
            .onEach { _uiState.value = it }
            .launchIn(viewModelScope)
    }

    fun fatchSome() = viewModelScope.launch {
        someRepository.fatchSome()
    }
}

interface SomeRepository {

    fun flowSome(): Flow<SomeEntity>

    suspend fun fatchSome()
}

calss SomeRepsotiryImpl(
    private val api: SomeApi,
) : SomeRepository {

    private val flowSome = MutableStateFlow<SomeEntity?>(null)

    override fun flowSome(): Flow<SomeEntity> =
        flowSome.filterNotNull()

    override suspend fun fatchSome() {
        val resutl = api.fatchSome()
        flowSome.value = result.toEntity()
    }
}

코드에 대한 설명은 제외하고 데이터 흐름만을 알아보자.

  • 사용자의 onClick Event를 Composable 함수 Screen에서 발생
  • ViewModel fatchSome() 함수가 호출
  • ViewModel에서는 fatchSome() 함수에서 repository의 fatchSome() 함수를 호출
    • repository에서는 fatchSome 함수의 응답을 지속적인 흐름을 가지기 위해 flow를 별도로 가진다
  • repository에서는 someApi를 통해 fatchSome()을 호출한다.
  • 응답받은 fatchSome()의 결과를 flowSome에 전달한다.
  • ViewModel에서는 구독 중인 flowSome으로부터 응답을 받고, 상태를 변환하여 UI에 통지하여 UI를 갱신한다.

이 코드는 아주 일반적인 UiState를 서버와의 통신을 통해 갱신하기 위한 부분이다. 여기서 조금 더 나아가면 리엑트의 이펙트까지 포함할 수 있다.

말은 길지만 이 방식은 UDF로 설명하지 않았을 뿐 오래전부터 써오던 방식이다.

그리고 다른 사람들에게 설명하는 가장 간단한 프로세스인데, 필자의 블로그에서도 다양하게 확인할 수 있는 과거의 글들이 많이 있다.

UDF. 단방향을 통해 데이터 흐름을 설명할 수 있다는 부분이 중요한 포인트이다.

이 시점에서 추가로 알아두면 좋은 글들을 나열한다.

Composable 함수를 어떻게 분리하는 것이 좋을지에 대한 글과 MVI에 대한 설명의 글이다.

그리고 직전에 작성했던 Theme를 다루는 내용도 있으니 함께 보아도 좋을 듯하다.


서버와의 데이터 흐름

데이터 흐름의 마지막을 서버와 데이터 흐름을 이야기해 볼 수 있지만 자세한 내용은 없이 어떤 도구를 활용하는지 정도만 이야기해 보려 한다.

클라 입장에서는 서버와의 데이터 통신할 때는 json을 주로 활용한다. 최근에는 protobuf를 활용하는 곳도 많은데 장/단점이 있으니 각각 기술은 서버 개발자와 논의하면 좋다.

데이터 흐름을 얼마나 더 넓게 보는지에 따라 서버에서 제공하는 직전까지의 데이터 흐름으로 설명할 것인지, 이를 넘어가서 설명할 것인지도 정의할 수 있을 것 같다.

우리는 string 형태의 데이터를 주고받는 것이 일반적이지만 결국 이런 데이터는 0/1의 데이터로 변환된다.


서버까지 포함하여 데이터의 개념은?

필자는 클라 개발자라 서버에 대해 자세한 이해는 없으니 가볍게 설명해 보겠다.

  • 클라에서의 데이터 응답을 http 통신을 통해 요청하게 된다.
    • 이때 http는 블로킹 상태로 클라에서는 서버가 응답을 주기 전까지 대기하는데, 이때 클라에서는 UI 상 사용자에게 처리 중임을 알려준다.
  • 서버는 캐싱 상태를 체크하고, DB 서버에 응답을 요청한다.
  • DB 서버는 동기/비동기 상태로 entity를 전달해 주고, 이를 기반으로 클라와 약속한 json 데이터로 변환 후 bloking 상태의 http에 응답해 준다.

클라 입장에서는 nonblocking이겠지만 nonblocking 이후에는 모두 blocking 상태로 서버도 동작한다는 점이다.

서버는 사실상 blocking 작업 상태처럼 보이지만 nonblocking으로 보일 수 있다. 그 안에서 또 nonblocking 작업들이 일어나는 것이다.

모든 처리가 완료되면 클라이언트는 이를 바탕으로 UI에 표현하는 작업을 할 수 있다.

여기서는 아키텍처 개념이 포함될 수 있지만 이 글에서는 다루지 않고, 좀 더 넓은 개념의 아키텍처에 대한 이야기를 준비 중이다.


데이터 흐름으로 리액트를 이해할 수 있을까?

리덕스에 대해 제미나이의 응답은 아래와 같다.

리덕스(Redux)는 자바스크립트 애플리케이션의 상태 관리를 위한 예측 가능한 상태 컨테이너이다.
쉽게 말해, 애플리케이션의 데이터를 효율적으로 관리하고 예측 가능하게 만들어주는 도구라고 할 수 있다.

이미 많은 안드로이드 개념에서 리덕스 개념이 포함되어 있는데, UiState와 UDF? 부분일 것 같다.

필자가 직접 iOS TCA를 접하고 있는데, 직접 써보고, 설명을 통해 파악한 리덕스 개념은 아래와 같다.

  • The Composable Architecture - link

  • 이벤트의 흐름 : 이벤트의 흐름은 최종 사용자가 가장 아래에 있으니 이를 거슬러 올라가듯 설명한다. 현재 나의 이벤트가 보이는 화면상이라면 이를 이전 화면에 전달한다. 이런 흐름의 설명이 업스트림으로 설명할 수 있다.
  • 데이터 흐름 : 데이터는 위에서 아래로 흘러간다. 데이터 최신화 시 아래로 아래로 흘러간다. 이런 데이터 흐름을 통해 내가 필요한 부분을 캐치하고 화면을 갱신할 수 있다.

검색과 짧은 지식으로 정리할 수 있는 개념은 딱 요 정도일 것 같다. Redux의 데이터 흐름 글이 있어 링크를 추가한다.

Redux의 데이터 흐름 - link

그리고 안드로이드에서 리액트 형태를 가장 잘 구현한 코드가 드로이드 카이기 아닐까 하여 링크를 추가한다.


마무리

데이터 흐름을 이해한다는 것은 더 많은 것을 볼 수 있고, 파악할 수 있음을 뜻한다고 생각한다.

짝퉁처럼 개념을 이해하는 데 도움이 될 수 있으면 좋겠지만 이 글에서도 알 수 있는데 동기/비동기/ReactiveX 개념까지 알면 이를 통해 리액트의 리덕스 개념도 이해할 수 있는 시점이 된 것 같다.

아직 부족한 부분이 많아서 이 내용으로 모든 걸 다 설명할 순 없지만 어느 정도 충분히 가장 표면적인 내용을 이해하는 데 도움이 되었길.

마지막으로 재웅님의 안드로이드 면접 질문과 관련한 내용 중 compose 개념 설명이 잘 되어있어 링크를 추가한다.

Top 9 Android Developer Interview Questions You Should Know - link



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments