CoroutineScope : lifecycleScope, viewModelScope

thsamajiki·2022년 11월 7일
0

Coroutine

목록 보기
2/8

Coroutine 사용시 Memory Leak을 방지하기 위해 Activity에서는 Activity의 Lifecycle에 맞춰진 CoroutineScope을 사용해야 하며, ViewModel에서도 ViewModel의 Lifecycle에 맞춰진 CoroutineScope를 사용해야 한다.



Activity에서 사용해야 하는 lifecycleScope

Activity에서는 LifecycleOwner 인터페이스의 확장 프로퍼티로 선언된 lifecycleScope를 사용하면 된다.

public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
	get() = lifecycle.coroutineScope

이 Scope를 사용하면 Activity가 onDestroy될 때 lifecycleScope또한 해제되어 내부 Coroutine Job 들이 취소된다.

class MainActivity : ComponentActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContent {
			// UI 코드 작성
		}
		
        val stringFlow : Flow<String> = flow {
			for(i in 0..1000) {
				emit("integer: $i")
				delay(1000)
			}
		}

		lifecycleScope.launch {
			stringFlow.collect {
				println(it)
			}
		}
	}
}



ViewModel에서 사용해야 하는 viewModelScope

ViewModel에서 또한 Acitivty에서와 마찬가지로 ViewModel의 확장 프로퍼티로 선언된 viewModelScope를 쓰면 된다.

public val ViewModel.viewModelScope: CoroutineScope

ViewModel은 Fragment혹은 Activity의 Lifecycle에 binding되므로 viewModelScope는 binding된 lifecycle에 맞춰 viewModelScope내의 Job에 대한 취소를 하도록 한다.

예시 코드는 다음과 같다. ViewModel 클래스 내부에서는 Coroutine Job을 생성할 때 viewModelScope를 사용한다.

class MainViewModel() : ViewModel() {
	val stringFlow : Flow<String> = flow {
		for(i in 0..1000) {
			emit("integer: $i")
			delay(1000)
		}
	}

	fun collectStringFlow() {
		viewModelScope.launch {
			stringFlow.collect {
				println(it)
			}
		}
	}
}



lifecycleScope 사용의 한계점

하지만 lifecycleScope에도 여전히 한계점이 있다. 바로 onDestroy가 호출되면 Job이 cancel된다는 것이다. 이 말은 백그라운드로 내려가는 OnStop이 호출되면 여전히 Job이 수행됨을 뜻한다.

즉 Activity를 finish 시키는게 아니라. 홈버튼을 눌러 onStop만 되었더라면 Activity에서는 여전히 데이터를 수집하게 된다. 이러한 불필요한 동작은 백그라운드로 내려간 앱의 메모리 사용량을 증가시켜 시스템에 의한 Crash를 만들어낼 수도 있고 사용자가 원치 않는 데이터 사용이 일어나도록 만들 수도 있다.

lifecycleScope 문제 예시

아래와 같이 flow를 이용해 데이터를 collect하는 Coroutine Job이 있다고 해보자. 아래 코드에서는 lifecycleScope에서 flow에 대한 collect가 호출된다

class MainActivity : ComponentActivity() {
	private val stringFlow: Flow<String> = flow {
		for (i in 0..1000) {
			emit("integer: $i")
			delay(1000)
		}
	}

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		lifecycleScope.launch {
			stringFlow.collect {
				println(it)
			}
		}
	}
}

위의 코드를 실행시켰을 때 다음과 같이 동작이 일어난다

  • MainActivity에서 뒤로 가기를 눌렀을 때는 finish가 호출되어 onDestroy가 호출되어 아래와 같이 데이터 수집이 중단된다.
  • 하지만 만약 홈버튼을 눌러 앱이 그냥 백그라운드로 갔다면 어떻게 될까? 바로 onStop만 호출되어 데이터가 계속해서 백그라운드에서 수집된다.

onStart에서 Job을 생성 시작하고 onStop에서 Job을 cancel하기
우리는 이를 처리하기 위해 onStart에서 Job을 생성하고 onStop에서 Job을 cancel하는 방향으로 해결할 수 있다 아래 코드는 예시 코드이다. 예시를 위해 간단히 적어두었다

class MainActivity : ComponentActivity() {
	private val stringFlow: Flow<String> = flow {
		for (i in 0..1000) {
			emit("integer: $i")
			delay(1000)
		}
	}

