본문 바로가기
Android App Architecture

UI Layer 안티패턴 실제 사례

by A Coder's Daydream 2025. 7. 14.
SMALL

1. UI Layer 안티패턴 실제 사례

 


1-1. 다른 프래그먼트/액티비티의 ViewModel을 직접 가져다 써 메모리 누수 발생 사례

// 프래그먼트A 뷰모델
class FragmentAViewModel : ViewModel() {
    val sharedData = MutableLiveData<String>()
}



// 프래그먼트B (잘못된 사용)
class FragmentB : Fragment() {

    // 직접 생성함 → 프래그먼트A에서 쓰던 ViewModel이 아님
    private val viewModel = FragmentAViewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
    }
}


🔥 왜 안티패턴인가?
- ViewModel은 LifecycleOwner 범위에 맞게 동작해야 하는데, 의도하지 않은 Lifecycle에서 관리되면 메모리 누수의 원인이 된다.

✅ 고친 방법
- 프래그먼트A와 프래그먼트B가 같은 액티비티 안에 있음
- 액티비티 범위 공유 ViewModel을 활용함 (activityViewModels())

// 프래그먼트B
class FragmentB : Fragment() {
    // 고친 부분
    private val viewModel: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    }
}

 



1-2. View가 ViewModel의 상태를 직접 변경 (상태 노출) 사례

viewModel.userName.value = "김짱구"


🔥 왜 안티패턴인가?
- 상태가 어디에서 어떻게 바꼈는지 추적이 어렵다.

✅ 고친 방법
- 상태를 Immutable 형태로만 노출

private val _state = MutableStateFlow(UiState())
val state: StateFlow<UiState> = _state




1-3. ViewModel init 블록에서 초기 데이터를 불러오는 것은 안티패턴이다?

class ProductViewModel : ViewModel() {
    private val _products = MutableStateFlow<List<Product>>(emptyList())
    val products = _products.asStateFlow()
    
    init {
        // 뷰모델 생성 도중 사이드 이펙트 발생 우려
        loadProducts()
    }
}


🔥 왜 안티패턴인가?
- ViewModel이 언제 생성될지 모르므로 사이드 이펙트도 예측 불가능한 시점에 실행될 우려가 있다.
- UI에서 데이터를 observe하지 않는 상태에서 에러 발생 우려가 있다.

✅ 권장하는 방법
- Cold Flow를 사용한 지연 초기화를 권장하고 있다. 방법은 아래와 같다.


1. Repository에서 Cold Flow 생성

class ProductRepository {
    
    // Cold Flow - 구독할 때만 API 호출
    fun getProducts(): Flow<List<Product>> = flow {
        val products = api.getProducts() // 구독 시에만 호출
        emit(products)
    }
}



2. ViewModel에서 Hot Flow로 변환

class ProductViewModel(
    private val repository: ProductRepository
) : ViewModel() {
    
    //Cold Flow를 Hot Flow로 변환
    val products: StateFlow<List<Product>> = repository.getProducts()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000), //핵심
            initialValue = emptyList()
        )
}



2-1. SharingStarted.WhileSubscribed가 하는 일

// SharingStarted.WhileSubscribed(5000)의 의미
//1. 구독자가 생기면 → Flow 시작
//2. 구독자가 없어지면 → 5초 후 Flow 중단
//3. 구독자가 다시 생기면 → Flow 재시작

@Composable
fun ProductScreen(viewModel: ProductViewModel = hiltViewModel()) {
    //화면이 보일 때 구독 시작 (API 호출)
    val products by viewModel.products.collectAsStateWithLifecycle()
    
    //화면이 사라질 때 구독 중단 (5초 후 API 호출 중단)
    
    LazyColumn {
        items(products) { product ->
            ProductItem(product)
        }
    }
}




2-2. 5_000은 어디서 온 숫자인가?

- 실제로 화면 터치, 키 누르기 등 이벤트에 5초 안에 응답하지 않으면 ANR이 트리거 된다.
  ANR의 기한이 5초이다.
- 따라서 마지막 구독자가 5초 이상 사라지면 이미 타임아웃이며, 데이터 Flow가 더이상 UI에 영향을 줄 수 없다.


2-3. 이 방법을 사용했을 시 장점

- 실제로 필요할 때만 데이터를 로딩한다. (지연 초기화)
- UI가 보일 때만 활성화된다.
- 불필요한 호출을 방지한다.
- Configuration Change에도 물론 안전하다.