Skip to content

Commit

Permalink
docs: coroutine 블로그 초안들
Browse files Browse the repository at this point in the history
  • Loading branch information
murjune committed Sep 7, 2024
1 parent e9c755a commit 73f5c69
Show file tree
Hide file tree
Showing 12 changed files with 1,186 additions and 2 deletions.
19 changes: 19 additions & 0 deletions coroutine/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 테스트
- [ ] 테스트 코드를 작성해야하는 이유
- [ ] Fake vs Mock
- [ ] 좋은 테스트를 작성하는 법
- [ ] 코루틴 테스트하는법
- [ ] 플로우 테스트하는법
- [ ] ViewModel 테스트하는법
- [ ] 테스트하기 어려운 코드 테스트하는법

# 코루틴 예외
- [x] 코루틴 예외 전파
- [x] 코루틴 예외 전파 방지 - SupervisorJob
- [ ] 코루틴 예외 전파 방지 - supervisorScope
- [ ] 코루틴 예외 전파 방지 - supervisorScope vs SupervisorJob
- [ ] 코루틴 예외 처리 - CoroutineExceptionHandler
- [ ] 코루틴 예외 처리 - 코루틴 스코프 함수 + try-catch

# 코루틴
- [ ] suspend function 을 잘 써보자
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
코루틴 컨택스트의 일종

일반 디스패처 특징 : 코루틴의 실행을 요청받으면 작업 대기열에 적재한 후 스레드풀에서 사용할 수 있는 스레드로 보낸다
무제한 디스패처 특징: 스레드 스위칭 없이 호출자 스레드에서 바로 실행

무제한 디스패처: 재개될 때 코루틴을 재개하는 스레드에서 재개된

일반적으로 재개될 때 코루틴은 코루틴 컨택스에 존재하는 Dispatcher 에 의해 재분배된다. (Continuation 안에 있는 coroutineContext 속에 있는 Dispatcher 를 통해)
그러나 무제한 디스패처는 resume 될 때 재개가 실행되는 스레드에서 재실행됨 (예측이 불가능한 비동기 작업이됨 그래서 실 프로덕트 환경에서는 쓰지 않음)



# CPS

코루틴은 코루틴의 실행 정보를 저장하고 전달할 때 CPS(Continuation Passing Style) 기법을 채택하고 있다.
77 changes: 77 additions & 0 deletions coroutine/docs/coroutine/coroutine_context/코루틴_스코프.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# 코루틴 스코프

코루틴 스코프: `스코프 범위 내에서 생성된 코루틴에게 실행 환경을 제공하고, 이 코루틴의 동작을 관리`한다.

코루틴의 실행 환경을 저장하고 관리하는 CoroutineContext 를 가지고 있다.

코루틴 스코프에 있는 CoroutineContext 에 만약 IO Dispatcher 가 존재한다면 해당 코루틴 스코프 내에서 만들어진 코루틴들은
모두 IO Dispatcher 에 의해 IO 스레드에서 동작하게 될 것이다.

그리고, CoroutineScope 에 있는 Job 이 있으면 모든 코루틴들은 이 Job 을 root Job 으로 삼고 있을 것이다.

확인해보자

```kotlin
import kotlin.coroutines.coroutineContext

suspend fun main() {
val scope = object : CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Default + Job()
}

// launch 로 인해 생성되는 코루틴에게 coroutineContext(실행 환경) 을 제공한다.

scope.launch {
println(coroutineContext)
}
}
```

# CoroutineScope() 팩토리 함수를 통해 스코프를 만들자

기본적으로 `CoroutineScope()` 팩토리 함수에는 params로 받는 context 에 Job 이 없으면 Job() 을 통해 root Job을 만들고 있다.

# 커스텀 CoroutineScope 를 만들지 말아라 !

만약, 커스텀 CoroutineScope 를 만들면 어떻게 될까?

```kotlin
suspend fun main() {
val scope = object : CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Default
}

scope.launch {
println(coroutineContext[Job]) // StandaloneCoroutine{Active}@4fba6dc8
println(coroutineContext[Job]?.parent) // null
}
delay(100)
}
```
CoroutineScope 에 rootJob 이 존재하지 않게된다.
그럼 CoroutineScope 의 책임 중 하나인 코루틴의 동작을 관리할 수 없게 된다.

