SharingStarted 전략 완전 정리 — WhileSubscribed, Eagerly, Lazily


1. SharingStarted란?

stateInshareIn은 Cold Flow를 Hot Flow로 변환하는 함수입니다.
이때 언제 업스트림 Flow 수집을 시작하고 중단할지를 결정하는 것이 SharingStarted 전략입니다.

val hotFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000), // 전략 지정
    initialValue = emptyList()
)

전략 선택이 잘못되면 불필요한 리소스 낭비 또는 UI 업데이트 누락이 발생할 수 있습니다.


2. 세 가지 전략 한눈에 비교

전략 업스트림 시작 업스트림 중단 재구독 시
WhileSubscribed 첫 구독자 등장 시 마지막 구독자 해제 후 N ms 새 업스트림 시작
Eagerly scope 생성 즉시 scope 취소 시 기존 값 유지
Lazily 첫 구독자 등장 시 scope 취소 시 기존 값 유지

3. WhileSubscribed

동작 원리

구독자 등장 → 업스트림 시작
마지막 구독자 해제 → (stopTimeout ms 후) 업스트림 중단
새 구독자 등장 → 업스트림 재시작
SharingStarted.WhileSubscribed(
    stopTimeoutMillis = 5000,       // 구독자 0명 후 업스트림 중단까지 대기 시간 (기본 0)
    replayExpirationMillis = Long.MAX_VALUE // 캐시 만료 시간 (기본 무한)
)

실전 사용

class MainViewModel(
    private val userRepository: UserRepository
) : ViewModel() {

    val userList: StateFlow<List<User>> = userRepository.getUsersFlow()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

stopTimeoutMillis = 5000 인 이유

화면 회전 시 Activity가 destroy → recreate 됩니다.
이 과정에서 구독자가 잠깐 0명이 되는데, 5초 여유를 주면 업스트림을 불필요하게 재시작하지 않습니다.

Activity destroy → 구독자 0명 → (5초 카운트) → 5초 내 재구독 → 업스트림 유지!
                                               → 5초 초과       → 업스트림 중단

언제 사용


4. Eagerly

동작 원리

scope 생성 즉시 → 업스트림 시작
구독자 유무 무관 → 항상 실행
scope 취소 → 업스트림 중단
val hotFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Eagerly,
    initialValue = 0
)

특징

언제 사용

// 예: 앱 시작 시 위치 추적
val locationFlow = locationSource.asFlow()
    .stateIn(
        scope = applicationScope,    // Application 레벨 scope
        started = SharingStarted.Eagerly,
        initialValue = Location.UNKNOWN
    )

5. Lazily

동작 원리

첫 구독자 등장 → 업스트림 시작
구독자가 0명이 되어도 → 업스트림 유지 (scope 취소까지)
scope 취소 → 업스트림 중단
val hotFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Lazily,
    initialValue = 0
)

Eagerly vs Lazily 비교

Eagerly: scope 생성 즉시 시작 → 구독자 없어도 실행
Lazily:  첫 구독자 등장 시 시작 → 이후 구독자 없어도 계속 실행

언제 사용


6. stateIn vs shareIn에서의 전략 차이

// stateIn: StateFlow 생성 (항상 최신 값 1개 보관)
val state: StateFlow<T> = flow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000),
    initialValue = defaultValue
)

// shareIn: SharedFlow 생성 (replay 설정으로 캐시 조절)
val shared: SharedFlow<T> = flow.shareIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000),
    replay = 1
)

stateIn은 내부적으로 replay = 1인 SharedFlow이므로 나중에 구독해도 최신 값을 받습니다.
shareInreplay = 0이면 구독 전 이벤트는 완전히 누락됩니다.


7. 커스텀 SharingStarted

특수한 경우 직접 구현할 수도 있습니다.

object SharingStartedWhileVisible : SharingStarted {
    override fun command(subscriptionCount: StateFlow<Int>): Flow<SharingCommand> =
        subscriptionCount
            .map { count ->
                if (count > 0) SharingCommand.START
                else SharingCommand.STOP_AND_RESET_REPLAY_CACHE
            }
            .distinctUntilChanged()
}

8. 전략 선택 가이드

상황 권장 전략
Android ViewModel + 네트워크/DB WhileSubscribed(5000)
앱 시작 시 즉시 데이터 필요 Eagerly
첫 구독 전까지 지연, 이후 계속 유지 Lazily
이벤트 누락 없이 재구독 WhileSubscribed + replay
백그라운드 리소스 최소화 WhileSubscribed

9. 실전 예제 — ViewModel 패턴

class ProductViewModel(
    private val repo: ProductRepository
) : ViewModel() {

    // 네트워크 요청: WhileSubscribed로 리소스 절약
    val products: StateFlow<List<Product>> = repo.getProductsFlow()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )

    // 로컬 DB: Lazily로 첫 접근 시에만 시작
    val favorites: StateFlow<List<Product>> = repo.getFavoritesFlow()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.Lazily,
            initialValue = emptyList()
        )
}
// Fragment에서 수집
viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.products.collect { products ->
            adapter.submitList(products)
        }
    }
}

10. 정리

대부분의 Android 앱에서는 WhileSubscribed(5000) + stateIn이 가장 안전하고 효율적인 선택입니다.



Related Posts