Android
Kotlin Flow를 통한 단방향 데이터 스트림 설계서
Jonas여기어때
2025년 7월 8일
원문에서 보기 ↗Android 여기어때 앱의 상품정보 화면은 사용자에게 정보를 제공하기 위해 다양한 데이터를 조합하여 데이터를 수집 후 단방향으로 화면을 그리고 있습니다. 이때 조합해야 할 정보가 많을수록 복잡도는 같이 올라갑니다. 여기어때 안드로이드 팀은 Flow를 활용하여 어떻게 복잡도를 극복했는지 같이 알아보도록 하겠습니다.
왜 Flow인가?
여기어때 안드로이드 앱은 사용자 상호작용이 실시간으로 이어지며, 그에 따라 UI는 끊임없이 변화하는 데이터를 받아 반응해야 합니다.
- 버튼 클릭 → 네트워크 요청
- 응답 수신 → UI 렌더링
- 로딩 중 → 인디케이터 표시
- 실패 시 → 메시지 노출
이러한 이벤트는 UI 스레드에서 동기적으로 처리될 수 없으며, 결국 비동기 흐름과 상태 관리가 핵심 과제가 됩니다.
Kotlin Flow는 이러한 흐름을 설계하고, 추적 가능하고, 반응적으로 만드는 도구 중 최선의 선택이 됩니다.
명령형 구조 vs 데이터 스트림 방식
기존 명령형 구조의 한계
일반적인 명령형 프로그래밍에서는 상태를 var로 선언하고 직접 갱신합니다.
var isLoading = false
var calendar: CalendarData? = null
- 여러 필드 변수를 통한 상태 관리
- 상태 변경 로직이 이곳 저곳 흩어짐
- 멀티스레드 환경에서는 동기화 이슈 발생
- 상태 변화 흐름을 추적하기 어려움
이로 인해 복잡성이 높아지고 유지보수성이 떨어지게 됩니다.
데이터 스트림 방식의 장점
Kotlin Flow 기반의 함수형 스타일 에서는 상태를 Flow, StateFlow, SharedFlow와 같은 Flow 를 통해 데이터 흐름 안에서 직접 관리합니다.
val flow = calendarFlow
.flatMapLatest { calendarData ->
...
}
- 상태가 흐름 안에서만 생성되고 제어됨
- 불변성 기반으로 side-effect를 최소화
- 코루틴 기반으로 동시성에 강함
결론
데이터 스트림 방식은 상태 관리를 스트림 내부로 집중시켜, 상태 변화의 흐름과 원인을 명확히 파악할 수 있게 하며, 동시성과 복잡성 문제를 효과적으로 줄여줍니다.
상세 화면을 통해 알아보는 실전 케이스

여기어때 해외 숙소 상세 화면입니다. 상세 화면을 서버에 호출을 하기 위해서는 다음과 같은 변동성이 있는 정보들이 필요합니다.
- 로그인 상태
- 현재 달력의 날짜/인원 상태
- 쿠폰 더보기 상태
위 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,
)
- 화면에 진입시 캘린더 Flow 의 데이터를 수집하여 API 을 호출하는 구조.
- shareIn 을 통해 Cold → Hot으로 전환
- 여러 구독자에게 동일 데이터 공유
- replay로 최신 값 유지, (1개의 버퍼 성격을 가지며 collect 시 최신값을 꺼내 준다.)
언제 사용하는가?
- 하나의 Flow를 여러 곳에서 구독해야 할 때
- API 요청 등 비용이 큰 작업의 중복 실행을 막고 싶을 때
실전 케이스 — merge vs combine
merge: 독립된 이벤트(Flow) 를 하나로 합쳐야 할 때
merge(
calendarUpdateFlow, // 날짜/인원
isLoginFlow
.distinctUntilChanged() // 로그인 상태가 변할 때마다 화면 전체 갱신을 해야함.
.map { foreignScheduleUseCase.currentForeignCalendarData },
)
- Flow가 독립적으로 값을 발행 (예시에서는 호출 당시 API 에 필요한 캘린더 값을 전달하는 Flow 로 변환)
- 하나라도 emit 되면 바로 downstream으로 전달
- 날짜 변동 및 로그인이 바뀔때마다 현재의 날짜/인원 값을 downstream 으로 전달하므로 이미 그려진 화면에서 화면을 재갱신을 할때 사용이 용이함.
combine: 최신 상태를 조합해서 계산할 때
combine(
// foreignPdpDto 값이 업데이트 될때 마다 호출 (init, refresh)
foreignBuildingDetailUseCase.foreignPdpFlow,
// 쿠폰 보기 값이 업데이트 될때 마다 호출
isApplyCoupon,
) { dto, activatedCoupon -> dto to activatedCoupon }
- 모든 Flow가 최소 한 번 값을 낸 이후부터 작동
- 각 Flow의 최신 값을 조합해서 새로운 값을 생성
- 서버에서 받은 DTO 값과 쿠폰 더보기 상태를 조합하여 쿠폰의 상태를 변경 해야하는 컴퍼넌트를 갱신할때 사용에 용이함.
결론

