깨알 개념/Kotlin

[Kotlin] Coroutines - Structured Concurrency

interfacer_han 2024. 2. 16. 12:11

#1 Unstructured Concurrency

코루틴 코드를 설계할 때 조심해야 하는 점이 있다. 다음 코드를 보자.

 

#1-1 코드

import kotlinx.coroutines.*

class MyClass {
    suspend fun getMyCount(): Int {
        var count = 0
        CoroutineScope(Dispatchers.IO).launch {
            delay(3000)
            count += 50
        }
        return count
    }
}

suspend fun main() {
    println(MyClass().getMyCount())
}

/* 출력 결과
0
*/

getMyCount()라는 Suspending Function은 count += 50을 수행하는 하위 Suspending Function을 가지고 있다. 프로그래머는 getMyCount()를 호출하며, 그 하위 Suspending Function까지 제대로 동작하길 바랬다. 그리고 그 결과로 50이 출력되는 것을 기대했다. 하지만, 실제로 출력된 것은 0이다. 이는 count에 += 50 하는 걸 기다리지(Suspend)하지 않았기 때문에 발생한 일이다.

 

Unstructured Concurrency는 하위 Suspending Function들이 전부 return(완료)되기 전에, 상위 Suspending Function이 return(완료)될 도 있는 구조를 말한다. Unstructured Concurrency는 위와 같이 프로그래머의 의도대로 작동하지 않는다는 문제도 있지만, 상위 Suspending Function가 종료되었음에도 하위 Suspending Function가 종료되지 않고 돌아간다는 문제도 있다. 이는 메모리를 낭비시키며, 예상불가능한 에러까지도 발생시킬 잠재성을 지닌다.

 

Structured Concurrency는 반대로, 상위 Suspending Function return(완료) 전에, 그 안의 모든 하위 Suspending Function들이 전부 return(완료)됨을 보장하는 구조다.

 

#1-2 코드 보완

import kotlinx.coroutines.*

class MyClass {
    suspend fun getMyCount(): Int {
        var count = 0

        val myJob = CoroutineScope(Dispatchers.IO).launch {
            delay(3000)
            count += 50
        }

        myJob.join()
        return count
    }
}

suspend fun main() {
    println(MyClass().getMyCount())
}

/* 출력 결과
50
*/

#1-1의 코드를 보완했다. 그저, 기다리면 된다. Job.join(), Deferred.await(), ReceiveChannel.receive() 함수를 사용한다.

 

하지만, 이 코드는 여전히 Structured Concurrency가 아니라, Unstructured Concurrency다. 왜냐하면, 구조가 변한 것은 아니기 때문이다. 어떤 클래스의 public 프로퍼티를 은밀하게 사용한다고 private 프로퍼티로 취급될 수 없는 것처럼, Job.join(), Deferred.await(), ReceiveChannel.receive() 등의 함수로 구조를 보완했다고 Unstructured Concurrency를 Structured Concurrency로 취급할 수는 없는 법이다.

 

그렇다면, Structured Concurrency는 대체 어떤 구조인가?

 

#2 Structured Concurrency (coroutineScope)

#2-1 CoroutineScope (대문자 C)

 

CoroutineScope

CoroutineScope Defines a scope for new coroutines. Every coroutine builder (like launch, async, etc.) is an extension on CoroutineScope and inherits its coroutineContext to automatically propagate all its elements and cancellation. The best ways to obtain

kotlinlang.org

우리가 잘 아는 그 CoroutineScope.

 

#2-2 coroutineScope (소문자 c)

 

coroutineScope

Creates a CoroutineScope and calls the specified suspend block with this scope. The provided scope inherits its coroutineContext from the outer scope, using the Job from that context as the parent for a new Job. This function is designed for concurrent dec

kotlinlang.org

Structured Concurrency는 바로 coroutineScope를 사용한 코드를 의미한다. coroutineScope의 c는 소문자다. 우리가 알던 그 CoroutineScope가 아니다. coroutineScope는 runBlocking처럼 그 안의 모든 Suspending Function을 기다린 후에 종료(완료)된다. 물론 coroutineScope은 비동기 함수고, runBlocking은 동기 함수라는 차이가 있다. CoroutineScope와 이름이 비슷해서 뭔가 싶지만, 그저 Job.join(), Deferred.await(), ReceiveChannel.receive()가 암시적으로 수행되는 함수라고 생각하면 된다. 즉, 프로그래머가 명시적으로 신경쓰지 않아도, 하위 Suspending Function의 완료를 보장한다는 말이다. 프로그래머는 무슨 짓을 해도 하위 Suspending Function의 완료 전에 coroutineScope를 종료시킬 수 없다. 즉, 일종의 무결성이 보장되는 구조가 만들어지는 것이다.

 

#2-3 코드

import kotlinx.coroutines.*

class MyClass {
    suspend fun getMyCount(): Int {
        var count = 0

        coroutineScope {
            launch {
                delay(5000)
                count += 50
            }

            launch {
                delay(3000)
                count += 30
            }
        }

        return count
    }
}

suspend fun main() {
    println(MyClass().getMyCount())
}

/* 출력 결과
80
*/

암시적으로 수행된 Suspending Function들 덕에, 프로그래머는 의도한 값을 얻었다. 덤으로, 코드량도 줄였다.

 

#2-4 주의할 점 (coroutineScope의 범위)

...

coroutineScope {
    launch(Dispatchers.IO) {
        ...
    }
}

...

함수 coroutineScope { ... }는 범위 지정 함수와 비슷하다. 이 범위(Scope, 영역)에서 CoroutineContext는 괄호 밖의 것을 상속받아 쓴다. 그래서 CoroutineContext를 변경하고 싶으면, Coroutine builder에 위와 같이 CoroutineContext를 매개변수로 주면 된다.

 

coroutineScope은 Structured Concurrency를 보장한다고 했다. 게다가 범위 지정 함수는 아니어도 비슷한 기제가 있다고도 했다. 이 말은, coroutineScope가 보장하는 Structured Concurrency는 오직 coroutineScope의 범위만으로 한정된다는 얘기다. 다음 코드를 보자.

 

import kotlinx.coroutines.*

class MyClass {
    suspend fun getMyCount(): Int {
        var count = 0

        coroutineScope {
            CoroutineScope(Dispatchers.Default).launch {
                delay(5000)
                count += 50
            }

            CoroutineScope(Dispatchers.Default).launch {
                delay(3000)
                count += 30
            }
        }

        return count
    }
}

suspend fun main() {
    println(MyClass().getMyCount())
}

/* 출력 결과
0
*/

출력 결과가 80이 아닌 0이다. 새롭게 CoroutineScope를 만들었기 때문이다. 저 영역(Scope) coroutineScope의 Structured Concurrency가 보장되지 않는다. coroutineScope 안에서 또 다른 CoroutineScope를 사용하지 않게 주의하자.

 

#3 요약

마치 private 접근지정자처럼, 'c'oroutineScope는 일종의 무결성이 유지되는 영역을 만든다.