Kotlin
[Kotlin] CoroutineScope
꾸준함.
2024. 10. 3. 16:16
CoroutineScope
- Coroutine들에 대한 scope를 정의하며 scope에는 여러 Coroutine들 포함
- 자식 Coroutine들에 대한 생명주기를 관리하며 자식 Coroutine들이 모두 완료되어야만 scope도 완료되었다고 처리
- CoroutineScope의 coroutineContext에는 꼭 Job을 포함해야 함
1. CoroutineScope 함수
- CoroutineScope 함수를 이용해 CoroutineScope 생성
부연 설명
- 인자로 주어진 context에 Job이 포함되었는지 확인
- 포함되어 있지 않다면 Job을 생성하여 추가하고 ContextScope에 전달
- 여기서 ContextScope는 CoroutineScope의 단순한 구현체
부연 설명
- EmptyCoroutineContext를 통해 빈 CoroutineScope를 생성
- 다만, Job을 생성하여 ContextScope를 생성하기 때문에 JobImpl을 포함
2. Coroutine Builder
- 코틀린에서 코루틴을 생성하고 실행하기 위한 기본적인 도구
- Coroutine Builder는 CoroutineScope의 CoroutineContext와 인자로 전달받은 context를 merge 하여 newContext 생성
- Coroutine을 생성하여 start하고 반환
- CoroutineScope의 Job을 부모로 가짐
- Coroutine Builder를 통해 생성된 Coroutine은 비동기 하게 동작
2.1 launch 코루틴 빌더
- StandaloneCoroutine을 생성하고 start 하는데 isLazy가 참일 경우 LazyStandaloneCoroutine 생성
- StandaloneCoroutine을 Job으로 반환하는데 외부에서 launch를 실행한 후 Job을 획득하여 cancel, join 등을 실행 가능
- newCoroutineContext를 통해 CoroutineScope의 coroutineContext와 인자로 주어진 context를 merge
launch 코루틴 빌더 예제
부연 설명
- CoroutineScope에 Coroutine Builder인 launch를 통해 코루틴을 생성하고 this로 접근
- 비동기로 동작하기 때문에 join을 통해 완료될 때까지 지연
2.2 async 코루틴 빌더
- DeferredCoroutine을 생성하고 시작하는데 isLazy가 참이라면 LazyDeferredCoroutine을 생성
- DeferredCoroutine을 Deferred로 반환
- Deferred는 Kotlin 코루틴에서 비동기 작업의 결과를 나타내는 객체
- Deferred는 미래에 완료될 값을 나타내며, 이는 자바의 Future나 자바스크립트의 Promise와 비슷한 역할
- Deferred는 Job을 상속하고 있기 때문에 join, cancel 뿐만 아니라 await을 통해 block이 반환하는 값에 접근 가능
async 코루틴 빌더 예제
부연 설명
- CoroutineScope에 코루틴 빌더인 async를 통해 코루틴을 생성하고 this로 접근
- 비동기로 동작하기 때문에 await을 통해 완료될 때까지 suspend 하고 값에 접근
3. AbstractCoroutine
- 모든 코루틴은 AbstractCoroutine을 상속
- 코루틴은 Job이며 Continuation이고 CoroutineScope
부연 설명
- Job: 작업의 단위가 되고 start, cancel로 상태를 변경할 수 있으며 join으로 완료 시점 명시
- CoroutineScope: 코루틴 빌더를 통해 자식 코루틴을 생성하고 자식 코루틴들의 생명주기를 관리
부연 설명
- CoroutineScope에 코루틴 빌더인 launch를 통해 coroutine1 생성
- coroutine1에서 자식 코루틴인 coroutine2 생성하며 이는 coroutine1이 CoroutineScope이기 때문에 가능한 것
- job2의 join을 통해 완료될 때까지 지연
4. Coroutine Builder의 문제점과 해결 방법
4.1 launch 코루틴 빌더 문제점
- 여러 job들이 완료되어야 하는 시점이 중요할 때 job을 받아서 직접 join을 실행해야 하지만 매번 여러 job들을 join 하는 것은 쉽지 않음
- ex) job1, job2, job3은 정확히 step1 출력과 step2 출력 사이에 완료되어야 하는 케이스
4.1.1 launch 코루틴 빌더 문제점 - 해결 방법 1
- 코루틴들을 묶는 코루틴을 생성하여 join
- job1은 3개의 코루틴을 포함하고 join을 호출하여 지연시킨 후 3개의 코루틴이 모두 완료된 후 재개
- 여전히 job1에 대한 join은 존재하지만 기존처럼 N번 join을 호출할 필요는 없어짐
4.1.2 launch 코루틴 빌더 문제점 - 해결 방법 2
- CoroutineScope를 생성하여 step1과 step2 사이에 추가
- CoroutineScope는 동기적으로 동작
- CoroutineScope의 ScopeCoroutine이 launch의 StandaloneCoroutine을 자식으로 두기 때문에 launch들이 모두 완료되고 coroutineScope가 종료됨을 보장
부연 설명
- CoroutineScope는 외부 scope의 context를 상속하고 job을 오버라이드하기 때문에 분리된 환경에서 자식 코루틴들의 생명주기를 관리하고 자식 코루틴이 모두 완료되고 coroutineScope 함수가 완료됨을 보장
- ScopeCoroutine을 생성하여 block을 실행하기 때문에 ScopeCoroutine은 동기적으로 동작
4.2 async 코루틴 빌더 문제점
- 여러 deferred들이 완료되어야 하는 시점이 중요할 경우 모든 deferred에 await()을 호출해줘야 함
- ex) deferred 1, 2, 3은 정확히 step 1 출력과 step2 출력 사이에 반환되어야 함
4.2.1 aysnc 코루틴 빌더 문제점 - 해결 방법
- coroutineScope로 async를 감싸고 마지막에 await를 호출하여 결과를 합산할 경우 async 함수를 호출하는 순간부터 각각의 코루틴들을 시작
- coroutineScope로 감싸고 async를 실행하기 때문에 포함하고 있는 async들만 종료되고 나면 coroutineScope도 종료
5. Job Cancellation
- 코루틴은 기본적으로 협력적 취소(cooperative cancellation) 방식을 사용하며, 이는 코루틴이 취소 신호를 받아들여 스스로 취소되는 방식
- 코루틴에서 Job은 코루틴의 생명 주기를 관리하며 launch나 async 같은 코루틴 빌더는 Job 객체를 반환하는데, 해당 객체들을 통해 코루틴을 취소할 수 있음
- 코루틴 취소를 할 때는 Job.cancel() 메서드를 호출하면 됨
- 취소 신호는 코루틴이 실행되는 동안 협력적으로 전달되며, 코루틴이 중단 가능한 상태에서 해당 신호를 감지했을 때 취소가 완료됨
- 중단 가능한 지점은 suspend 함수 중에서 취소를 인식하고 처리할 수 있는 함수들이며 대표적인 예는 delay(), yield(), 그리고 채널 또는 플로우와 같은 비동기 스트림의 중단 함수
5.1 Root Coroutine에서 cancel을 호출하는 예제
- Root Coroutine에서 cancel을 진행하고 join을 통해 모든 코루틴이 종료될 때까지 대기
- job2, job3의 delay에서 모두 JobCancellationException 발생
5.2 Leaf Coroutine에서 cancel을 호출하는 예제
- 종단에 해당하는 coroutine2와 coroutine3 중에 coroutine2를 job으로 받아 cancel
- job2는 cancel 된 후 JobCancellationException 발생하지만 job3는 제대로 실행됨
- Root Coroutine도 cancelled 상태가 아닌 것을 확인 가능
5.3 Leaf Coroutine에서 예외가 발생한다면?
- coroutine3의 delay가 JobCancellationException을 던지면 Root Coroutine, CoroutineScope의 job 모두 cancelled 상태로 변경하며 이유는 Coroutine 예외 전파 규칙 내용을 참고
비고
1. launch 코루틴 빌더 vs CoroutineScope
- launch는 비동기적으로 동작하는데 반해 coroutineScope는 동기적으로 동작
- coroutineScope는 ScopeCoroutine을 생성하지만 ContinuatinoId를 오버라이드하지 않고 launch는 새로운 ContinuationId를 생성
- 또한 coroutineScope는 결과를 반환하지만 launch는 Job을 반환
2. Coroutine 예외 전파 규칙
- 코루틴에서 예외는 CoroutineScope에 따라 다르게 전파
- 코루틴의 예외는 부모-자식 관계에 따라 전파되며, 자식 코루틴에서 발생한 예외는 부모 코루틴에 영향을 미침
- launch 코루틴 빌더는 결과를 반환하지 않기 때문에, 예외가 발생하면 바로 부모 코루틴으로 전파되며 만약 예외를 잡아내지 않을 경우 부모 코루틴이 예외에 의해 취소됨
- async 코루틴 빌더는 결과를 반환하는 Deferred 객체를 생성하기 때문에 예외가 발생해도 즉시 부모 코루틴으로 전파되지 않으며, await()을 호출할 때 예외가 발생
- SupervisorJob을 사용하면, 자식 코루틴의 예외가 부모에게 전파되지 않고 독립적으로 처리될 수 있음
- SupervisorJob을 사용하면 부모 코루틴이 자식 코루틴의 예외로 인해 영향을 받지 않으며, 다른 자식 코루틴들도 계속해서 실행
참고
패스트 캠퍼스 - Spring Webflux 완전 정복 : 코루틴부터 리액티브 MSA 프로젝트까지
반응형