etc. 실전 케이스 — flatMapLatest
해외 숙소 상세 화면에서는 다양한 상태 변화가 있다고 안내드렸습니다. 그럼 상태 변화를 통해 변화된 API 가 수신되는 동안 또다시 상태 변화가 생긴다면 이미 진행되고 있는 변화된 API는 어떻게 처리를 해야 할까요?? 가장 좋은 전략은 취소를 하고 새로 호출된 상태변화에 맞게 다시 API를 호출하는 것입니다. 기존 Flow를 flatMapLatest 변형을 통해 새로운 흐름의 취소 관리를 할 수 있게 됩니다.
Nesting 방식 — 유연하고 세밀한 제어
pdpApiFlow
.flatMapLatest {
merge(
calendarUpdateFlow,
isLoginFlow,
).flatMapLatest { calendar ->
foreignBuildingDetailUseCase.getForeignPdp(
placeId = placeId,
foreignCalendarData = calendar,
)
}
}
- 상위 계층에서 방출 된 값을 하위 계층에서도 접근이 가능.
- calendarUpdateFlow, isLoginFlow 값을 merge 하여 변화 된 값을 수신하여 getForeignPdp API 를 호출
- 이때 getForeignPdp 호출 응답이 길어 질 경우에 최상위 pdpApiFlow 값이 방출 된다면 취소가 되고 다시 merge flow 수신함.
- 대부분 새로운 이벤트를 통해 API 를 호출 하게 될 경우 최신값을 받길 기대하기 때문에 flatMapLatest 사용이 적합함.
주의!! Chaining 방식
pdpApiFlow
.flatMapLatest {
merge(
calendarUpdateFlow,
isLoginFlow,
)
}
.flatMapLatest { calendar ->
foreignBuildingDetailUseCase.getForeignPdp(
placeId = placeId,
foreignCalendarData = calendar,
)
}
- 단일 결과만 필요하며, 깔끔한 선언형 흐름을 보일 때 적합
- 하지만 위 경우 getForeignPdp 호출이 실행 되고 있는 동안 pdpApiFlow 이 방출이 된다면 실행 되고 있던 getForeignPdp 함수가 취소가 되지않음. (중복 실행이 발생)
- flatMapLatest 람다 내부에서 취소가 이루어지므로 Chaining 을 통한 외부에서는 취소가 되지 않는 점을 유의해야 함.
Summary
여기어때 해외 숙소 사례를 통해 Kotlin Flow의 사용방법을 알아봤습니다. Flow는 단순한 비동기 처리 도구를 넘어, 상태를 추적 가능하고 구조적으로 관리 할 수 있게 도와주는 데이터 흐름 설계 도구입니다.
특히,
- 단방향 흐름을 전제로 하는 MVI 구조
- 선언형 UI인 Jetpack Compose
- 코루틴 기반 비동기 처리
이 세 가지 요소와 결합되면, Android 앱 개발에 최고의 선택이 될 것입니다.
감사합니다.