구조화된 동시성의 원칙
- 비동기 작업을 구조화함으로써 비동기 프로그래밍을 보다 안정적이고 예측할 수 있게 만드는 원칙
- 코루틴은 구조화된 동시성의 원칙을 사용해 비동기 작업인 코루틴을 부모-자식 관계로 구조화함으로써 코루틴이 보다 안전하게 관리되고 제어하도록 지원
- 부모 코루틴을 만드는 코루틴 빌더의 람다식 속에서 새로운 코루틴 빌더를 호출하면 코루틴을 부모-자식 관계로 구조화할 수 있음
구조화된 코루틴의 특징
- 부모 코루틴의 실행 환경이 자식 코루틴에게 상속됨
- 작업을 제어하는 데 사용됨
- 부모 코루틴이 취소되면 자식 코루틴도 취소됨
- 부모 코루틴은 자식 코루틴이 완료될 때까지 대기
- CoroutineScope를 사용해 코루틴이 실행되는 범위를 제한 가능
실행 환경 상속
1. 부모 코루틴의 실행 환경 상속
- 부모 코루틴은 자식 코루틴에게 실행 환경을 상속함
- 부모 코루틴이 자식 코루틴을 생성하면 부모 코루틴의 CoroutineContext가 자식 코루틴에게 전달됨
2. 실행 환경 덮어씌우기
- 부모 코루틴의 모든 실행 환경이 항상 자식 코루틴에게 상속되지는 않음
- 만약 자식 코루틴을 생성하는 코루틴 빌더 함수로 새로운 CoroutineContext 객체가 전달되면 부모 코루틴에게서 전달받은 CoroutineContext 구성 요소들은 자식 코루틴 빌더 함수로 전달된 CoroutineContext 객체의 구성 요소들로 덮어씌워짐
- 또한 다른 CoroutineContext 구성 요소들과 달리 Job 객체는 상속되지 않고 빌더 함수가 호출되면 새롭게 생성됨
3. 상속되지 않는 Job
- launch나 async를 포함한 모든 코루틴 빌더 함수는 호출할 때마다 코루틴 추상체인 Job 객체를 새롭게 생성
- 코루틴 제어에 Job 객체가 필요한데 Job 객체를 부모 코루틴으로부터 상속받게 될 경우 개별 코루틴의 제어가 어려워지기 때문
- 단, 전혀 관계가 없는 것은 아니고 자식 코루틴이 부모 코루틴으로부터 전달받은 Job 객체는 코루틴을 구조화하는 데 사용됨
4. 구조화에 사용되는 Job
- 코루틴 빌더가 호출되면 Job 객체는 새롭게 생성되지만 생성된 Job 객체는 내부에 정의된 parent 속성을 통해 부모 코루틴의 Job 객체에 대한 참조를 가짐
- 부모 코루틴의 Job 객체는 Sequence 타입의 children 속성을 통해 자식 코루틴의 Job에 대한 참조를 가져 자식 코루틴의 Job 객체와 부모 코루틴의 Job 객체는 양방향 참조를 가짐
Job 속성 | 타입 | 설명 |
parent | Job? | 최상 위에 있는 코루틴은 부모 코루틴이 없을 수 있기 때문에 parent 속성은 null이 될 수 있는 타입인 Job? 부모 코루틴이 있더라도 최대 하나 |
children | Sequence<Job> | 하나의 코루틴이 복수의 자식 코루틴을 가질 수 있음 |
코루틴의 구조화와 작업 제어
- 코루틴의 구조화는 하나의 큰 비동기 작업을 작은 비동기 작업으로 나눌 때 일어남
- 코루틴의 구조화는 큰 작업을 연관된 작은 작업으로 분할하는 방식으로 이루어짐
- 코루틴을 구조화하는 가장 중요한 이유는 코루틴을 안전하게 관리하고 제어하기 위함
- 구조화된 코루틴은 다음의 특징을 가짐
- 코루틴으로 취소가 요청되면 자식 코루틴으로 전파됨
- 부모 코루틴은 모든 자식 코루틴이 실행 완료돼야 완료될 수 있음
1. 취소의 전파
- 코루틴은 자식 코루틴으로 취소를 전파하는 특성을 가지기 때문에 특정 코루틴이 취소되면 하위의 모든 코루틴이 취소됨
- 특정 코루틴에 취소가 요청되면 취소는 자식 코루틴 방향으로만 전파되며 부모 코루틴으로는 취소가 전파되지 않음
- 자식 코루틴으로만 취소가 전파되는 이유는 자식 코루틴이 부모 코루틴 작업의 일부이기 때문
2. 부모 코루틴의 자식 코루틴에 대한 완료 의존성
- 부모 코루틴은 모든 자식 코루틴이 실행 완료돼야 완료될 수 있음
- 코루틴의 구조화는 큰 작업을 연관된 여러 작업으로 나누는 방식으로 이뤄지는데 작은 작업이 모두 완료돼야 큰 작업이 완료될 수 있기 때문
- 이를 부모 코루틴이 자식 코루틴에 대해 완료 의존성을 가진다고 함
부연 설명
- 부모 코루틴은 마지막 코드를 8밀리초 정도에 실행했지만 자식 코루틴이 실행 완료되어야 실행 완료되므로 약 1초가 지난 시점에 실행 완료됨
- invokeOnCompletion 콜백은 코루틴이 실행 완료됐을 때뿐만 아니라 취소 완료된 경우에도 동작함
2.1 실행 완료 중 상태
- 코루틴 빌더와 Job 정리 게시글에서 코루틴 상태를 정리했었는데 `실행 완료` 상태는 스킵을 했었음
- `실행 완료` 상태를 이해하기 위해서는 코루틴의 구조화에 대한 이해가 선행되어야 하기 때문
- 여태까지 코루틴의 구조화에 대해 정리했으니 이제 `실행 완료` 상태에 대한 이해를 할 준비가 완료됨
- `실행 완료(Completing)` 상태는 부모 코루틴의 모든 코드가 실행됐지만 자식 코루틴이 실행 중인 경우 부모 코루틴이 갖는 상태
- 부모 코루틴은 더 이상 실행할 코드가 없더라도 자식 코루틴들이 모두 완료될 때까지 실행 완료될 수 없어 `실행 완료 중` 상태에 머묾
부연 설명
- 위 결과에서 확인할 수 있다시피 `실행 완료 중` 상태는 `실행 중` 상태와 완전히 같은 Job 상태 값을 가짐
- 따라서 일반적으로 두 상태를 구분하기 쉽지 않음
- 하지만 코루틴의 실행 흐름을 이해하기 위해서는 자식 코루틴이 실행 완료되지 않으면 부모 코루틴도 실행 완료될 수 없다는 점을 이해하는 것이 중요
CoroutineScope 사용해 코루틴 관리하기
- CoroutineScope 객체는 자신의 범위 내에서 생성된 코루틴들에게 실행 환경을 제공하고, 이들의 실행 범위를 관리하는 역할을 수행
1. CoroutineScope 생성하기 (1)
- CoroutineScope 인터페이스는 코루틴의 실행 환경인 CoroutineContext를 가진 단순한 인터페이스
- 해당 인터페이스의 구현체를 사용하면 CoroutineScope 객체 생성 가능
부연 설명
- launch 코루틴이 CustomScopeThread 쓰레드를 사용해 실행되며 이를 통해 CustomCoroutineScope 객체로부터 코루틴 실행 환경을 제공받는 것을 확인 가능
2. CoroutineScope 생성하기 (2)
- CoroutineScope 객체를 생성하는 또 다른 방법은 CoroutineScope 함수를 사용하는 것
- CoroutineScope 함수는 CoroutineContext를 인자로 입력받아 CoroutineScope 객체를 생성하며, 인자로 입력된 CoroutineContext에 Job 객체가 포함돼 있지 않으면 새로운 Job 객체를 생성함
부연 설명
- 위 코드에서 coroutineScope 변수는 CoroutineScope(Dispatchers.IO)가 호출돼 만들어진 CoroutineScope 객체를 가리킴
- 따라서 CoroutineScope에 대해 launch를 호출해 코루틴을 실행하면 coroutineScope 범위에서 코루틴이 실행되며, coroutineScope 내부에 설정된 CoroutineContext가 launch 코루틴의 실행 환경으로 제공됨
- 이에 따라 코드를 실행하면 launch 코루틴이 Dispatchers.IO에 의해 백그라운드 쓰레드인 DefaultDispatcher-worker-1으로 보내져 실행됨
3. 코루틴에 실행 환경을 제공하는 CoroutineScope
3.1 CoroutineScope가 코루틴에게 실행 환경을 제공하는 방식
- launch 함수가 호출되면 다음 과정을 통해 CoroutineScope 객체로부터 실행 환경을 제공받아 코루틴의 실행 환경을 설정함
- 수신 객체인 CoroutineScope로부터 CoroutineContext 객체를 제공받음
- 제공받은 CoroutineContext 객체에 launch 함수의 context 인자로 넘어온 CoroutineContext를 더함
- 생성된 CoroutineContext에 코루틴 빌더 함수가 호출돼 새로 생성되는 Job을 더하는데 이때 CoroutineContext를 통해 전달되는 Job 객체는 새로 생성되는 Job 객체의 부모 Job 객체가 됨
3.2 CoroutineScope로부터 실행 환경 상속받기
- launch 함수가 호출돼 생성되는 코루틴의 CoroutineContext 객체는 launch 함수의 람다식에서 수신 객체인 CoroutineScope를 통해 제공됨
- launch 함수의 람다식에서 this.coroutineContext를 통해 launch 함수로 생성된 코루틴의 실행 환경에 접근할 수 있었던 이유는 CoroutineScope가 수신 객체로 제공됨
- launch 함수의 람다식 내부에서 launch 함수가 호출돼 새로 생성되는 자식 코루틴에 실행 환경이 상속될 수 있었던 이유 또한 이 CoroutineScope 객체로부터 부모 코루틴의 실행 환경을 상속받았기 때문
- launch 함수뿐만 아니라 runBlocking이나 async 같은 코루틴 빌더 함수의 람다식도 CoroutineScope 객체를 람다식의 수신 객체로 제공하며 이를 통해 코루틴의 실행 환경이 상속됨
3.3 CoroutineScope에 속한 코루틴의 범위
- 각 코루틴 빌더의 람다식은 CoroutineScope 객체를 수신 객체로 가짐
- CoroutineScope 객체는 기본적으로 특정 범위의 코루틴들을 제어하는 역할을 수행
- 코루틴 빌더 람다식에서 수신 객체로 제공되는 CoroutineScope 객체는 코루틴 빌더로 생성되는 코루틴과 람다식 내에서 CoroutineScope 객체를 사용해 실행되는 모든 코루틴을 포함시킴
- 특정 코루틴만 기존에 존재하던 CoroutineScope 객체의 범위에서 벗어나게 만들려면 새로운 CoroutineScope 객체를 생성하고 해당 CoroutineScope 객체를 사용해 코루틴을 실행하면 됨
부연 설명
- Coroutine-4 코루틴은 runBlocking 람다식의 CoroutineScope 객체의 범위에서 벗어나 새로운 CoroutineScope 객체의 범위에 속하게 됨
- CoroutineScope 함수가 호출되면 새로운 Job 객체가 생성되고 코루틴은 Job 객체를 사용해 구조화됨
- CoroutineScope 함수를 사용해 새로운 CoroutineScope 객체를 생성하면 기존의 계층 구조를 따르지 않는 새로운 Job 객체가 생성돼 새로운 계층 구조를 만들기 때문
3.4 CoroutineScope 취소하기
- CoroutineScope 인터페이스는 확장 함수로 cancel 함수를 지원함
- CoroutineScope 인터페이스의 cancel 함수는 CoroutineScope 객체의 범위에 속한 모든 코루틴을 취소하는 함수로 CoroutineScope 객체에 cancel 함수가 호출되면 범위에서 실행 중인 모든 코루틴에 취소가 요청됨
- CoroutineScope 객체에 cancel 함수가 호출되면 CoroutineScope 객체는 자신의 coroutineContext 속성을 통해 Job 객체에 접근한 후 cancel 함수를 호출
- CoroutineScope 객체에 대한 취소 동작은 앞서 언급한 취소의 전파에서 다뤘듯이 부모 코루틴을 취소하면 모든 자식 코루틴들에게 취소가 전파되는 동작과 같은 원리로 발생
3.5 CoroutineScope 활성화 상태 확인하기
- CoroutineScope 객체는 CoroutineScope 객체가 현재 활성화돼 있는지 확인하는 isActive 확장 속성을 제공
- isActive 확장 속성은 coroutineContext에 설정된 Job 객체의 isActive 속성을 확인
- Job 객체의 isActive 확장 속성은 Job 객체에 취소가 요청되면 false로 변경
- isActive 사용 시 일시 중단 시점이 없는 코루틴을 안전하게 관리하는 데 사용 가능
구조화와 Job
- 코루틴의 구조화의 중심에는 Job 객체가 있음
1. runBlocking과 루트 Job
- 부모 Job 객체가 없는 구조화의 시작점 역할을 하는 Job 객체를 루트 Job이라고 지칭
- 해당 Job 객체에 의해 제어되는 코루틴을 루트 코루틴이라고 함
부연 설명
- runBlocking을 통해 루트 코루틴이 생성됨
- runBlocking 람다식 내부에서는 launch 함수가 호출돼 Coroutine1과 Coroutine2가 실행되는데 Coroutine1 내부에서 다시 Coroutine3과 Coroutine4가 실행되고, Coroutine2 내부에서는 Coroutine5가 실행됨
- runBlocking 코루틴을 루트 코루틴으로 해서 하위에 모든 코루틴들이 구조화됨
2. Job 구조화 깨기
2.1 CoroutineScope 사용해 구조화 깨기
- CoroutineScope 객체는 코루틴 실행 환경으로 CoroutinContext 객체를 갖기 때문에 코루틴과 마찬가지로 Job 객체를 가질 수 있음
- CoroutineScope 함수를 통해 CoroutineScope 객체가 생성되면 새로운 루트 Job이 생성되며, 이를 사용해 코루틴의 구조화를 깰 수 있음
부연 설명
- runBlocking 함수를 통해 루트 Job이 생성되지만 CoroutineScope(Dispatchers.IO)가 호출돼 새로운 루트 Job을 가진 newScope가 생성됨
- 이후 newScope는 launch 함수를 호출해 Coroutine1과 Coroutine2를 실행하는데 Coroutine1의 자식 코루틴으로 Coroutine3과 Coroutine4, Corutine2의 자식 코루틴으로 Coroutine5가 실행됨
- runBlocking 람다식 마지막에 일정 시간 동안 대기하는 코드를 넣는 이유는 newScope로 인해 구조화가 깨졌기 때문에 runBloocking 코루틴이 다른 코루틴들의 완료를 기다리지 않고 메인 쓰레드 사용을 종료할 수 있기 때문에 newScope 하위의 모든 코루틴이 실행돼 결과가 출력하도록 처리하기 위해 추가
- 단, 코루틴의 구조화를 깬 후 delay 함수 등을 통해 구조화가 깨진 코루틴이 실행 완료되는 것을 기다리는 것은 코드를 불안정하게 만들 수 있으므로 지양해야 함
2.2 Job 사용해 구조화 깨기
- 루트 Job은 부모가 없는 Job 객체로 Job()을 통해 생성 가능
부연 설명
- Job()을 통해 새로운 루튼 Job인 newRootJob이 생성되며 말 그대로 newRootJob 자체가 루트 Job이 됨
- 따라서 newRootJob.cancel()이 호출되면 취소 전파에 의해 하위의 모든 Job 객체에 취소가 전파돼 코루틴이 취소됨
2.3 Job 사용해 일부 코루틴만 취소되지 않게 만들기
- 새로 Job 객체를 생성해 계층 구조를 끊음으로써 일부 코루틴만 취소되지 않도록 설정 가능
부연 설명
- Coroutine5의 계층 구조만 끊기 위해 Coroutine5의 인자로 Job()을 추가로 넘김
- Coroutine5가 생성되기 전에 Coroutine2가 취소되면 실행이 안되므로 Coroutine2가 취소되는 상황 방지를 위해 delay(50L) 호출
2.4 생성된 Job의 부모를 명시적으로 설정하기
- Job()을 통해 Job 객체를 생성할 경우 parent 속성이 null이 돼 부모가 없는 루트 Job이 생성됨
- 만약 Job 생성 함수의 parent 인자로 Job 객체를 넘기면 해당 Job을 부모로 하는 새로운 Job 객체를 생성할 수 있음
부연 설명
- Job 생성 함수를 통해 Job 객체를 생성할 경우 자동으로 실행 완료되지 않는 것을 확인 가능
2.5 생성된 Job은 자동으로 실행 완료되지 않는다
- launch 함수를 통해 생성된 Job 객체는 더 이상 실행할 코드가 없고, 모든 자식 코루틴들이 실행 완료되면 자동으로 실행이 완료됨
- 하지만 Job 생성 함수를 통해 생성된 job 객체는 자식 코루틴들이 모두 실행 완료되더라도 자동으로 실행 완료되지 않으며, 명시적으로 완료 함수인 complete을 호출해야 완료됨
비고
1. runBlocking vs launch
- runBlocking은 쓰레드를 차단하고 모든 작업이 완료될 때까지 기다리며, 테스트 또는 초기화 용도로 주로 사용
- launch는 쓰레드를 차단하지 않으며 비동기적으로 작업을 실행하는 데 적합하며, 실제 비동기 코루틴 실행에 주로 사용
참고
코틀린 코루틴의 정석 (조세영 저)
반응형
'Kotlin > 코틀린 코루틴의 정석' 카테고리의 다른 글
[Kotlin 코루틴] 일시 중단 함수 (0) | 2024.11.22 |
---|---|
[Kotlin 코루틴] 예외 처리 (0) | 2024.11.22 |
[Kotlin 코루틴] CoroutineContext 정리 (0) | 2024.11.19 |
[Kotlin 코루틴] async와 Deferred (0) | 2024.11.15 |
[Kotlin 코루틴] 코루틴 빌더와 Job 정리 (0) | 2024.11.08 |