```kotlin
suspend fun main() {
val scope = object : CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Default
}

scope.cancel()
}
```
만약, scope 의 cancel() 을 호출해보자

바로 에러 발생..
```
Exception in thread "main" java.lang.IllegalStateException: Scope cannot be cancelled because it does not have a job
```


CoroutineScope 는 스코프 내에서 생성된 코루틴들에게 실행 환경을 제공하고 이들의 실행 범위를 제어하는 역할
그래서 coroutineContext를 홀딩하고 있다
1) 코루틴들에게 실행 환경을 제공
- Dispatcher - corotineContext 내부의 CoroutineDispatcher 가 launch 코루틴이 어느 스레드에서 돌아가도록할지 배분한다.
- 부모 Job 제공
-
2) 실행 범위를 제어하는 역할
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 코루틴 컨택스트

코루틴 컨택스트는 코루틴의 실행 환경을 그룹화하여 저장하고 전달하는 객체이다.
이를 활용해 코루틴의 실행 상태가 어떤지, 어떤 스레드에 분배 받을지 등 `코루틴의 작동 방식`을 정합니다.
- Job: 코루틴의 실행 상태를 나타내고 코루틴의 실행을 제어할 수 있다.
- Dispatcher: 코루틴이 어떤 스레드에서 보내져 동작할지 지정한다.
- CoroutineExceptionHandler: 예외 처리기

코루틴 컨택스트는 여러 Element 로 구성되어 있는데 대표적으로 Dispatcher, Job, CoroutineExceptionHandler 가 있겠다.
코루틴은 자신의 컨택스트를 자식에게 전달한다.(자식이 부모의 컨텍스트를 상속받는다.)
그리고, 자식은 상속받은 부모의 컨텍스트를 대체한다.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@

만약, 해당 글이나 테코톡을 보고 궁금하신 점이나 함께 논의하고 싶은 부분이 있다면 댓글이나 메일로 남겨주세요 😁

> 아래 지식들을 알고 있으면 해당 글을 이해하기 쉬울 거에요!
> - CoroutineContext
> - Job, launch, async
> - CoroutineScope
> - 코루틴 구조화된 동시성
> - 코루틴 취소 메커니즘
> - suspend function
> - coroutineScope

## 코루틴 예외 처리의 중요성

kotlin을 활용하는 대부분의 프로그램(대표적으로 Android) 에서는 [Coroutine](https://kotlinlang.org/docs/coroutines-overview.html) 을 활용하여 비동기 처리하고 있다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,24 @@
코루틴 예외에 대한 보충 설명 및 상세한 설명을 포스팅하려 합니다.

이번 포스팅에서는 `SupervisorJob 을 활용해서 예외 전파 제한하는 방법`에 대해서 알아볼 것입니다.
테코톡에서는 [3:48 ~ 8:12] 에 해당하는 내용입니다.
만약, 코루틴이 예외를 어떻게 전파되는지 궁금하신 분은 이전에 포스팅한 [코루틴 예외가 전파되는 방식](https://velog.io/@murjune/kotlin-Coroutine-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%98%88%EC%99%B8%EA%B0%80-%EC%A0%84%ED%8C%8C%EB%90%98%EB%8A%94-%EB%B0%A9%EC%8B%9D%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-0lac2p97) 을 참고해주세요!
테코톡에서는 [3:48 ~ 8:12] 에 해당하는 내용입니다.

지난 시간에 배운 내용 리마인드~
> - 코루틴 예외 전파 메커니즘
> 1) 예외가 발생할 시, `자기 자신`을 취소시킨다. (자식 코루틴들 모두 취소)
> 2) 예외 발생 시, `부모로 예외를 전파`시킨다. (부모, 형제 코루틴들 모두 취소)
>
> 좀 더 궁금하신 분은 이전에 포스팅한 [코루틴 예외가 전파되는 방식](https://velog.io/@murjune/kotlin-Coroutine-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%98%88%EC%99%B8%EA%B0%80-%EC%A0%84%ED%8C%8C%EB%90%98%EB%8A%94-%EB%B0%A9%EC%8B%9D%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-0lac2p97)을 참고해주세요


---

# 예외 전파 제한이 필요한 경우
코루틴을 활용하여 비동기 작업을 하다 보면 하나의 작업을 여러 작업으로 쪼개 병렬처리하는 경우가 종종 있습니다. 보통 suspend 함수에서 코루틴 빌더함수 [async](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) 와 코루틴 스코프 함수[coroutineScope](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html)를 활용하여 처리합니다.

> async 와 coroutineScope 를 사용하여 병렬 처리하는 이유를 자세히 알고 싶으신 분은 [Kotlin Coroutine: suspend 함수를 Effective 하게 설계하자!](https://velog.io/@murjune/Kotlin-Coroutine-suspend-%ED%95%A8%EC%88%98%EB%A5%BC-Effective-%ED%95%98%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90-fsaiexxw#3-coroutinescope-or-withcontext-%ED%95%A8%EC%88%98%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EC%9E%90) 에서 `2) suspend function 에서 병렬 처리할 때, CoroutineScope를 사용하지 말자``3) coroutineScope or withContext 함수를 활용하자!⭐️` 부분을 참고해주세요 😉
로컬 저장소의 이미지 경로를 통해 서버에 이미지들을 업로드한 후, 이미지 url을 받아오는 예제를 통해 `예외 전파 제한이 필요성`에 대해 알아볼 것이에요!😎

<p ailgn="center">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
!youtube[3DNbRnl0im4]

테코톡에서는 [9:01 ~ 9:58] 에 해당하는 내용입니다.
지금까지 배운 내용 리마인드 해봅시다.

> - SupervisorJob
> 1) SupervisorJob 은 자식 코루틴의 예외 전파를 방지한다.
> 2) SupervisorJob() 로 생성된 SupervisorJob 은 root Job 이 된다.
> 3) SupervisorJob() 로 생성된 SupervisorJob 은 항상 active 하다.
>
> - 이전 포스팅
> [코루틴 예외가 전파되는 방식](https://velog.io/@murjune/kotlin-Coroutine-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%98%88%EC%99%B8%EA%B0%80-%EC%A0%84%ED%8C%8C%EB%90%98%EB%8A%94-%EB%B0%A9%EC%8B%9D%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-0lac2p97)
> [코루틴 예외 전파 제한 왜 하는거지?(with SupervisorJob)](https://velog.io/@murjune/kotlin-Coroutine-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%98%88%EC%99%B8-%EC%A0%84%ED%8C%8C-%EC%A0%9C%ED%95%9C-%EC%99%9C-%ED%95%98%EB%8A%94%EA%B1%B0%EC%A7%80with-SupervisorJob)
## Intro

[지난 포스팅](https://velog.io/@murjune/kotlin-Coroutine-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%98%88%EC%99%B8-%EC%A0%84%ED%8C%8C-%EC%A0%9C%ED%95%9C-%EC%99%9C-%ED%95%98%EB%8A%94%EA%B1%B0%EC%A7%80with-SupervisorJob)에서는 이미지를 업로드하는 예시를 통해 `SupervisorJob`로 예외 전파를 제한하는 방법을 배워봤습니다. 사실은 이미지 업로드 예시는 `SupervisorJob` 보다는 `supervisorScope` 을 활용하는 것이 더 적절하다고 했었죠?

코루틴 스코프함수와 `supervisorScope`에 대해 배워본 후, 이미지 업로드 예시를 `supervisorScope` 로 리팩토링해봅시다!💪

## 1. 코루틴 스코프 함수

> 코루틴 스코프 함수에 대해 이미 알고 계신 독자는 이번 챕터는 넘기셔도 좋습니다 😁

코루틴 스코프 함수는 `새로운 코루틴 스코프를 생성하는 suspend 함수` 입니다.
`launch``async`는 CoroutineScope 확장함수이기에 일반 suspend 함수에서는 호출할 수 없습니다.

```kotlin
suspend fun foo() = coroutineScope {
launch { ... } // 호출 불가 ❌
launch { ... } // 호출 불가 ❌
}
```

이런 경우 coroutineScope 와 같은 코루틴 스코프 함수를 활용합니다.
(withContext, withTimeOut, supervisorScope.. 등 다양한 스코프 함수가 있습니다.)

```kotlin
suspend fun bar() = coroutineScope {
launch { ... } //
launch { ... } //
}
```

좀 더 자세한 설명을 원하시면 [suspend 함수를 Effective 하게 설계하자!](https://velog.io/@murjune/Kotlin-Coroutine-suspend-%ED%95%A8%EC%88%98%EB%A5%BC-Effective-%ED%95%98%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90-fsaiexxw)을 봐주세요 🙏

# 2. 코루틴 스코프 함수의 특징

코루틴 스코프 함수는 다음과 같은 특징을 가지고 있습니다.
> 1) ⭐️ 호출자의 코루틴 컨텍스트를 받아, 호출자 코루틴과 부모-자식관계를 보장한다.
> 2) ⭐️ 호출자 코루틴은 끝날때까지 일시 중단된다.
> 3) ⭐️ 자식 코루틴의 작업이 끝날때까지 대기한다.
> 4) 일반 함수와 같은 방식으로 예외를 던진다.
> 5) 결과값을 반환한다.
코루틴 빌더 함수(launch) 도 Job을 생성할 때 부모-자식관계를 보장해주는데요, `코루틴 빌더 함수``코루틴 스코프 함수`를 비교해볼까요?

- 코루틴 빌더 함수 (launch)
```kotlin
fun main() = runBlocking {
launch {
delay(10)
println("after")
}
println("before") // 먼저 호출
}
```

launch 의 경우에는 비동기적으로 실행되기 때문에 start 하자마자 바로 탈출하기 때문에 `before` 가 먼저 출력됩니다.

<p ailgn="center">
<img width ="600" src="https://velog.velcdn.com/images/murjune/post/cc297bd9-36ee-45d6-9a40-e12b968d6ddb/image.png" />
</p>

- 코루틴 스코프 함수 (coroutineScope)
```kotlin
fun main() = runBlocking {
coroutineScope {
delay(10)
println("after") // 먼저 호출
}
println("before")
}
```

coroutineScope 는 suspend 함수이기 때문에 종료할때까지 `호출자 코루틴은 일시중단` 됩니다.
그리고, `after` 가 출력된 이후에 coroutineScope 종료되고 `before` 가 출력됩니다.

<p ailgn="center">
<img width ="600" src="https://velog.velcdn.com/images/murjune/post/ac013c7b-890e-49d9-bf44-2c3b604f79ec/image.png" />
</p>

이를 통해 코루틴 빌더 함수(launch)와 달리 코루틴 스코프 함수(coroutineScope)는 호출자 코루틴과 동기적으로 실행된다는 것을 알 수 있습니다😁

## 2. supervisorScope

> 1) ⭐️ 호출자의 코루틴 컨텍스트를 받아, 호출자 코루틴과 부모-자식관계를 보장한다.
> 2) ⭐️ 호출자 코루틴은 끝날때까지 일시 중단된다.
> 3) ⭐️ 자식 코루틴의 작업이 끝날때까지 대기한다.
> 4) 일반 함수와 같은 방식으로 예외를 던진다.
> 5) 결과값을 반환한다.
supervisorScope 함수는 `coroutineScope` 와 같은 코루틴 스코프 함수입니다.
그렇기에 `coroutineScope` 와 같이 위와 같은 특징을 갖는데요, 추가적으로`supervisorScope``자식 코루틴의 예외 전파를 제한`해줍니다.

![](https://velog.velcdn.com/images/murjune/post/dfa2c4ef-eb66-49be-b190-8f83c1f27be4/image.png)

그 이유는 `supervisorScope` 는 내부적으로 `SupervisorJob` 을 가지고 있기 때문입니다.

간단한 예시로 확인해볼까요?

```kotlin
suspend fun foo() = supervisorScope {
launch { println("Child 1") }
launch { error("예외 발생 😵") }
launch { println("Child 2") }
launch { println("Child 3") }
}

suspend fun main() {
println("시작")
foo()
println("")
}
```

<p ailgn="center">
<img width ="600" src="https://velog.velcdn.com/images/murjune/post/8da61ac0-feb4-4387-b6c1-c36d6056a7a5/image.png" />
</p>

child 코루틴에서 예외가 발생해도 부모 코루틴에 예외를 전파시키지 않고 있네요!

> 위 예제의 `supervisorScope``coroutineScope` 로 바꿔서 실행한 결과와 비교해보시길 추천드려요
추가적으로 `supervisorScope`을 처음 사용하면 자주 오인하는 부분이 있습니다.

#### supervisorScope {} 내부에서 발생하는 예외는 전파합니다. 자식 코루틴의 예외를 전파 제한합니다.

```kotlin
suspend fun foo() = supervisorScope {
error("예외 전파 제한 못함 🤯")
launch { println("Child 1") }
launch { println("Child 2") }
}
```

![](https://velog.velcdn.com/images/murjune/post/e9f3dc9e-677d-4e3d-83d0-abb7a4aeea44/image.jpg)

헷갈릴 수 있는 부분이기에 주의해주세요 😎


## 3. 이미지 업로드 예제 supervisorScope 로 리팩토링

```kotlin
suspend fun uploadImages(localImagePaths: List<String>): List<String?> = coroutineScope {
val supervisor = SupervisorJob(coroutineContext.job) // 부모 코루틴 설정
val result = localImagePaths.map { localImagePath ->
async(supervisor) { uploadImage(localImagePath) } { uploadImage(localImagePath) }
}.map {
try { // await() 예외 처리
it.await()
} catch (e: IllegalStateException) {
null
}
}
supervisor.complete() // supervisor 명시적 종료
result
}
```
[코루틴 예외 전파 제한 왜 하는거지?](https://velog.io/@murjune/kotlin-Coroutine-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%98%88%EC%99%B8-%EC%A0%84%ED%8C%8C-%EC%A0%9C%ED%95%9C-%EC%99%9C-%ED%95%98%EB%8A%94%EA%B1%B0%EC%A7%80with-SupervisorJob) 에서 사용한 다수의 이미지 업로드하는 예제에요.

SupervisorJob() 으로 생성된 Job 의 특징 때문에 아래와 같은 작업을 처리해줬습니다.

```kotlin
// 1. 부모-자식 관계 설정
val supervisorJob = SupervisorJob(coroutineContext.job)
CoroutineScope(supervisorJob).launch { .. }
// 2. supervisor 명시적 종료
supervisorJob.complete()
```
딱 봐도 복잡해보이죠? 😤
이제 supervisorScope 를 활용해서 이미지 업로드 예제를 리팩토링해봅시다.

```kotlin
suspend fun uploadImages(localImagePaths: List<String>): List<String?> = supervisorScope {
localImagePaths.map { localImagePath ->
async { uploadImage(localImagePath) } { uploadImage(localImagePath) }
}.map {
try {
it.await()
} catch (e: IllegalStateException) {
null
}
}
}
```

훨씬 깔끔해졌죠? (try-catch 문 때문에 그렇게 안보일 수도 있겠지만 😅)
이렇듯, 독립적인 작업들을 병렬 처리해야할 경우에 supervisorScope 를 많이 사용합니다.

> supervisorScope 을 사용한 예시를 좀 더 보고 싶으시면 [suspend 함수를 Effective 하게 설계하자!](https://velog.io/@murjune/Kotlin-Coroutine-suspend-%ED%95%A8%EC%88%98%EB%A5%BC-Effective-%ED%95%98%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90-fsaiexxw) 의 마지막 챕터를 봐주세요~
## 4. 정리

> - supervisorScope
> 1) 호출자의 코루틴 컨텍스트를 받아, 호출자 코루틴과 부모-자식관계를 보장한다.
> 2) 호출자 코루틴은 끝날때까지 일시 중단된다.
> 3) 자식 코루틴의 작업이 끝날때까지 대기한다.
오늘은 코루틴 스코프 함수에 대해 간략하게 알아보고 그 중 `supervisorScope` 를 활용해 예외를 전파하는 방법에 대해 알아 봤어요.

`SupervisorJob`, `supervisorScope` 둘다 배워보니 `supervisorScope`가 훨씬 사용하기 쉽고 편하다는 생각이 들거에요. 그럼 예외를 전파할때는 무조건 supervisorScope 만을 사용하면 될까요? 🤔

다음 포스팅에서는 `supervisorJob``supervisorScope`의 차이점을 비교하고, 각각을 적절하게 사용하는 방법과 실제 사용 사례를 소개해드리겠습니다. 👋


Loading

0 comments on commit 73f5c69

Please sign in to comment.