[Kotlin] Coroutine 연습 문제
Android/Kotlin

[Kotlin] Coroutine 연습 문제


Coroutine을 공부하면서, 문서를 꼼꼼히 읽어봐도 실전에서는 분명 헷갈리는 부분이 적지 않았다.
그래서 다시 Unit Test를 통하여 연습하던 중에, 이를 문제 - 정답 형식의 문서로 작성해두면 좋을 것 같아 남기게 되었다.

🤔 Coroutine 연습문제

Unit Test 내에서의 결과이기 때문에 상대적으로 긴 런타임을 가지는 실제 상황과는 다른 결과가 나타날 수 있습니다.

그러나 오히려 그렇기에 공부하기에는 더 확실한 방법이므로, 출력의 보장 결과 여부는 Unit Test Runtime 기준으로 합니다.


🕐 문제 1. 출력 결과를 예상하시오.

class ExampleUnitTest {

    @Test
    fun assert_test() {
        runBlocking {
            println("start")
            /** A Scope */
            CoroutineScope(Dispatchers.IO).launch {
                println("A Scope : I'am CoroutineScope, start!")
                for (item in 0..1000) {
                    println("A Scope : $item")
                }
            }
            /** B Scope */
            CoroutineScope(Dispatchers.IO).launch {
                println("B Scope : I'am CoroutineScope, start!")
                for (item in 0..1000) {
                    println("B Scope : $item")
                }
            }
        }
    }

}

⭕ = 정상적으로 끝까지 출력, ❌ = 끝까지 모두 출력되는 것은 보장 안됨

① A,B 모두 ⭕

② A Scope ⭕, B Scope ❌. 서로 동시에 출력되지 않는다.

③ B Scope ⭕, A Scope ❌. 서로 동시에 출력되지 않는다.

④ A Scope ⭕, B Scope ❌. 서로 동시에 출력될 수 있다.

⑤ B Scope ⭕, A Scope ❌. 서로 동시에 출력될 수 있다.

⑥ A,B 모두 ❌

 

아래에 정답이 있습니다.

 

정답은 ⑥

 

runBlocking은 내부 로직이 끝날 동안 외부에게 기다리도록 하게한다.

그에 반해, CoroutineScope는 비동기적으로 돌아간다. runBlocking 과는 별개로 또다른 쓰레드,

A scope와 B Scope 2개가 비동기적으로 돌고 있는 것이다. 따라서 CoroutineScope의 종료는 테스트

코드 런타임의 관심밖이므로 runBlocking의 내부 로직이 마친 직후에 런타임이 종료가 되면 해당

프로세스는 종료되기 때문에 A와 B 모두 정상 출력이 보장되지 못 한다.

🕐 문제 2. 출력 결과를 예상하시오.

class ExampleUnitTest {
    @Test
    fun assert_test() {
        runBlocking {
            println("start")
            /** A Scope */
            val job = CoroutineScope(Dispatchers.IO).async {
                println("A Scope : I'am CoroutineScope, start!")
                for (item in 0..5000) {
                    println("A Scope : $item")
                }
            }
            /** B Scope */
            CoroutineScope(Dispatchers.IO).launch {
                println("B Scope : I'am CoroutineScope, start!")
                for (item in 0..10000) {
                    println("B Scope : $item")
                }
            }
            job.await()
        }
    }
}

⭕ = 정상적으로 끝까지 출력, ❌ = 끝까지 모두 출력되는 것은 보장 안됨

① A,B 모두 ⭕

② A Scope ⭕, B Scope ❌. 서로 동시에 출력되지 않는다.

③ B Scope ⭕, A Scope ❌. 서로 동시에 출력되지 않는다.

④ A Scope ⭕, B Scope ❌. 서로 동시에 출력될 수 있다.

⑤ B Scope ⭕, A Scope ❌. 서로 동시에 출력될 수 있다.

⑥ A,B 모두 ❌

 

아래에 정답이 있습니다.

 

정답은 ④

 

CoroutineScope의 async/await을 주문하였으므로 해당 Scope의 출력은 보장된다. 그러나 B Scope는

그렇지 못하다. 또한 둘 다 CoroutineScope를 사용하였고, A Scope의 await 함수가 B Scope의 호출

이후 선언되었기 때문에, 서로 동시에 출력될 수 있다.

🕐 문제 3. 출력 결과를 예상하시오.

class ExampleUnitTest {
    @Test
    fun assert_test() {
        runBlocking {
            println("start")
            /** A Scope */
            CoroutineScope(Dispatchers.IO).async {
                println("A Scope : I'am CoroutineScope, start!")
                for (item in 0..5000) {
                    println("A Scope : $item")
                }
            }.await()
            println("A Scope End!")
            /** B Scope */
            CoroutineScope(Dispatchers.IO).launch {
                println("B Scope : I'am CoroutineScope, start!")
                for (item in 0..5000) {
                    println("B Scope : $item")
                }
            }
        }
    }
}

⭕ = 정상적으로 끝까지 출력, ❌ = 끝까지 모두 출력되는 것은 보장 안됨

① A,B 모두 ⭕

② A Scope ⭕, B Scope ❌. 서로 동시에 출력되지 않는다.