	private var job : Job? = null

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		/ ..
	}

	override fun onStart() {
		super.onStart()
		job = lifecycleScope.launch {
        	stringFlow.collect {
				println(it)
			}
		}
	}

	override fun onStop() {
		super.onStop()
		job .cancel()
	}
}

코드를 해석하면 Coroutine Job을 onStart에서 생성하고 onStop에서 cancel하게 되면 아래와 같이 home 버튼을 눌렀을 때도 데이터 스트림 flow에 대한 collect가 중지된다.
위의 코드를 수행하면 아래와 같이 home버튼을 눌렀을 때도 데이터 수집이 중단됨을 확인할 수 있다



위 방식의 문제점과 해결책

위와 같이 매번 Coroutine Job을 생성하고 해제해주는 일은 보일러 플레이트 코드를 생성시킨다 특히 사람의 실수로 인해 하나의 Coroutine Job이라도 cancel하는 것을 깜박했을 경우 백그라운드에서 해당 작업이 계속 일어나 메모리 사용량이 높은 상태로 유지되어 시스템에 의해 App이 강제 종료될 수도 있다

*보일러 플레이트 코드 반복적으로 비슷한 형태를 띄는 코드

이러한 상황 방지를 위해 안드로이드에서는 onStart에서의 job의 생성과 onStop에서의 job의 cancel을 위한 API를 제공한다. 바로 lifecycleScope에서 쓸 수 있는 repeatOnLifecycle 함수이다.

lifecycleScope 내부에서 repeatOnLifecycle 사용해 collect 동작 최적화하기

코루틴의 새로운 버전에서는 위의 문제를 처리하기 위한 repeatOnLifecycle이라는 메서드가 생겼다. repeatOnLifecycle은 Activity가 포그라운드에 있을 때로 한정지어 특정 Lifecycle이 Trigger 되었을 때 동작하도록 만드는 block이다.

public suspend fun LifecycleOwner.repeatOnLifecycle(
	state: Lifecycle.State,
	block: suspend CoroutineScope.() > Unit
): Unit = lifecycle.repeatOnLifecycle(state, block)

repeatOnLifecycle 메서드의 주석에는 다음과 같이 쓰여있다

// Runs the block of code in a coroutine when the lifecycle is at least STARTED.
// The coroutine will be cancelled when the ON_STOP event happens and will

해석: LifecycleOwnerActivity Fragment의 Lifecycle이 최소 onStart가 되었을 때 블록에 있는 메서드들이 수행되며 onStop되었을 때는 코루틴 Job이 취소된다


풀어서 해석하면 onStart ~ onStop Lifecycle 범위 내에서 특정 Lifecycle.StateonStart onResume onPause onStop이 Trigger될 때마다 블록에 대한 Coroutine Job을 생성하고 onStop에서 Job을 취소하도록 만드는 것이다.


간단히 말하면 repeatOnLifecycle은 Coroutine Job이 생성될 LifecycleState을 인자로 받아 특정 생명주기가 포그라운드에 있을 때까지만 onStart ~ onStop 동작하는 Coroutine Job을 생성해주는 메소드이다. 즉 repeatOnLifecycle 블록 내부에 있는 flow에 대한 collect는 액티비티가 포그라운드에 있을 때만 진행된다.

class MainActivity : ComponentActivity() {
	private val stringFlow: Flow<String> = flow {
		for (i in 0..1000) {
			emit("integer: $i")
			delay(1000)
		}
	}
	
    override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		lifecycleScope.launch {
			repeatOnLifecycle(Lifecycle.State.STARTED) {
				stringFlow.collect {
					println(it)
				}
			}
		}
	}
}

따라서 위의 코드를 수행하면 홈버튼을 누를때(onStop 시킬 때), 뒤로가기 버튼을 누를 때(onDestroy 시킬 때) 모두 Coroutine Job이 cancel되는 것을 확인할 수 있다.

repeatOnLifecycle의 의의

repeatOnLifecycle을 활용하면 포그라운드에서 안전하게 Coroutine Job을 수행할 수 있게 된다 이번 글의 요점은 포그라운드에서만 작업되어야 하는 코루틴 블록은 repeatOnLifecycle을 활용해 작업해야 한다는 것이다.

백그라운드에서 작업이 일어나야 하는 일에서는 repeatOnLifecycle을 쓰면 안된다.

profile
안드로이드 개발자

0개의 댓글