[Android] UDF 적용기 (feat : redux)

Choi Sang Rok·2023년 3월 9일
5
post-custom-banner

https://velog.io/@evergreen_tree/Android-MVI-%ED%8C%A8%ED%84%B4
이전에 MVI에 대해 작성했던 글입니다. 연관되는 내용이 많으니, 보고 오시면 이해가 더 쉽습니다.
필자의 개인의 주관적인 생각이 섞여있습니다. 오타, 반박 언제나 환영합니다!


💁‍♂️UDF란?

UniDirectional Data Flow의 약자로, 단방향 데이터 흐름을 의미합니다.

UDF의 규약

  • 단방향 수정
  • 동기적 실행
  • View와 State의 분리

Android의 경우 Activity에 View, Data, Logic이 겹치는 경우 유지보수가 힘들고 테스트가 어렵기 때문에 이전부터 View와 Data를 분리시키려는 노력이 있었습니다. 현재는 AAC Viewmodel의 활약으로 MVVM 등의 아키텍처가 성행하고 있고, 그만큼의 장점을 가져오고 있습니다.

UDF를 이러한 현재 View Data 분리 시스템에 녹일 수 있는데요,
UDF의 규약을 가지고 태어난 3개의 아키텍처로 Flux, Redux, MVI가 있습니다.
YAPP 21기로 활동하면서 참여한 프로젝트 티미티미에서는 Redux 아키텍처를 채택하여 앱을 개발하였는데요, 왜 선택하게 되었고 어떻게 적용했는지 공유하려고 합니다.

💁‍♀️왜 UDF인가?

상태 문제 예방

  • State가 여러 작업에서 참조되고, 뒤엉키게 됨
  • 비동기 작업과 얽혀 상태가 복잡해지는 경우가 생김
  • ex) 페이스북의 알림 카운트의 동기화가 올바르지 않은 경우

UDF를 적용하면 이러한 장점을 얻을 수 있습니다.

  • 상태 변경을 동기적으로 실행하기에, 상태 문제에 대해 안전함
  • State 변화 지점이 명확하여 디버깅이 쉽다.

물론, 단점도 있습니다.

  • 높은 러닝 커브
  • 많은 코드 수와, 복잡한 구조

저도 해당 아키텍처를 적용하면서, 1번의 문턱을 넘기 위해 꽤나 많은 노력을 하였고, 프로젝트를 진행하면서 2번에도 많은 고통을 받았습니다.

위에서 말했듯, UDF의 규약을 가지고 태어난 3개의 아키텍처로 Flux, Redux, MVI가 있다고 하였는데요, 각각의 아키텍처 또한 서로 다른 차이점을 가지고 있습니다.

ViewModel이 가지고 있는 비즈니스 로직을 Reducer에 위임하면서, ViewModel이 책임지는 행위를 줄이고, 발생할 수 있는 문제를 디버깅할때 더 쉽게 찾을 수 있도록 Redux를 채택하게 되었습니다.

💁‍♂️구현

ViewModel에서 Intent를 받아, IntentFlow에 데이터를 발행 (View에서 ViewModel)

fun dispatch(intent: INTENT) {
        viewModelScope.launch {
            _mutableIntentFlow.emit(intent)
        }
    }

다음 과정을 거쳐 state가 최종적으로 생산

open fun start() {
        val initialViewState = getInitialState()
        viewState = _mutableIntentFlow
            .mutate()
            .filter { registerReducer() != null }
            .scan(initialViewState) { prevState, mutate ->
                registerReducer()!!.invoke(mutate, prevState)
            }
            .catch { processError(it) }
            .stateIn(
                viewModelScope,
                // 바로 생산 시작.
                SharingStarted.Eagerly,
                initialViewState
            )
    }

mutate() middleware에서 mutate 작업을 수행한 후 다시 intentflow에 병합됩니다. 이는 스트림의 가장 마지막 데이터를 반환하게 됩니다.

private fun SharedFlow<INTENT>.mutate(): Flow<INTENT> {
        val middlewareList = registerMiddleware()
        return if (middlewareList.isEmpty()) {
            this
        } else {
            middlewareList
                .scan(this.filterNotNull()) { prevIntentFlow, nextMiddleware ->
                    merge(
                        nextMiddleware.mutate(viewModelScope, prevIntentFlow, _singleEventFlow),
                        prevIntentFlow
                    )
                }.last()
        }
    }

MiddleWare은 말그대로 중개자의 역할을 하는데, Intent와 Event를 받아서 상태(State)를 업데이트 할 것인지, SideEffect를 호출할지를 결정하게 됩니다.

interface BaseMiddleware<INTENT : BaseIntent, EVENT: BaseSingleEvent> {
    fun mutate(scope: CoroutineScope, intentFlow: Flow<INTENT>, eventFlow: MutableSharedFlow<EVENT>): Flow<INTENT>
}

피쳐에서 정의한 Intent에 대해 onEach로 SideEffect를 발생시킵니다.(여기서 event를 방출) 그리고 HotFlow로 변환하여 다시 intenFlow에 병합하게 됩니다.

override fun mutate(
        scope: CoroutineScope,
        intentFlow: Flow<CreateProjectIntent>,
        eventFlow: MutableSharedFlow<CreateProjectSingleEvent>
    ): Flow<CreateProjectIntent> {
        return intentFlow.run {
            merge(
                filterIsInstance<CreateProjectIntent.ChangeProjectName>()
                    .onEach {
                        Timber.e(it.toString())
                    }
                    .shareIn(scope, SharingStarted.WhileSubscribed()),
...

reducer에서는 이전 상태를 가지고 비즈니스 로직을 수행하게 되는데, 이 과정에서 상태 변경이 일어날 수 있게 됩니다.

 .scan(initialViewState) { prevState, mutate ->
	 registerReducer()!!.invoke(mutate, prevState)
 }

reducer를 통해 새로운 상태를 정의하는 과정입니다.

override fun invoke(action: BaseIntent, state: CreateProjectState): CreateProjectState {
    var newState = state
    when (action) {
        is CreateProjectIntent.ChangeProjectName -> {
            newState = newState.copy(
                projectName = action.name
            )
        }

.catch { processError(it) } 과정에서 발생하는 에러를 캐치하고, 처리할 수 있습니다.

또한 StateFlow로 변환하여 View에서 ViewState를 관찰하고, update 할 수 있는 환경을 제공합니다.



참고 : https://medium.com/@maryangmin/data-uni-directional-architecture-in-android-데이터-단방향-구조-fd7ef26e80fc

Github : https://github.com/YAPP-Github/21st-ALL-Rounder-Team-3-Android/tree/develop/core/src/main/java/com/yapp/timitimi/redux

참고 :https://maryang-developer.medium.com/data-uni-directional-architecture-in-android-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8B%A8%EB%B0%A9%ED%96%A5-%EA%B5%AC%EC%A1%B0-fd7ef26e80fc
Feat : @jshme

profile
android_developer
post-custom-banner

3개의 댓글

comment-user-thumbnail
2023년 5월 11일

내공냠냠

답글 달기
comment-user-thumbnail
2023년 6월 2일

UAF에 대해선 관심 없나요?

답글 달기
comment-user-thumbnail
2024년 4월 18일

좋은 글 감사합니다~

답글 달기