③ B Scope ⭕, A Scope ❌. 서로 동시에 출력되지 않는다.

④ A Scope ⭕, B Scope ❌. 서로 동시에 출력될 수 있다.

⑤ B Scope ⭕, A Scope ❌. 서로 동시에 출력될 수 있다.

⑥ A,B 모두 ❌

 

아래에 정답이 있습니다.

 

정답은 ②

 

B Scope가 실행되기 이전 A Scope에서 await을 요청하였으므로 A Scope 내부 로직이 종료 될

때까지 기다린 후, 다음 로직이 시작된다. 따라서 동시에 출력되지 않을 뿐더러 B Scope의

종료까지는 보장되지 않는다.

🕐 문제 4. 출력 결과를 예상하시오.

class ExampleUnitTest {
    @Test
    fun assert_test() {
        runBlocking {
            println("start")
            /** A Scope */
            withContext(Dispatchers.IO) {
                println("A Scope : I'am withContext, start!")
                for (item in 0..5000) {
                    println("A Scope : $item")
                }
            }
            println("withContext End!")
            /** B Scope */
            CoroutineScope(Dispatchers.IO).launch{
                println("B Scope : I'am CoroutineScope, start!")
                for (item in 0..5000) {
                    println("B Scope : $item")
                }
            }
        }
    }
}

⭕ = 정상적으로 끝까지 출력, ❌ = 끝까지 모두 출력되는 것은 보장 안됨

① A,B 모두 ⭕

② A Scope ⭕, B Scope ❌. 서로 동시에 출력되지 않는다.

③ B Scope ⭕, A Scope ❌. 서로 동시에 출력되지 않는다.

④ A Scope ⭕, B Scope ❌. 서로 동시에 출력될 수 있다.

⑤ B Scope ⭕, A Scope ❌. 서로 동시에 출력될 수 있다.

⑥ A,B 모두 ❌

 

아래에 정답이 있습니다.

 

정답은 ②

 

정답은 문제 3번과 같은 ②. 이유는 withContext는 단순 coroutineContext의 변경일 뿐 같은 Scope내

에 있는 것이므로, runBlocking 내에서 도는 로직이기 때문이다. 그러나 문제 3번에 비해 이쪽이 좀

더 자연스럽다. async 직후 await를 쓰는 것과는 성능 차이가 거의 나지 않으나, 애초에 Coroutine

async/await 라는 것은 다른 Coroutine들도 있고 하지만 await하여 함께 동기할 작업을 하고 싶다는

것에 의의가 있다. 따라서 문제 2번과 같이, await가 뒤로 밀려있고 그 사이에 다른 Coroutine이 존재

하는 상황이라면 Best UseCase 이나, 문제 3번과 같이 async 직후 await를 쓰는 상황이라면

withContext로 교체하는 것이 낫다.

🕐 문제 5. 출력 결과를 예상하시오.

class ExampleUnitTest {

    @Test
    fun assert_test() {
        runBlocking {
            launch {
                println("launch 1")
            }
            println("2")
            coroutineScope {
                for(item in 4..100) {
                    println("cS1 : $item")
                }
            }
            CoroutineScope(Dispatchers.Default).launch{
                for(item in 4..100) {
                    println("CS : $item")
                }
            }
            launch {
                println("launch 2")
            }
            println("Hey!")
            coroutineScope {
                for(item in 4..100) {
                    println("cS2 : $item")
                }
            }
        }
    }
}

정답은 각자 생각해봅시다.

 

아래에 정답이 있습니다.

 

정답은

2
cS1 : 4
cS1 : 5
cS1 : 6
...
cS1 : 98
cS1 : 99
cS1 : 100
Hey! 
이후 CS와 cS2가 동시에 출력되며
마지막으로
launch 1
launch 2 

물론 위의 순서는 for문으로 인한 반복의 숫자가 4~ 100으로 적어서 그렇기 때문이다.

만약 for(item in 4..10000) 정도 되는 수치라면 아래와 같은 결과가 나온다

2
cS1 : 4
cS1 : 5
cS1 : 6
...
cS1 : 9998
cS1 : 9999
cS1 : 10000
Hey!
이후 CS와 cS2가 동시에 출력되며
그 사이에 launch 1 과 launch 2가 출력된다.

우선 launch ( 정확히는 CoroutineScope.launch)는 block 내부 로직을 suspending(지연)하여

실행시키기 때문에 즉각 시행되는 것들에 비해 조금 더 나중에 실행된다.

coroutineScope와 CoroutineScope의 차이를 명확히 알아야하는데, coroutineScope는 내부 로직을

감싸는 역할을 하여 구조화된 비동기화를 진행시킬 수 있게 하고, 그와 다르게 CoroutineScope는

단지 내부 로직을 비동기적으로 진행시킨다.

따라서 coroutineScope 내부는 비동기로 진행되지만 외부에서는 blocking 하듯이 작동한다.

coroutineScope는 Exception 상황 발생 등, 특수 상황을 해당 내부 Scope 내에서 처리하는 등,

구조적으로 비동기쌍을 묶는 경우에 용이하다.

🍊 Structured Concurrency

🍊 coroutineScope VS CoroutineScope

🍊 coroutineScope CoroutineScope