# Android Kotlin Flow 완벽 가이드 🌊
## 들어가며
Android 개발을 하다 보면 **실시간으로 변화하는 데이터**를 다뤄야 할 때가 많습니다. 예를 들어:
- 채팅 메시지가 실시간으로 업데이트되는 화면
- 위치 정보가 계속 변경되는 지도 앱
- 메모 목록이 자동으로 갱신되는 노트 앱
이런 상황에서 **Kotlin Flow**가 빛을 발합니다. Flow를 제대로 이해하고 사용하면 더 반응적이고 효율적인 앱을 만들 수 있어요.
## Flow란 무엇인가? 🤔
**Flow = 시간에 따라 연속적으로 데이터를 방출하는 스트림**
일반적인 함수는 한 번 호출하면 하나의 값을 반환하고 끝나지만, Flow는 여러 개의 값을 시간차를 두고 연속적으로 방출할 수 있습니다.
```kotlin
// 일반 함수 (한 번만)
fun getNumber(): Int {
return 42 // 한 번 반환하고 끝
}
// Flow (계속해서)
fun getNumberFlow(): Flow<Int> = flow {
emit(1) // 첫 번째 값
delay(1000)
emit(2) // 두 번째 값
delay(1000)
emit(3) // 세 번째 값
}
```
## Flow vs 일반 함수 비교 📊
### 일반 함수의 한계
```kotlin
fun getUserList(): List<User> {
return database.getAllUsers() // 현재 시점의 데이터만 반환
}
// 사용할 때
val users = getUserList() // 이 시점 이후 데이터 변경되면 모름
```
### Flow의 장점
```kotlin
fun getUserListFlow(): Flow<List<User>> {
return database.getAllUsers() // 데이터 변경 시마다 새로운 값 방출
}
// 사용할 때
userListFlow.collect { users ->
println("업데이트된 사용자 목록: $users")
// 데이터베이스가 변경될 때마다 자동으로 호출됨
}
```
## Flow의 생명주기 🔄
Flow는 **Cold Stream** 방식으로 동작합니다:
### 1. 생성 단계 (Cold)
```kotlin
val flow = repository.getAllBlocks() // 아직 아무것도 실행되지 않음
```
### 2. 구독 시작 (Hot)
```kotlin
flow.collect { data -> // 여기서 실제 작업 시작
println("받은 데이터: $data")
}
```
### 3. 데이터 방출
```kotlin
// 데이터 소스에서 변경이 발생할 때마다
emit(newData) // Room 등에서 자동으로 처리
```
### 4. 구독 취소
```kotlin
// Coroutine이 취소되거나 수집기가 종료되면 자동으로 정리
```
## 실제 사용 예시: 메모 앱 📝
메모 앱을 예로 Flow가 어떻게 활용되는지 살펴보겠습니다.
### Repository 계층
```kotlin
interface BlockRepository {
fun getAllBlocks(): Flow<List<Block>> // Flow 반환
suspend fun insertBlock(block: Block): Long
suspend fun deleteBlock(id: Long)
}
class BlockRepositoryImpl @Inject constructor(
private val blockDao: BlockDao
) : BlockRepository {
override fun getAllBlocks(): Flow<List<Block>> {
return blockDao.getAllBlocks()
.map { entities -> entities.map { it.toDomain() } }
}
}
```
### Room DAO
```kotlin
@Dao
interface BlockDao {
@Query("SELECT * FROM blocks ORDER BY updatedAt DESC")
fun getAllBlocks(): Flow<List<BlockEntity>> // Room이 Flow 지원
@Insert
suspend fun insertBlock(block: BlockEntity): Long
}
```
### ViewModel
```kotlin
@HiltViewModel
class BlockListViewModel @Inject constructor(
private val repository: BlockRepository
) : ViewModel() {
// Flow를 StateFlow로 변환하여 UI에서 사용
val blocks: StateFlow<List<Block>> = repository.getAllBlocks()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun addBlock(content: String) {
viewModelScope.launch {
val newBlock = Block(content = content)
repository.insertBlock(newBlock)
// Flow 덕분에 UI가 자동으로 업데이트됨!
}
}
}
```
### Compose UI
```kotlin
@Composable
fun BlockListScreen(
viewModel: BlockListViewModel = hiltViewModel()
) {
val blocks by viewModel.blocks.collectAsState()
LazyColumn {
items(blocks) { block ->
BlockItem(
block = block,
onDelete = { viewModel.deleteBlock(block.id) }
)
}
}
// blocks가 변경될 때마다 자동으로 리컴포지션 발생
}
```
## 실시간 업데이트의 마법 ✨
위 코드에서 주목할 점은 **데이터 변경 시 UI가 자동으로 업데이트**된다는 것입니다:
1. 사용자가 새 메모 추가 버튼 클릭
2. `addBlock()` 함수가 데이터베이스에 새 메모 저장
3. Room이 변경을 감지하고 새로운 데이터를 Flow에 방출
4. Compose UI가 자동으로 리컴포지션되어 새 메모 표시
**개발자가 수동으로 UI를 업데이트할 필요가 없습니다!**
## Flow 연산자 활용하기 🛠️
Flow는 다양한 연산자를 제공하여 데이터를 변환하고 조작할 수 있습니다.
### map - 데이터 변환
```kotlin
repository.getAllBlocks()
.map { blocks ->
blocks.filter { !it.content.isBlank() } // 빈 메모 제거
}
.collect { filteredBlocks ->
// 필터링된 결과 사용
}
```
### combine - 여러 Flow 결합
```kotlin
class SearchViewModel @Inject constructor(
private val repository: BlockRepository
) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
// 메모 목록과 검색어를 결합하여 검색 결과 생성
val searchResults = combine(
repository.getAllBlocks(),
searchQuery
) { blocks, query ->
if (query.isEmpty()) {
blocks
} else {
blocks.filter {
it.content.contains(query, ignoreCase = true)
}
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = emptyList()
)
fun updateSearchQuery(query: String) {
_searchQuery.value = query
}
}
```
### debounce - 입력 지연 처리
```kotlin
// 사용자가 타이핑을 멈춘 후 300ms 후에 검색 실행
searchQuery
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { query ->
repository.searchBlocks(query)
}
.collect { results ->
// 검색 결과 처리
}
```
## StateFlow vs Flow 🔄
### Flow
- Cold Stream: 구독할 때만 활성화
- 상태를 유지하지 않음
- 구독자마다 독립적인 스트림
### StateFlow
- Hot Stream: 항상 활성화 상태
- 현재 상태를 유지
- 마지막 값을 즉시 방출
- Compose와 완벽한 호환성
```kotlin
// Flow 사용
val dataFlow: Flow<List<Item>> = repository.getData()
// StateFlow 사용 (UI에 권장)
val dataState: StateFlow<List<Item>> = repository.getData()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = emptyList()
)
```
## 메모리 관리와 성능 최적화 ⚡
### 자동 구독 취소
```kotlin
@Composable
fun MyScreen() {
val data by viewModel.data.collectAsState()
// Composable이 사라지면 자동으로 구독 취소
// 메모리 누수 방지
}
```
### SharingStarted 전략
```kotlin
val data = repository.getData()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(
stopTimeoutMillis = 5000, // 5초 후 구독 정지
replayExpirationMillis = 0 // 재구독 시 마지막 값만 재생
),
initialValue = emptyList()
)
```
### conflate - 백프레셔 처리
```kotlin
repository.getFrequentUpdates()
.conflate() // 빠른 업데이트 시 최신 값만 유지
.collect { latestValue ->
// 최신 값만 처리
}
```
## 에러 처리 🚨
```kotlin
repository.getAllBlocks()
.catch { exception ->
// 에러 발생 시 처리
emit(emptyList()) // 기본값 방출
logError(exception)
}
.collect { blocks ->
// 정상 데이터 또는 기본값 받음
}
```
## 실무에서 주의할 점 ⚠️
### 1. Flow vs suspend 함수 구분
```kotlin
// ✅ 올바른 사용
fun getAllBlocks(): Flow<List<Block>> // 실시간 업데이트 필요
suspend fun getBlockById(id: Long): Block? // 한 번만 조회
// ❌ 잘못된 사용
suspend fun getAllBlocks(): Flow<List<Block>> // suspend 불필요
```
### 2. Context Switching 주의
```kotlin
repository.getAllBlocks()
.flowOn(Dispatchers.IO) // IO 작업은 IO 디스패처에서
.collect { blocks ->
// Main 스레드에서 UI 업데이트
}
```
### 3. 무한 재시도 방지
```kotlin
repository.getData()
.retry(3) { exception ->
exception is NetworkException // 특정 예외만 재시도
}
.collect { data ->
// 최대 3번 재시도 후 데이터 받음
}
```
## 마무리 🎯
Flow는 Android 앱에서 **반응형 프로그래밍**을 구현하는 핵심 도구입니다.
**Flow의 핵심 장점:**
- ✅ 실시간 데이터 업데이트
- ✅ 메모리 효율적 관리
- ✅ 자동 구독 취소
- ✅ 풍부한 연산자 지원
- ✅ Compose와 완벽한 호환성
**언제 Flow를 사용해야 할까요?**
- 데이터베이스 변경 사항을 실시간으로 반영해야 할 때
- 사용자 입력에 반응하는 검색 기능
- 위치, 센서 데이터 등 지속적으로 변경되는 데이터
- 네트워크 상태나 연결 상태 모니터링
Flow를 제대로 활용하면 더 반응적이고 사용자 경험이 뛰어난 Android 앱을 만들 수 있습니다.
다음 프로젝트에서 Flow를 도입해보세요! 🚀
---
## 참고 자료 📚
- [Kotlin Flow 공식 문서](https://kotlinlang.org/docs/flow.html)
- [Android 개발자 가이드 - Flow](https://developer.android.com/kotlin/flow)
- [Jetpack Compose State 가이드](https://developer.android.com/jetpack/compose/state)