brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Sep 01. 2023

코루틴 구조화된 동시성

코틀린 코루틴 (6)

코루틴 구조화된 동시성 (Structured Concurrency)


suspend 함수를 거쳐 코루틴 스코프에서 간단한 리팩터링 중 구조화된 동시성으로 인해 예상과 다른 결과를 얻게 될 것이라고 한 적이 있다. 그때 시도하려고 했던 방법은 다음과 같다. (실행결과를 예상해 보면 더 좋다.)

main()에 있던 launch를 각 suspend 함수 내부로 이동한 것이다. 리팩터링 전 실행결과는 아래와 같다.

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

하지만 위 코드를 실행하면 원하는 결과를 얻을 수 없다. 실제 실행결과는 다음과 같다.

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


리팩터링 전과 후의 코드의 주요 차이점이다. 언뜻 보면 같은 동작을 할 것 같다. launch 블록이 평범한 블록이었다면 그럴 수도 있을 것이다. 하지만 여기서는 크게 세 가지 사항에 대해서 주목해야 한다.

1. main() 내부는 runBlocking에 의해 감싸져 있는 형태이다.
2. launch는 코루틴 빌더이다.
3. suspend 함수는 coroutineScope에 의해 감싸져 있는 형태이고 그 안에 launch가 존재한다.

위 사항을 토대로 코루틴의 관계를 머릿속에 그림으로 그려보자. 먼저 리팩터링 전의 코드는 delay 함수에서 봤던 것과 동일한 구조를 가지고 있다. runBlocking 내부, 즉 같은 coroutineScope에 launch를 통해 두 개의 코루틴이 존재하고 있다. 이 경우에는 하나의 코루틴이 일시정지 했을 때 다른 코루틴에게 제어권을 넘겨주어 해당 코루틴이 작업을 수행할 수 있도록 한다. 그렇기 때문에 launch A Start, launch B Start가 연달아서 출력되는 것이다.

그림 1) 리팩터링 전 구조

리팩터링 후에는 다르다. runBlocking 내부에 두 개의 코루틴이 존재하는 것처럼 생각할 수 있다. 하지만 앞서 하나의 coroutineScope에 두 개의 코루틴이 존재한 구조를 생각해 보자. 이 경우에는 runBlocking 내부에 두 개의 코루틴이 존재하는 것이 아니라 두 개의 coroutineScope가 존재하는 것이다. 그리고 각 coroutineScope 내부에 하나의 코루틴이 존재한다. (좀 더 엄밀히 말하면 runBlocking 내부 coroutineScope 내부에 두 개의 coroutineScope가 존재하고 있다. 거의 같은 의미로 보면 되기도 하고 그림에서는 시인성을 위하여 runBlocking의 coroutineScope 영역은 생략했다.)

그림 2) 리팩터링 후 구조

이런 경우에는 launchA()에서 실행한 코루틴을 일시정지 한다고 해도 해당 coroutineScope 내에 존재하는 다른 코루틴이 없기 때문에 제어권을 넘겨줄 곳이 없다. 따라서 일시정지한 시간만큼 정지한 후 다시 해당 코루틴이 재개된다. 따라서 처음에 기대한 결과와는 다르게 동작한다. launch A Start, launch A End가 출력된 후에 launchB()가 실행될 것이다. 그 후에 마지막으로 runBlocking 내부의 작업이 실행된다.


그림 1과 같이 coroutineScope 내부에 코루틴을 가지고 있는 이런 구조를 말로 표현하면 부모 코루틴과 자식 코루틴이라고 할 수 있다. 대입해 보면 coroutineScope가 부모 코루틴이 되고 내부 코루틴이 자식 코루틴이 된다. 즉 부모 코루틴은 자식 코루틴을 내부에 품고 있다. 그리고 자식 코루틴이 종료될 때까지 기다려준다. (반대로 부모 코루틴이 취소되면 자식 코루틴도 모두 취소된다. 취소? 취소하는 방법은 추후에 다룬다.)


그림 1 구조에서 실행결과를 다시 보라. launch A Start, launch B Start, Hello World! 여기까지 진행된 상황이라면 runBlocking의 끝자락에 도달했다는 의미다. Hello World! 출력하는 것이 마지막 작업이니까. 하지만 종료되지 않고 나머지 코루틴의 작업을 수행한다. 엥? runBlocking은 코루틴이 완료되기 전까지 메인 스레드를 잡아주는 녀석이라서 그런 것 아닌가? 맞다. 그런 개념과 아주 유사하게 생각하면 된다. 스레드를 잡아주는 역할을 하지만 동시에 CoroutineScope를 만들어주는 존재임을 잊지 말자.


그래서 좀 더 명확한 그림 2 구조를 준비했다. 그림 2에서는 launchA()의 coroutineScope에서 일시정지가 발생했을 때 지정한 시간만큼 정지한다. 그리고 나머지 작업도 수행한다. 이것은 모든 작업이 완료될 때까지 coroutineScope가 기다려준 것이다. 해당 coroutineScope 내부에 두 개의 launch가 있었다면 어떻겠는가? 역시 두 코루틴이 모두 완료될 때까지 기다려줄 것이다. 이 구조는 익숙하지 않나? 마치 그림 1 구조에서 runBlocking 대신에 coroutineScope을 사용하고 있는 것과 같다.


결과적으로 두 코드에서 실행결과가 차이가 난 것은 구조적인 차이다. 그림 2 구조는 runBlocking 내부에서 자식 코루틴이면서 각자 부모 코루틴의 역할을 하고 있는 두 coroutineScope이 존재한다. 그 부모 코루틴은 자신의 모든 자식 코루틴의 작업이 완료될 때까지 기다려준다. 이로 인해서 제어권이 다른 부모 코루틴에 속한 자식 코루틴에게 넘어가지 않는다.


이러한 구조적인 동작 방식을 이해하고 생각하면서 다시 예제 코드를 보라. 이것이 코루틴의 정말 간단한 구조적인 동시성이다. 이러한 특징 때문에 코루틴 간의 관계가 부모 코루틴과 자식 코루틴 간의 관계로 명확하게 정리된다. 또한 자식 코루틴이 완료될 때까지 자원을 잘 가지고 있고 반대로 부모 코루틴이 취소되면 자식 코루틴도 취소되는 식으로 자원을 해지할 수도 있다. 또한 에러 처리도 자식에서 부모로 전달되는 형태를 취하는데 이것에 관해서는 나중에 다시 살펴볼 것이다.


요약하자.

- 부모 코루틴은 자신의 모든 자식 코루틴의 작업이 완료될 때까지 기다려준다.
- 구조적인 동시성으로 인해 코루틴 간의 관계가 명확해진다.
- 구조적인 동시성으로 인해 자원을 안전하게 관리할 수 있다.
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari