📌 시험지 링크: https://android-exam25.gdg.kr/
지난 글에 이어서 이번에는 코루틴 영역의 일부 문제에 대한 해설을 작성해보았다.
⭐️ 필자가 스스로 학습하고 정리한 내용을 포함하고 있으므로, 틀린 내용이 있다면 꼭 댓글로 알려주시길 바랍니다!
CoroutineContext에 대한 설명으로 옳은 것만을 보기에서 있는대로 고른 것은?
ㄱ. CoroutineContext를 구성하는 element는 CoroutineContext 타입이다. (O)
ㄴ. 코루틴 빌더로 생성된 자식 코루틴은 부모 코루틴의 CoroutineContext를 모두 상속 받는다. (X)
ㄷ. GlobalScope는 단 하나의 CoroutineContext를 가지며, 이 CoroutineContext는 프로그램이 종료되기 전까지 소멸되지 않는다. (O)
정답: ㄱ, ㄷ
보기 ㄱ은 아래 코드를 보면 옳은 설명이라는 걸 알 수 있다.
/**
* Persistent context for the coroutine. It is an indexed set of [Element] instances.
* An indexed set is a mix between a set and a map.
* Every element in this set has a unique [Key].
*/
@SinceKotlin("1.3")
public interface CoroutineContext {
// ...
/**
* An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
*/
public interface Element : CoroutineContext {
/**
* A key of this coroutine context element.
*/
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}
}
다음으로, 보기 ㄴ을 살펴보자.
자식 코루틴은 부모 코루틴으로부터 CoroutineName, CoroutineDispatcher, CoroutineExceptionHandler 같은 주요 구성요소를 상속 받지만, Job은 상속 받지 않는다.
그 이유는 Job으로 코루틴을 제어할 수 있는데, 자식 코루틴이 부모 코루틴의 Job까지 상속 받으면 개별 코루틴을 제어하기 어렵기 때문이다. 따라서 보기 ㄴ은 틀린 설명이다.
마지막으로, 보기 ㄷ은 옳은 설명이다.
@DelicateCoroutinesApi
public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
다음 테스트 코드를 통과시키기 위해 expected에 할당되어야 하는 값으로 적절한 것을 고르시오.
class CoroutineTest {
@Test
fun coroutineScopeTest() = runTest {
// given
val actual: StringBuilder = StringBuilder()
// when
val deferred = async {
delay(500)
actual.append(1)
}
launch {
delay(200)
actual.append(2)
}
coroutineScope {
launch {
delay(300)
actual.append(3)
}
actual.append(4)
} // 스코프 내의 코루틴 작업 끝날 때까지 대기
deferred.await() // 결과 반환될 때까지 대기
actual.append(5)
// then
val expected = "_____" // 42315
assertEquals(expected, actual.toString())
}
}
delay 시간에 따라 숫자를 하나씩 붙이면 42315가 나온다.
flatMapLatest는 flow를 최신 데이터만 이용해 새로운 flow로 변환해주는 함수이다.
flatMapLatest를 사용하면 flow에서 발행된 데이터를 변환하는 도중 새로운 데이터가 발행될 경우, 기존의 변환 로직을 취소하고 새로운 데이터로 변환을 수행한다.
collectLatest의 경우 먼저 발행된 데이터를 처리하는 도중 새로운 데이터가 들어올 경우, 이전 데이터의 처리를 취소하고 새로운 데이터를 처리하는데 flatMapLatest는 collectLatest와 동작이 매우 유사하다.
이제, 30번 문제를 살펴보자.
다음 코드에 대한 설명으로 틀린 것은?
suspend fun main() {
coroutineScope {
val stateFlow = MutableStateFlow(1)
val flow = flow {
emit(1)
delay(1000L)
emit(2)
delay(1000L)
emit(3)
}.flatMapLatest { value ->
stateFlow.map {
value + it
}
}
launch {
delay(999L)
stateFlow.emit(2)
delay(999L)
stateFlow.emit(3)
}
flow.collect { value ->
println("value: $value")
}
}
}
출력 값: 2, 3, 4, 5, 6
flatMapLatest에 걸어둔 StateFlow에 값을 넘기더라도, flow{} 부터 다시 발행되지는 않는다.
flatMapLatest는 가장 최근에 발행된 데이터에 대한 변환 작업을 수행하는 함수다.
하지만 예외적으로 exception이 발생하고, retry를 걸었다면 flow {}부터 발행될 수도 있다.
📌 아래 변형 코드들은 이 블로그에서 참고했다!
suspend fun main() {
coroutineScope {
val sharedFlow = MutableSharedFlow<Int>()
val flow = flow {
emit(1)
delay(1000L)
emit(2)
delay(1000L)
emit(3)
}.flatMapLatest { value ->
sharedFlow.map {
value + it
}
}
launch {
delay(999L)
sharedFlow.emit(2)
delay(999L)
sharedFlow.emit(3)
}
flow.collect { value ->
println("value: $value")
}
}
}
출력 값: 3, 5
SharedFlow는 replayCache 설정을 하지 않으면, 이전에 방출된 값을 유지하지 않기 때문에 위와 같은 결과가 나온다.
suspend fun main() {
coroutineScope {
val sharedFlow = MutableSharedFlow<Int>()
val stateFlow = MutableStateFlow(false)
val flow = flow {
emit(1)
delay(1000L)
emit(2)
delay(1000L)
emit(3)
}
.flatMapLatest { value ->
sharedFlow.map {
value + it
}
}
.flatMapLatest { // 여기에 추가
stateFlow.filter { it }
}
launch {
delay(999L)
sharedFlow.emit(2)
delay(999L)
sharedFlow.emit(3)
stateFlow.value = true // 여기에 추가
}
flow.collect { value ->
println("value: $value")
}
}
}
출력 값: true
flow와 sharedFlow에서 데이터를 emit 해도, filter 함수에 의해 stateFlow가 true일 때만 최종적으로 emit 되기 때문에 위와 같은 결과가 나온다.
viewModelScope의 예외 전파 방식에 대해 아직 헷갈리는 부분이 있어서, 좀더 자세히 알아보고 풀이를 작성하도록 하겠다!