grep

Android

Kotlin Flow를 통한 단방향 데이터 스트림 설계서

Jonas여기어때

2025년 7월 8일

원문에서 보기 ↗

Android 여기어때 앱의 상품정보 화면은 사용자에게 정보를 제공하기 위해 다양한 데이터를 조합하여 데이터를 수집 후 단방향으로 화면을 그리고 있습니다. 이때 조합해야 할 정보가 많을수록 복잡도는 같이 올라갑니다. 여기어때 안드로이드 팀은 Flow를 활용하여 어떻게 복잡도를 극복했는지 같이 알아보도록 하겠습니다.

왜 Flow인가?

여기어때 안드로이드 앱은 사용자 상호작용이 실시간으로 이어지며, 그에 따라 UI는 끊임없이 변화하는 데이터를 받아 반응해야 합니다.

이러한 이벤트는 UI 스레드에서 동기적으로 처리될 수 없으며, 결국 비동기 흐름과 상태 관리가 핵심 과제가 됩니다.

Kotlin Flow는 이러한 흐름을 설계하고, 추적 가능하고, 반응적으로 만드는 도구 중 최선의 선택이 됩니다.

명령형 구조 vs 데이터 스트림 방식

기존 명령형 구조의 한계

일반적인 명령형 프로그래밍에서는 상태를 var로 선언하고 직접 갱신합니다.

var isLoading = false
var calendar: CalendarData? = null

이로 인해 복잡성이 높아지고 유지보수성이 떨어지게 됩니다.

데이터 스트림 방식의 장점

Kotlin Flow 기반의 함수형 스타일 에서는 상태를 Flow, StateFlow, SharedFlow와 같은 Flow 를 통해 데이터 흐름 안에서 직접 관리합니다.

val flow = calendarFlow
        .flatMapLatest { calendarData ->
            ...
        }

결론

데이터 스트림 방식은 상태 관리를 스트림 내부로 집중시켜, 상태 변화의 흐름과 원인을 명확히 파악할 수 있게 하며, 동시성과 복잡성 문제를 효과적으로 줄여줍니다.

상세 화면을 통해 알아보는 실전 케이스

여기어때 해외 숙소 상세 화면입니다. 상세 화면을 서버에 호출을 하기 위해서는 다음과 같은 변동성이 있는 정보들이 필요합니다.

위 3가지 변동성 정보를 가지고 해외 숙소 화면을 그리는데 다음과 같은 데이터 흐름이 필요 해 보입니다.

실전 케이스를 통해 문제와 해결 방법을 알아보도록 하겠습니다.

실전 케이스 — shareIn

문제: Cold Flow의 반복 실행

val flow = flow {
    println("API 호출")
    emit(fetchData())
}

flow.collect() // API 호출
flow.collect() // 또 fetchData API 호출 발생!!!

Flow는 기본적으로 Cold Stream 이므로 collect()할 때마다 fetchData() 가 실행이 됩니다. 하지만 해외숙소 상세 화면은 첫 API 호출 결과를 가지고 여러곳에서 연동 처리를 해야 합니다. 예를 들면 화면이 API 호출 이후 리뷰, 쿠폰 모듈을 비동기로 호출하여 모듈을 다시 그리는 구조이므로 한번의 호출을 통해 다음 비동기 작업에 알맞은 Hot Stream 을 선택해야 합니다.

해결: shareIn으로 Hot Flow로 전환

// PDP Api 응답
// 진입 시 캘린더 값을 받고 나서 API 응답 처리를 함
private val pdpApiFlow = initCalendarFlow
   .flatMapLatest { calendarData ->
      foreignBuildingDetailUseCase
       .getForeignPdp(
          placeId = placeId,
          foreignCalendarData = calendarData,
      )
      ...
    }
      .shareIn(
        scope = viewModelScope,
        started = SharingStarted.Eagerly,
        replay = 1,
    )

언제 사용하는가?

실전 케이스 — merge vs combine

merge: 독립된 이벤트(Flow) 를 하나로 합쳐야 할 때

merge(
   calendarUpdateFlow, // 날짜/인원
   isLoginFlow
     .distinctUntilChanged() // 로그인 상태가 변할 때마다 화면 전체 갱신을 해야함.
     .map { foreignScheduleUseCase.currentForeignCalendarData },
)

combine: 최신 상태를 조합해서 계산할 때

combine(
   // foreignPdpDto 값이 업데이트 될때 마다 호출 (init, refresh)
   foreignBuildingDetailUseCase.foreignPdpFlow,
   // 쿠폰 보기 값이 업데이트 될때 마다 호출
    isApplyCoupon,
) { dto, activatedCoupon -> dto to activatedCoupon }

결론

etc. 실전 케이스 — flatMapLatest

해외 숙소 상세 화면에서는 다양한 상태 변화가 있다고 안내드렸습니다. 그럼 상태 변화를 통해 변화된 API 가 수신되는 동안 또다시 상태 변화가 생긴다면 이미 진행되고 있는 변화된 API는 어떻게 처리를 해야 할까요?? 가장 좋은 전략은 취소를 하고 새로 호출된 상태변화에 맞게 다시 API를 호출하는 것입니다. 기존 Flow를 flatMapLatest 변형을 통해 새로운 흐름의 취소 관리를 할 수 있게 됩니다.

Nesting 방식 — 유연하고 세밀한 제어

pdpApiFlow
   .flatMapLatest {
      merge(
         calendarUpdateFlow,
         isLoginFlow,
      ).flatMapLatest { calendar ->
         foreignBuildingDetailUseCase.getForeignPdp(
            placeId = placeId,
            foreignCalendarData = calendar,
         )
      }
   }

주의!! Chaining 방식

pdpApiFlow
  .flatMapLatest {
      merge(
       calendarUpdateFlow,
       isLoginFlow,
    )
   }
   .flatMapLatest { calendar ->
      foreignBuildingDetailUseCase.getForeignPdp(
        placeId = placeId,
        foreignCalendarData = calendar,
     )
   }

Summary

여기어때 해외 숙소 사례를 통해 Kotlin Flow의 사용방법을 알아봤습니다. Flow는 단순한 비동기 처리 도구를 넘어, 상태를 추적 가능하고 구조적으로 관리 할 수 있게 도와주는 데이터 흐름 설계 도구입니다.

특히,

이 세 가지 요소와 결합되면, Android 앱 개발에 최고의 선택이 될 것입니다.

감사합니다.