brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Aug 28. 2023

코루틴 스코프 (CoroutineScope)

코틀린 코루틴 (5)

코루틴 스코프 (CoroutineScope)


CoroutineScope(이하 코루틴 스코프)는 처음부터 지금까지 살펴본 개념들을 이야기할 때 한 번도 빠지지 않고 언급되었다. 코루틴 스코프가 runBlocking의 수신 객체로 처음 언급되었을 때 말 그대로 코루틴의 범위이라고 했다.


그동안의 사용 형태를 보면 이제 이 말이 조금은 이해가 될 것이다. 코루틴의 범위라는 말을 조금 더 풀어보면 코루틴을 생성하고 실행할 수 있는 범위를 정의하고 관리한다고 할 수 있다. 코루틴을 생성하고 실행할 수 있는 것을 무엇이라고 했나? 코루틴 빌더라고 하였다.


이렇게 듣고 보니 코루틴 스코프는 코루틴 빌더와 밀접한 관계가 있어 보인다. 실제로 앞서 언급했던 코루틴 빌더인 launch는 코루틴 스코프의 확장 함수이다. launch를 살펴보면 다음과 같다.



launch, async 등 코루틴 스코프의 확장 함수들은 저마다 다른 특징을 가지고 있다. 그에 따라 필요한 상황에 따라 적절한 용도로 사용할 수 있다. (이에 대한 것은 각 코루틴 빌더를 하나씩 살펴볼 때 다시 이야기할 것이다.)


참고로 runBlocking은 코루틴 스코프의 확장 함수가 아니다. suspend 함수도 아닌 일반 함수이다. 만약 suspend 함수였다면 코루틴 스코프 외부에서 사용할 수 없었을 것이다. 따라서 runBlocking은 코루틴 스코프 외부, 즉 코루틴 환경이 아닌 곳에서 코루틴을 실행하기 위해 사용할 수 있다. 이러한 특징 때문에 main() 함수 또는 테스트 시 주로 사용한다고 한 것이다.


(공식적으로는 코루틴 빌더와 Scoping Function(이하 스코핑 함수)을 구분하고 있다. 그리고 모든 코루틴 빌더(launch, async 등)는 코루틴 스코프의 확장 함수라고 한다. 재미난 것은 코루틴 빌더와 스코핑 함수인 withContext() 모두 Builders.common.kt라는 같은 파일에 선언되어 있다는 점이다. 개인적으로는 하나의 용어로 통일되어도 괜찮지 않나 싶다. 그래서 앞서 코루틴 빌더라는 말에 runBlocking과 withContext도 포함시켰던 것이다. 공통점을 가진 개념들에 대해 빠르게 이해하는데 도움이 될 것으로 생각했기 때문이다. 용어는 매우 중요한 부분인데 완전히 엉뚱한 것도 아니고 그 의미가 충분히 통할 정도의 차이라고 보았다. 덕분에 속도를 내어 여기까지 왔고 이제는 그 이유를 좀 더 잘 이해할 수 있는 지식 기반을 가진 상태라고 생각한다. 그래서 이렇게 좀 더 확실하게 정정한다. 그러나 앞으로도 여전히 코루틴 스코프를 만든다는 차원에서 구분 없이 코루틴 빌더라는 말로 통일해서 사용할 것이니 참고하길 바란다.)


지금까지는 코루틴 스코프를 만들 때 코루틴 빌더를 통해서 만들었다. 그러나 코루틴 빌더 자체가 코루틴 스코프 내에서만 선언될 수 있다. 이런 제약으로 인해 suspend 함수를 통한 리팩터링을 진행하면서 마무리하지 못한 작업이 있다. 지금까지 완료된 작업은 아래와 같다.

main()의 launch를 suspend 함수 내부로 옮기면 더욱 좋을 것 같다. 하지만 suspend 함수 내부는 코루틴 스코프가 아니기 때문에 launch를 사용할 수가 없다. 그렇다면 코루틴 스코프를 직접 지정할 수 있으면 해결할 수 있는 문제다. 그렇게 하려면 코루틴 스코프 외부에서 코루틴 스코프를 지정할 수 있어야 한다.


일단 현재 상태에서 알고 있는 방법이 있다. 무엇일까? 바로 runBlocking을 사용하는 것이다. 그런데 launchA(), launchB() 각각에 새로운 코루틴 스코프를 만들면 기존 동작과 동일한 결과를 낼까? 잠시 생각해 보자. launchA() 내부를 runBlocking으로 감싸고 그 내부를 launch로 감싸면 delay(1000L)은 어떤 코루틴에 대한 일시정지인가? launchA() { runBlokcing { launch { delay(1000L) } } } 이러한 상태이다. launch에 대한 일시정지가 발생하고 runBlocking 내부의 다른 코루틴을 실행하려고 할 것이다. 하지만 없다. 따라서 그대로 1초 대기 후 launch가 재개된다. 그렇게 launch가 완료되면 runBlocking도 더 이상 실행할 코루틴이 없기 때문에 완료된다. 이것은 코루틴이 구조화된 동시성(Structured Concurrency) 따르기 때문이다. (나중에 다시 다룬다.)


아무튼 구조화된 동시성으로 인해서 위 예제에서 launchA(), launchB() 각각에 runBlocking을 씌우는 형태는 적합하지 않으니 간단하게 main() 모든 내용을 하나의 함수로 추출해 보자.

launchAB()의 내부가 코루틴 스코프가 되어 launch를 사용할 수 있게 되었다. 이때 launchAB()가 일반 함수인 것에 주목하자. 왜냐하면 runBlocking 대신에 coroutineScope를 사용하려면 launchAB()가 suspend 함수여야 하기 때문이다. 용어에 주의해야 한다. coroutineScope는 코루틴 빌더 중 하나다. 코루틴 스코프를 수신 객체로 가지는 suspend 함수이다. suspend 함수이기 때문에 suspend 함수 내에서만 호출이 가능하다. 그래서 launchAB()가 suspend 함수여야 하는 것이다.


따라서 다음과 같이 변경할 수 있다.

여기서 또 재미난 생각을 해볼 수 있다. 만약 main()을 suspend 함수로 바꾸고 runBlocking 대신에 coroutineScope를 사용하면 어떻게 될까? 아마 잘 동작하는 것처럼 보일 수도 있다. 그렇지만 실행결과가 예상과 다르게 나오는 경우를 발견하게 될 것이다. 다음과 같이 마지막의 두 개의 출력 순서가 기존과 다르게 나올 수 있다. 왜 그럴까? 이걸 이해하려면 Dispatcher에 대해서 알아야 한다. 추후에 Dispatcher를 공부하고 생각해 보길 바란다.

launch A Start
launch B Start
Hello World!
launch B End
launch A End


요약하자.

- 코루틴 빌더와 스코핑 함수는 구분되지만 동일하게 코루틴을 생성하고 실행하는 역할을 한다.
- 모든 코루틴 빌더(launch, async 등)는 코루틴 스코프의 확장 함수이다.
- runBlocking, coroutineScope, withContext는 스코핑 함수이며 코루틴 스코프의 확장 함수가 아니다.
- coroutineScope은 코루틴 스코프 또는 suspend 함수 내부에서 사용할 수 있다.
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari