diff --git a/coroutine/docs/README.md b/coroutine/docs/README.md new file mode 100644 index 0000000..937e4ff --- /dev/null +++ b/coroutine/docs/README.md @@ -0,0 +1,19 @@ +# 테스트 +- [ ] 테스트 코드를 작성해야하는 이유 +- [ ] Fake vs Mock +- [ ] 좋은 테스트를 작성하는 법 +- [ ] 코루틴 테스트하는법 +- [ ] 플로우 테스트하는법 +- [ ] ViewModel 테스트하는법 +- [ ] 테스트하기 어려운 코드 테스트하는법 + +# 코루틴 예외 +- [x] 코루틴 예외 전파 +- [x] 코루틴 예외 전파 방지 - SupervisorJob +- [ ] 코루틴 예외 전파 방지 - supervisorScope +- [ ] 코루틴 예외 전파 방지 - supervisorScope vs SupervisorJob +- [ ] 코루틴 예외 처리 - CoroutineExceptionHandler +- [ ] 코루틴 예외 처리 - 코루틴 스코프 함수 + try-catch + +# 코루틴 +- [ ] suspend function 을 잘 써보자 \ No newline at end of file diff --git "a/coroutine/docs/coroutine/coroutine_context/\354\275\224\353\243\250\355\213\264_\353\224\224\354\212\244\355\214\250\354\262\230.md" "b/coroutine/docs/coroutine/coroutine_context/\354\275\224\353\243\250\355\213\264_\353\224\224\354\212\244\355\214\250\354\262\230.md" new file mode 100644 index 0000000..1f6eb86 --- /dev/null +++ "b/coroutine/docs/coroutine/coroutine_context/\354\275\224\353\243\250\355\213\264_\353\224\224\354\212\244\355\214\250\354\262\230.md" @@ -0,0 +1,15 @@ +코루틴 컨택스트의 일종 + +일반 디스패처 특징 : 코루틴의 실행을 요청받으면 작업 대기열에 적재한 후 스레드풀에서 사용할 수 있는 스레드로 보낸다 +무제한 디스패처 특징: 스레드 스위칭 없이 호출자 스레드에서 바로 실행 + +무제한 디스패처: 재개될 때 코루틴을 재개하는 스레드에서 재개된 + +일반적으로 재개될 때 코루틴은 코루틴 컨택스에 존재하는 Dispatcher 에 의해 재분배된다. (Continuation 안에 있는 coroutineContext 속에 있는 Dispatcher 를 통해) +그러나 무제한 디스패처는 resume 될 때 재개가 실행되는 스레드에서 재실행됨 (예측이 불가능한 비동기 작업이됨 그래서 실 프로덕트 환경에서는 쓰지 않음) + + + +# CPS + +코루틴은 코루틴의 실행 정보를 저장하고 전달할 때 CPS(Continuation Passing Style) 기법을 채택하고 있다. diff --git "a/coroutine/docs/coroutine/coroutine_context/\354\275\224\353\243\250\355\213\264_\354\212\244\354\275\224\355\224\204.md" "b/coroutine/docs/coroutine/coroutine_context/\354\275\224\353\243\250\355\213\264_\354\212\244\354\275\224\355\224\204.md" new file mode 100644 index 0000000..dca45a8 --- /dev/null +++ "b/coroutine/docs/coroutine/coroutine_context/\354\275\224\353\243\250\355\213\264_\354\212\244\354\275\224\355\224\204.md" @@ -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) 실행 범위를 제어하는 역할 \ No newline at end of file diff --git "a/coroutine/docs/coroutine/coroutine_context/\354\275\224\353\243\250\355\213\264_\354\273\250\355\203\235\354\212\244\355\212\270.md" "b/coroutine/docs/coroutine/coroutine_context/\354\275\224\353\243\250\355\213\264_\354\273\250\355\203\235\354\212\244\355\212\270.md" new file mode 100644 index 0000000..a136131 --- /dev/null +++ "b/coroutine/docs/coroutine/coroutine_context/\354\275\224\353\243\250\355\213\264_\354\273\250\355\203\235\354\212\244\355\212\270.md" @@ -0,0 +1,11 @@ +# 코루틴 컨택스트 + +코루틴 컨택스트는 코루틴의 실행 환경을 그룹화하여 저장하고 전달하는 객체이다. +이를 활용해 코루틴의 실행 상태가 어떤지, 어떤 스레드에 분배 받을지 등 `코루틴의 작동 방식`을 정합니다. +- Job: 코루틴의 실행 상태를 나타내고 코루틴의 실행을 제어할 수 있다. +- Dispatcher: 코루틴이 어떤 스레드에서 보내져 동작할지 지정한다. +- CoroutineExceptionHandler: 예외 처리기 + +코루틴 컨택스트는 여러 Element 로 구성되어 있는데 대표적으로 Dispatcher, Job, CoroutineExceptionHandler 가 있겠다. +코루틴은 자신의 컨택스트를 자식에게 전달한다.(자식이 부모의 컨텍스트를 상속받는다.) +그리고, 자식은 상속받은 부모의 컨텍스트를 대체한다. \ No newline at end of file diff --git "a/coroutine/src/main/kotlin/com/murjune/practice/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270_\354\240\204\355\214\214.md" "b/coroutine/docs/coroutine/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270_\354\240\204\355\214\214.md" similarity index 97% rename from "coroutine/src/main/kotlin/com/murjune/practice/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270_\354\240\204\355\214\214.md" rename to "coroutine/docs/coroutine/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270_\354\240\204\355\214\214.md" index a3683fd..1bdc043 100644 --- "a/coroutine/src/main/kotlin/com/murjune/practice/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270_\354\240\204\355\214\214.md" +++ "b/coroutine/docs/coroutine/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270_\354\240\204\355\214\214.md" @@ -9,6 +9,16 @@ 만약, 해당 글이나 테코톡을 보고 궁금하신 점이나 함께 논의하고 싶은 부분이 있다면 댓글이나 메일로 남겨주세요 😁 +> 아래 지식들을 알고 있으면 해당 글을 이해하기 쉬울 거에요! +> - CoroutineContext +> - Job, launch, async +> - CoroutineScope +> - 코루틴 구조화된 동시성 +> - 코루틴 취소 메커니즘 +> - suspend function +> - coroutineScope + + ## 코루틴 예외 처리의 중요성 kotlin을 활용하는 대부분의 프로그램(대표적으로 Android) 에서는 [Coroutine](https://kotlinlang.org/docs/coroutines-overview.html) 을 활용하여 비동기 처리하고 있다. diff --git "a/coroutine/src/main/kotlin/com/murjune/practice/exception/supervisor/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200.md" "b/coroutine/docs/coroutine/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200_SupervisorJob.md" similarity index 91% rename from "coroutine/src/main/kotlin/com/murjune/practice/exception/supervisor/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200.md" rename to "coroutine/docs/coroutine/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200_SupervisorJob.md" index abcbf0e..83e0935 100644 --- "a/coroutine/src/main/kotlin/com/murjune/practice/exception/supervisor/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200.md" +++ "b/coroutine/docs/coroutine/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200_SupervisorJob.md" @@ -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을 받아오는 예제를 통해 `예외 전파 제한이 필요성`에 대해 알아볼 것이에요!😎

diff --git "a/coroutine/docs/coroutine/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200_supervisorScope.md" "b/coroutine/docs/coroutine/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200_supervisorScope.md" new file mode 100644 index 0000000..d832f69 --- /dev/null +++ "b/coroutine/docs/coroutine/exception/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200_supervisorScope.md" @@ -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` 가 먼저 출력됩니다. + +

+ +

+ +- 코루틴 스코프 함수 (coroutineScope) +```kotlin +fun main() = runBlocking { + coroutineScope { + delay(10) + println("after") // 먼저 호출 + } + println("before") +} +``` + +coroutineScope 는 suspend 함수이기 때문에 종료할때까지 `호출자 코루틴은 일시중단` 됩니다. +그리고, `after` 가 출력된 이후에 coroutineScope 종료되고 `before` 가 출력됩니다. + +

+ +

+ +이를 통해 코루틴 빌더 함수(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("끝") +} +``` + +

+ +

+ +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): List = 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): List = 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`의 차이점을 비교하고, 각각을 적절하게 사용하는 방법과 실제 사용 사례를 소개해드리겠습니다. 👋 + + diff --git "a/coroutine/docs/coroutine/suspend_func_effective_\355\225\230\352\262\214\354\202\254\354\232\251.md" "b/coroutine/docs/coroutine/suspend_func_effective_\355\225\230\352\262\214\354\202\254\354\232\251.md" new file mode 100644 index 0000000..089997f --- /dev/null +++ "b/coroutine/docs/coroutine/suspend_func_effective_\355\225\230\352\262\214\354\202\254\354\232\251.md" @@ -0,0 +1,494 @@ +> - 해당 포스팅에서 언급하는 코루틴은 [kotlin-coroutine](https://kotlinlang.org/docs/coroutines-overview.html) 입니다. +> - 코루틴에 대한 기본 지식이 없다면 해당 포스팅을 이해하기 어려울 수 있습니다. +> - 내부적으로 suspend function 이 어떻게 중단/재개되는지 다루지 않습니다. +> - 이번 포스팅과 [suspend function 공식문서](https://kotlinlang.org/docs/composing-suspending-functions.html#structured-concurrency-with-async) 을 함께 읽는 것을 추천해요 ⭐️ + +## Intro +간단한 예제를 통해 기본적인 suspend function 활용법에 대해 알아보자 + +```kotlin +// super 개발자의 삶..🫢 +fun main() = runBlocking { + println("아침에 일어난다") + println("밥을 먹는다.") + delay(100) + println("코딩하기") + println("밥을 먹는다.") + delay(100) + println("코딩하기") + delay(100) + println("밥을 먹는다.") + delay(100) + println("코딩하기") + delay(100) + println("잠을 잔다.") +} +``` +현재 "코딩하기"와 "밥을 먹는다." 코드가 중복해서 사용되고 있다. 이런 경우, 함수화를 통해 코드구조를 개선시키고 싶을 것이다. + +일반 함수의 경우 일시 중단을 지원하지 않기 때문에 코루틴을 일시 중단하는 `delay()` 를 호출 할 수 없다.(참고로 delay() 함수도 suspned 함수이다.) + + +

+ +

+ + +이럴 때, `일시 중단가능한` suspend function 을 사용해 함수화를 해주면 된다. + +

+ +

+ + +```kotlin +suspend fun coding() { + println("코딩하기") + delay(100) +} + +suspend fun eat() { + println("밥을 먹는다.") + delay(100) +} + +fun main() = runBlocking { + println("아침에 일어난다") + eat() + coding() + eat() + coding() + eat() + coding() + println("잠을 잔다.") +} +``` +이처럼 suspend function 은 코루틴을 활용한 복잡한 비동기 코드를 구조화하여 재사용성과 가독성을 위해 사용된다. + +이제 suspend function 여러 테스트 케이스를 통해 suspend function 의 여러가지 사용법에 대해 알아보자 😁 + +> 만약, 코루틴 테스트를 처음 접하는 독자가 있다면 다음 포스팅을 먼저 읽고 오길 추천한다 +[코루틴 테스트 쌩기초 탈출하기 💪](https://velog.io/@murjune/kotlin-Coroutine-%EC%BD%94%EB%A3%A8%ED%8B%B4-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8C%A9%EA%B8%B0%EC%B4%88-%ED%83%88%EC%B6%9C%ED%95%98%EA%B8%B0) + + +## 1) Suspend 함수는 코루틴이 아니다. + +```kotlin +suspend fun suspendFuncA() { + delay(300) + println("Hello") +} + +suspend fun suspendFuncB() { + delay(200) + println("Odooong") +} +``` + +간단한 suspend 함수 `suspendFuncA` , `suspendFuncA` 가 있다. 각각, 300ms, 200ms delay를 주었다. + +```kotlin +@Test +fun `Suspend 함수는 코루틴이 아니다`() = runTest { + val job = launch { + suspendFuncA() + suspendFuncB() + } + advanceUntilIdle() // 현재 testScope 내부 코루틴 작업이 모두 끝날 때까지 대기 + currentTime shouldBe 500 + // output: Hello Odooong +} +``` +해당 테스트는 몇 초 뒤에 종료될까? 🤔 300ms? 500ms? +한 번 생각해보시죠 ㅎ ㅎ +. +. +. +. +`suspendFuncA()` 과 `suspendFuncB()` 가 서로 독립적인 코루틴이라 생각하고, suspend 함수들이 비동기적으로 실행되어 `300ms` 만큼 시간이 걸릴 것이라 예상한 독자들도 있을 것이다. + + +![](https://velog.velcdn.com/images/murjune/post/c7711cff-34ee-43bc-9f8a-dd889bcb87a4/image.png) + + +그러나, 해당 코드는 위 그림처럼 실행된다. +** suspend function 이 종료될 때까지 호출부 코루틴(runBlocking)은 blocking ** 되기에 해당 테스트는 총 `500ms` 만큼 시간이 걸린다. + +위 테스트 코드는 아래 코드와 완전히 동일하다. + +```kotlin +@Test +fun `suspend function 은 코드 블록에 불과하다 - 위 테스트 함수와 완전히 동일`() = runTest { + val job = launch { + delay(300) + println("Hello") + delay(200) + println("Odooong") + } + advanceUntilIdle() + currentTime shouldBe 500 + // output: Hello Odooong +} +``` + +#### Suspend function 은 코루틴이 아니다. 호출부 코루틴 내에서 돌아가는 중단 가능한 코드 블럭에 불과하는 점을 잊지 말자 + +> Intro 에서 다룬 예제와 비슷한데 자주 오해할 수 있는 내용이라 한 번 더 강조하기 위해 다뤘다. + +## 2) suspend function 에서 병렬 처리할 때, CoroutineScope를 사용하지 말자 + +이번에는 suspendFuncA() 외 suspendFuncB() 를 병렬처리 해보자 + +```kotlin +@Test +fun `비동기 처리 - 동시성`() = runTest { + val job = launch { + val childA = launch { + suspendFuncA() + } + val childB = launch { + suspendFuncB() + } + } + advanceUntilIdle() + currentTime shouldBe 300 + // output: Odooong Hello +} +``` + +빌드 시 suspendFuncA()와 suspendFuncB() 를 동시에 실행시키기 때문에 해당 테스트는 `300ms` 만큼 걸린다. + + + +이때, 인덴트 깊이가 늘어나는 것이 가독성을 해친다고 생각하여 다음과 같이 `launch{}` 을 suspend 함수 내로 분리하고 싶은 욕구가 들 수도 있다. + +

+ +

+ + + +그러나, launch() 함수는 CoroutineScope의 확장 함수이기에 suspend function에서 바로 호출이 불가능하다. launch() 함수를 사용하기 위해서는 새로운 [CoroutineScope](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/) 를 통해 열어줘야 한다. + +

+ +

+ + +**여기서, 많은 사람들이 CoroutineScope 를 사용하는 실수를 한다. ** + +

+ +

+ +CoroutineScope 를 사용하면서 구조화된 동시성을 유지해주기 위해선 [CoroutineContext](https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html) 를 넣어줘야한다. 이때, 다음과 같이 `Dispatcher.IO` 혹은 `EmptyCoroutineContext` 를 넣어주곤 한다. + +

+ +

+ +해당 코드는 문제가 없을까? + +![](https://velog.velcdn.com/images/murjune/post/75a4eb30-cadf-40aa-88fa-a420377582b8/image.png) + +❌ 아니다. 해당 코드에는 2가지 문제점이 있다. +- 1) 호출자의 코루틴과 구조화된 동시성이 깨진다 +- 2) suspend function 이 종료되어도 코루틴은 동작한다.(즉, 비동기적으로 코드가 동작한다) + +CoroutineScope()은 매개변수에 호출자의 [Job](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/) or Job을 포함하는 CoroutineContext를 넘겨주지 않으면, 호출부 코루틴과 독립적인 코루틴 환경을 구축한다. + +

+ +

+ + +따라서, 위 그림처럼 호출부의 코루틴과의 구조화된 동시성이 깨지게 된다. + +```kotlin +// suspend 키워드도 빼도 된다 +fun suspendFunc() { + val independantJob = CoroutineScope(Dispatchers.IO).launch { + delay(200) + println("Hellow Odooong") + } +} + +@Test +fun `다른 코루틴 디스패처를 사용하면 구조화된 동시성이 깨져, 독립된 코루틴이 된다`() = runTest { + // runTest 과 아래 suspendFunc() 에서 생성된 코루틴은 별개이다. + suspendFunc() + advanceUntilIdle() + currentTime shouldBe 0 + // Hellow Odooong 이 호출되지 않음 +} +``` + +부모 코루틴은 자식 코루틴이 모두 종료될때까지 대기하는 특성을 가지고 있다. 그러나, CoroutineScope를 사용했기에 runTest 코루틴(부모 코루틴)과의 구조를 깨버렸기에 runTest 코루틴은 independantJob이 끝날 때까지 대기하지 않는다. + +

+ +

+ + +테스트를 실행시켜보면 `advanceUntilIdle()` 를 호출했음에도 runTest 가 바로 종료되는 것을 볼 수 있다. + +- coroutineContext 활용 + + +```kotlin +suspend fun suspendFuncAWithCoroutineScope() { + CoroutineScope(coroutineContext + CoroutineName("ChildA")).launch { + delay(300) + print(" Hello ") + } +} + +suspend fun suspendFuncBWithCoroutineScope() { + CoroutineScope(coroutineContext + CoroutineName("ChildB")).launch { + delay(200) + print(" Odooong ") + } +} +``` +coroutineContext property 를 활용하면 현재 실행되고 있는 코루틴의 context를 가져올 수 있다. CoroutineScope 에 `coroutineContext`를 넣어주면 runTest 코루틴과 `구조화된 동시성`을 유지할 수 있다. + +

+ +

+ + +그럼 테스트 코드를 다음과 같이 깔끔하게 나타낼 수 있다. + +```kotlin +@Test +fun `suspend 함수 내에 코루틴 스코프를 열어 자식 코루틴 생성 - 동시성`() = runTest { + suspendFuncAWithCoroutineScope() + suspendFuncBWithCoroutineScope() + advanceUntilIdle() + currentTime shouldBe 300 + // Output: Odooong Hello +} +``` + +> 휴! 시간도 `300ms` 로 단축했고, 가독성도 챙겼으니 해당 코드는 좋은 코드일까? 🤔 + +![](https://velog.velcdn.com/images/murjune/post/75a4eb30-cadf-40aa-88fa-a420377582b8/image.png) + +❌ 언듯 보기에는 좋아보일 수 있으나, 잘못 설계한 것이다. +동료개발자는 suspend 함수가 종료되는 시점에 내부 작업들이 끝났을 것이라 예상할 것이다. + +

+ +

+ +그러나, 실상은 그림과 같이 suspend 함수는 자식 코루틴을 만들고 바로 종료되고, 자식 코루틴들은 비동기적으로 실행되고 있다. + + + +이는 심각한 버그의 원인이 될 수 있으며, 어디서 발생한 버그인지 찾기도 매우 힘들다.🥲 + +> 위 코드와 비슷한 형태로 설계된 코드가 때문에 버그가 발생하여 쌩고생한 경험이 있다 🥲 + +- ** suspend function 에서 CoroutineScope 를 사용하지 말자 ** +- ** suspend function 실행이 종료되었을 때, 내부 코드의 실행이 완료되도록 설계하자! ** + +## 3) coroutineScope or withContext 함수를 활용하자!⭐️ + +suspend function 내부 동작을 병렬 처리하고 싶다면, [coroutineScope](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) 를 활용하자!(대문자 CoroutineScope 말고 소문자 coroutineScope 라는 함수가 있다) + +```kotlin +@Test +fun `suspend 함수 분리 - 동시성`() = runTest { + mergedSuspendFunc() + currentTime shouldBe 300 + // Output: Odooong Hello +} + +suspend fun mergedSuspendFunc() = coroutineScope { + launch(CoroutineName("ChildA")) { + suspendFuncA() + } + launch(CoroutineName("ChildB")) { + suspendFuncB() + } +} +``` +coroutineScope를 사용하면 `runTest 코루틴`는 `mergedSuspendFunc()` 가 종료될 때까지 대기하고, `mergedSuspendFunc()` 내부에서는 병렬적으로 코드를 실행하도록 할 수 있다. + +

+ +

+ +코루틴 관계도는 다음과 같다 + +

+ +

+ +코드 구조와 시간적 효율성 2마리 토끼를 모두 잡을 수 있게 되었다 😁 + +> [coroutineScope](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) 의 특성 +> - 호출부의 `coroutineContext` 를 상속받는 Job을 생성하기에 코루틴의 구조화된 동시성을 깨지 않는다. +- 자식 코루틴이 모두 끝날때까지 호출부의 코루틴을 blocking 시킨다는 특징이 있다.(suspend function과 찰떡궁합이다.) +- 코드 블럭의 마지막 값을 return 한다. + + 만약, 다른 디스패처를 활용하고 싶다면 [withContext](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html)를 활용해주면 된다. 디스패터를 지정해줄 수 있다는 점을 제외하고 coroutineScope와 동일한 기능을 한다. + +```kotlin +suspend fun mergedSuspendFunc() = withContext(Dispatcher.IO) { + launch { + suspendFuncA() + } + launch { + suspendFuncB() + } +} +``` + +--- + +suspend 함수 내부에서 **병렬 처리를 하고 결과값을 반환**해야 할 경우가 종종 생긴다. 이런 경우 coroutineScope 와 [async](https://www.google.com/search?q=async+coroutine&sca_esv=94e45fce1d51b060&sxsrf=ADLYWIJ8zw8Iclt335PJ9p5eyK67-80qfw%3A1719349228477&ei=7C97Zv_qHKu1vr0PkYCUgAs&ved=0ahUKEwj_kIiA0_eGAxWrmq8BHREABbAQ4dUDCA8&uact=5&oq=async+coroutine&gs_lp=Egxnd3Mtd2l6LXNlcnAiD2FzeW5jIGNvcm91dGluZTIFEAAYgAQyBRAAGIAEMgUQABiABDIEEAAYHjIGEAAYHhgPMgQQABgeMgQQABgeMgYQABgFGB4yBhAAGAUYHjIGEAAYCBgeSO8bUNACWPIacAN4AZABAZgB2QKgAbwUqgEHMS43LjQuMrgBA8gBAPgBAZgCD6ACqhDCAgoQABiwAxjWBBhHwgILEAAYgAQYsQMYgwHCAggQABiABBiiBJgDAIgGAZAGCpIHBzQuNy4zLjGgB9pI&sclient=gws-wiz-serp) 를 함께 사용하면 된다.(Like 분할정복) + + +우테코 선릉 캠퍼스 크루들을 fetch 해오는 예시를 통해 알아보자! +```kotlin +suspend fun fetchWootecoAndroidCrews(): List { + delay(300) + return listOf("오둥이", "꼬상", "하디", "팡태", "악어", "케이엠") +} + +suspend fun fetchWootecoFrontendCrews(): List { + delay(200) + return listOf("토다리", "제이드") +} +``` +안드 크루와 프론트 크루를 불러오는 api가 있다 +두 함수의 실행 결과는 서로 독립적이기 때문에 병렬 처리하기에 매우 적합하다 😁 + +```kotlin +suspend fun fetchWootecoCrews(): List = coroutineScope { + val androidJob = async { fetchWootecoAndroidCrews() } + val frontJob = async { fetchWootecoFrontendCrews() } + // 결과값 반환 + androidJob.await() + frontJob.await() +} +``` +따라서, `async{}`로 각 함수가 서로 다른 코루틴에서 실행되도록 묶어준 후, 결과값을 반환해주는 부분에 `await()`를 호출해준다. + +```kotlin +@Test +fun `우테코 선릉 캠퍼스 크루들 불러오기`() = runTest { + val crews = fetchWootecoCrews() + currentTime shouldBe 300 + crews shouldContainExactlyInAnyOrder listOf("오둥이", "꼬상", "하디", "팡태", "악어", "케이엠", "토다리", "제이드") +} +``` + +이처럼 어떤 값을 반환하고 내부적으로 병렬처리를 하는 suspend function 을 설계할 때 `coroutineScope + async`를 활용해보자 😁 + +필자는 우테코 쇼핑 주문하기 미션에서 적용가능한 쿠폰을 불러오는 UseCase 에서 `coroutineScope + async` 를 활용하여 병렬처리를 적용해본 적이 있다. + +[해당 코드](https://github.com/murjune/android-shopping-order/blob/step2/app/src/main/java/woowacourse/shopping/domain/usecase/order/LoadAvailableDiscountCouponsUseCase.kt) + +## 4) supervisorScope 사용을 고려해보자(심화) + +suspend function 내에서 `coroutineScope` 와 `async/launch` 를 활용하여 여러 api 들을 병렬 처리할 때 한가지 제약이 있다. +100개의 api를 통합하는 suspend function이 있고, 자식 코루틴이 한개라도 exception이 터진다면 예외가 전파되어 모든 코루틴이 cancel된다는 것이다. + +이때, 기획에서는 통신에 성공한 데이터라도 불러와달라고 요청을 했다! 그럼 어떻게 처리하는 것이 적절할까? +이번에는 우테코 코치님들을 불러오는 예시를 통해 알아보자 + +```kotlin +suspend fun fetchAndroidCoaches(): List { + delay(50) + return listOf("제이슨", "레아", "제임스") +} + +suspend fun fetchFrontCoaches(): List { + delay(150) + return listOf("준", "크론") +} + +suspend fun fetchBackCoaches(): List { + delay(70) + throw NoSuchElementException("제가 백엔드 코치님들은 모릅니다..") +} + + +``` +현재 `fetchBackCoaches()` 함수에서 NoSuchElementException 예외를 발생시키고 있다. + +```kotlin +@Test +fun `하나라도 예외가 발생하면, 모든 작업이 취소된다`() = runTest { + shouldThrow { + fetchErrorWootecoCoaches() + } + currentTime shouldBe 70 +} + +suspend fun fetchWootecoCoaches() = coroutineScope { + val androidJob = async { fetchAndroidCoaches() } + val frontJob = async { fetchFrontCoaches() } + val backendJob = async { fetchBackCoaches() } + androidJob.await() + frontJob.await() + backendJob.await() +} +``` + +

+ +

+ + +`fetchWootecoCoaches()` 를 불러올 때 예외가 `fetchBackCoaches -> coroutineScope -> runTest` 로 예외가 전파되며, 모든 작업들은 취소가 된다. + + + + +이럴 때 에러 전파를 방지하기 위해 [supervisorScope{}](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html) 를 활용할 수 있다. + +> supervisorScope 는 coroutineScope 와 거의 똑같지만, 자식 코루틴의 예외 전파를 차단시킨다는 특성이 있다. + +이를 활용하여 다음과 같이 개선할 수 있다. +```kotlin +suspend fun fetchWootecoCoaches() = supervisorScope { + val androidJob = async { fetchAndroidCoaches() } + val frontJob = async { fetchFrontCoaches() } + val backendJob = async { fetchBackCoaches() } + // result + val backendResult = runCatching { backendJob.await() } + androidJob.await() + frontJob.await() + backendResult.getOrDefault(emptyList()) +} +``` +기존 coroutineScope가 supervisorScope로 교체된 것 외에도, `await()` 하는 부분에서 runCatching으로 예외 처리를 해주는 것을 볼 수 있다. + +async 으로 인해 만들어진 코루틴에서 Exception이 발생하면 `1) 전파되는 예외 + 2) await() 에서 발생하는 예외`가 모두 처리 해줘야하기 때문이다. + +```kotlin +@Test +fun `supervisorScope 를 활용하여 Error 전파 방지`() = runTest { + val coaches = fetchWootecoCoaches() + currentTime shouldBe 150 + coaches shouldContainExactlyInAnyOrder listOf("제이슨", "레아", "제임스", "준", "크론") +} +``` + +

+ +

+ + +여러 코루틴 성공한 값들만 불러와 반환하도록 개선해주었다! + +## 정리 + +> +- suspend function 코루틴이 아니다. +- suspend function 은 호출부 코루틴의 코드블럭이다. +- suspend function 이 종료될 때, 내부 실행 코드도 종료되도록 설계하자 +- suspend function 내부에서 병렬 처리를 할 떄, 구조화된 동시성을 보장해주기 위해 coroutineScope/withContext 를 사용하자 +- 자식 코루틴의 예외 전파를 방지하고 싶다면, coroutineScope 대신 supervisorScope를 사용하자 + + +긴 글 읽어주셔서 감사합니다. +이해가 안되거나 피드백 주실 부분이 있다면 편하게 댓글로 부탁드려요!! 🙇‍♂️ diff --git "a/coroutine/docs/coroutine/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200_SupervisorJob_vs_supervisorScope.md" "b/coroutine/docs/coroutine/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200_SupervisorJob_vs_supervisorScope.md" new file mode 100644 index 0000000..701ccee --- /dev/null +++ "b/coroutine/docs/coroutine/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\240\204\355\214\214_\353\260\251\354\247\200_SupervisorJob_vs_supervisorScope.md" @@ -0,0 +1,172 @@ + +!youtube[3DNbRnl0im4] + +테코톡에서는 [8:13 ~ 9: 00] 에 해당하는 내용입니다. + +지난 시간에 배운 내용 정리~ +> - SupervisorJob +> 1) SupervisorJob 은 자식 코루틴의 예외 전파를 방지한다. +> 2) SupervisorJob() 로 생성된 SupervisorJob 은 root Job 이 된다. +> 3) SupervisorJob() 로 생성된 SupervisorJob 은 항상 active 하다. +> +> - supervisorScope +> 1) `자식 코루틴의 예외 전파를 방지`한다. +> 2) 호출자의 코루틴 컨텍스트를 받아, 호출자 코루틴과 부모-자식관계를 보장한다. +> 3) 호출자 코루틴은 끝날때까지 일시 중단된다. +> 4) 자식 코루틴의 작업이 끝날때까지 대기한다. + +> - 이전 포스팅 + > [1. 코루틴 예외가 전파되는 방식](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) + > [2. 코루틴 예외 전파 제한 왜 하는거지?(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) + > [3. 코루틴 예외 전파 제한 (supervisorScope)](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-supervisorScope) + +# Intro + +지난 포스팅에서 `SupervisorJob()` 과 `supervisorScope` 에 대해 알아보았습니다. +`supervisorScope` 이 자동으로 호출자 코루틴과의 부모-자식 관계를 보장해주기에 사용하기 훨씬 편합니다. + +> `supervisorScope` 사용하기 편하다는 거 알겠어 그럼 `SupervisorJob()`는 언제 씀? 🤔 + +그럼 위와 같은 생각이 들 수도 있는데요, `supervisorJob`과 `supervisorScope`의 차이점을 비교하고, 각각을 적절하게 사용하는 방법과 실제 사용 사례를 소개해드리겠습니다. 👋 + +# 1. 일반적인 경우에는 supervisorScope 를 사용하자 + +SupervisorJob 과 supervisorScope 를 사용해 예외 전파를 방지하는 예제입니다. + +```kotlin +SupervisorJob 예시 + +supervisorScope 예시 +``` + +`SupervisorJob()` 을 사용하면 구조화가 무너지기에 추가적인 설정들이 필요합니다. +확실히 `supervisorScope` 를 사용하는 것이 편해보이네요 + +그래서, 코루틴 내부(코루틴 스코프 내부)나 suspend 함수에서는 `supervisorScope` 를 활용하여 예외 전파 방지하는 것이 훨씬 낫습니다. + +# 2. CoroutineScope 를 생성할 때 SupervisorJob 을 사용하자 + +supervisorScope 는 suspend 함수이기에, 일반함수에서는 사용할 수 없다는 제약이 있습니다. + +> 참고) suspend 함수는 같은 suspend 함수나 코루틴 내부(코루틴 스코프 내부)에서만 호출할 수 있습니다. +> +> ![](https://velog.velcdn.com/images/murjune/post/9a9800dc-8405-430c-a21a-f3b734c57f40/image.png) + +그래서, 일반 함수에서 새롭게 코루틴을 생성하고 사용할 때는 `CoroutineScope` 의 coroutineContext 에 `SupervisorJob()` 을 지정해줘야합니다. + +CoroutineScope 에 SupervisorJob() 을 지정안해주면 어떤 문제가 있을까요? + + +```kotlin +val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> println("예외 발생") }) + +fun loadImages() = scope.launch { println("이미지..") } + +fun loadUsers() = scope.launch { error("error 😵") } + +fun loadCustomers() = scope.launch { println("손님..") } +``` + +이미지, 유저, 손님 정보를 비동기적으로 동시에 불러오고 있습니다. 그리고, 다음 시간에 배울 CoroutineExceptionHandler 를 통해 예외 처리도 해주고 있습니다. + +````kotlin +loadImages() +loadUsers() +loadCustomers() +```` +실행하면 어떻게 될까요? + +![](https://velog.velcdn.com/images/murjune/post/af0ba774-2bc0-4b2a-bf6c-1f6655084f40/image.png) + +이미지와 손님 정보를 불러오는 코루틴들이 모두 취소가 되었습니다 😨 +이는 `loadUsers()` 에서 발생한 예외가 CoroutineScope 내부에 있는 coroutineContext 의 Job 에 전파되어 CoroutineScope 가 관리하는 모든 코루틴이 취소되었기 때문입니다. + +![](https://velog.velcdn.com/images/murjune/post/b4d8aa41-52ed-401d-8cbf-34c6e235da0b/image.png) + +> 참고로 CoroutineExceptionHandler 는 예외만 처리하는 것이지 예외 전파는 막지 못합니다. +> 그래서, 모두 취소 된 것 + +CoroutineScope 가 담당하는 코루틴들 중 하나의 코루틴에서 예외가 발생했다고 모든 코루틴이 취소 되는 것은 아무래도 이상합니다.😨 그래서, 이런 경우에 CoroutineScope() 의 인자에 `SupervisorJob()` 을 넣어주어 `예외 전파 제한`해주어야 합니다. + +```kotlin +val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> println("예외 발생") }) +``` + +이제는 loadUsers() 에서 예외가 발생해도 다른 코루틴에 영향을 주지 않습니다 😁 + +![](https://velog.velcdn.com/images/murjune/post/8f1f03cb-85ab-4c46-a78f-f6061144f88b/image.png) + +이렇듯 `CoroutineScope` 를 생성할 때, Root 코루틴 컨택스트에 SupervisorJob 을 설정해두어 예외 전파 방지하는 것이 좋습니다. + +그럼 이제 안드로이드에서는 SupervisorJob() 을 어떻게 사용하는지 볼까요?? + +# 3. lifecycleScope, viewModelScope + +- lifecycleScope + ![](https://velog.velcdn.com/images/murjune/post/c571c6de-aa79-4fb1-b0d9-d78eb3609a62/image.jpg) +- viewModelScope + ![](https://velog.velcdn.com/images/murjune/post/cf74837f-8f34-4711-b0e0-47cae285d186/image.png) + +안드로이드에서는 `viewModelScope` 와 `lifecycleScope` 를 생성할 때 coroutineContext 에 `SupervisorJob()` 를 설정해줍니다. 그래서 지금까지 ViewModel 작업할 때 하나의 코루틴에서 예외가 발생해도 다른 작업들이 취소되지 않았던 것이에요 🤭 + +이번에는 제가 프로젝트에서 로깅 분석을 비동기 처리하기 위해 CoroutineScope 를 만들 때 SupervisorJob() 을 직접 사용한 사례를 소개해드리겠습니다~ + + +# 4. loggerScope, analyticsScope + +안드로이드에서는 사용자의 행동 분석, 에러 모니터링을 위해 Firebase Analytics, Crashlytics 를 사용하곤 합니다. +어떤 작업에 로그를 남기거나 분석하는 작업이 실 서비스의 성능에 영향을 주면 안될 것입니다. +따라서, 로깅 작업 같은 경우 비동기 처리하는 것이 적절할 것입니다. + +만약, 특정 id 에 해당하는 유저 정보를 받아오는 Repository 가 있다고 해봅시다. +```kotlin +suspend fun userDetail(id: String): User = coroutineScope { + userDataSource.userDetail(id).also { + launch { analytics.logUserEvent(id) } + } + } +``` +조회한 User id를 analytics 에 로그를 남기는 작업을 비동기 처리하였습니다. +이때, 2가지 문제점이 있습니다. +### 1) 성능 저하 + +비동기 처리를 하기 위해 coroutineScope 와 launch 를 사용했지만, coroutineScope 은 모든 자식이 끝날때까지 대기합니다. 따라서,launch 내부 로그 분석 작업이 끝나야 `userDetail()`가 종료됩니다. +해당 코드는 비동기 처리 작업을 하느니만 못한 잘못된 코드입니다..😨 + +### 2) 예외 전파 +```kotlin +launch { analytics.logUserEvent(id) } +``` +만약 `analytics.logUserEvent(id)` 에서 예외가 발생하면 coroutineScope 로 예외가 전파됩니다. +user 정보를 불러오는데 성공했는데 로그 분석에 실패했다고 실 서비스 코드가 실패하는 것은 절대 안될 일입니다 😨 + +그래서 저는 다음과 같이 `analyticsScope`라는 새로운 코루틴 스코프를 만들었습니다. +```kotlin +/** + * 비동기적으로 데이터 수집, 분석, 로깅 등의 작업을 처리하는 용도로 사용하는 CoroutineScope + */ +private val analyticsExcpetionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> + Timber.e(throwable) +} +val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + analyticsExcpetionHandler) +``` +그리고 `analyticsScope`를 활용해 다음과 같이 수정하였습니다. +```kotlin +suspend fun userDetail(id: String): User = coroutineScope { + userDataSource.userDetail(id).also { + analyticsScope.launch { analytics.logUserEvent(id) } + } + } +``` +이로써 실 서비스에 영향을 주지도 않고, 안전하고 효율적으로 모니터링을 할 수 있게 되었습니다 😎 + +> 🚨 일반적으로, 코루틴의 구조화를 깨는 것은 비동기 작업을 안전하게 처리할 수 없도록 하기에 최대한 지양 해야합니다. 해당 코드는 `로깅&모니터링` 이라는 특수한 경우이기에 구조화를 깨고 독립적인 작업으로 실행한 것입니다 + +# 정리 + +> - `SupervisorJob` + > CoroutineScope 를 생성할 때 coroutineContext에 SupervisorJob 을 지정해주자. +> - `supervisorScope` + > 그 외, 예외 전파 제한할 경우 사용하자. (CoroutineScope {} 내부 or suspend 함수) + +그럼 다음 포스팅 때는 코루틴 예외 처리하는 방법(CoroutineExceptionHandler)에 대해 소개해드리겠습니다~ \ No newline at end of file diff --git "a/coroutine/docs/coroutine/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\262\230\353\246\254.md" "b/coroutine/docs/coroutine/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\262\230\353\246\254.md" new file mode 100644 index 0000000..f605748 --- /dev/null +++ "b/coroutine/docs/coroutine/\354\275\224\353\243\250\355\213\264_\354\230\210\354\231\270\354\262\230\353\246\254.md" @@ -0,0 +1,4 @@ +# 예외 처리 방법 +1) CoroutineExceptionHandler +2) 코루틴 스코프 함수 (coroutineScope) + try-catch +코루틴 스코프 함수는 일반 함수와 같이 예외가 발생했을 때와 같이 그 예외를 던집니다. \ No newline at end of file diff --git "a/coroutine/docs/coroutine/\354\275\224\353\243\250\355\213\264\354\235\264\353\236\200?.md" "b/coroutine/docs/coroutine/\354\275\224\353\243\250\355\213\264\354\235\264\353\236\200?.md" new file mode 100644 index 0000000..75ad1f8 --- /dev/null +++ "b/coroutine/docs/coroutine/\354\275\224\353\243\250\355\213\264\354\235\264\353\236\200?.md" @@ -0,0 +1,75 @@ +코루틴은 중단 가능한 작업 단위입니다. + +- 코루틴은 동시성 프로그래밍을 최대한 쉽게 구현할 수 있도록 구현된 도구다 + +코루틴은 실행이 중단될 때 작업의 실행 환경을 Continuation 이라는 상태 머신에 저장을 하여 다시 resume 될 때 +중단된 지점부터 다시 실행할 수 있다는 특징이 있습니다. 이에 비해 스레드는 단순히 멈추는 것만 가능할 뿐 저장이 불가능합니다. + +중단됐을 때 코루틴은 어떤 자원도 사용하지 않습니다. 따라서, 코루틴은 다른 스레드에서 시작할 수도 있고, 컨티뉴에이션 객체를 통해 다시 실행될 수 있 + +비동기적인 방식으로 코드 작성을 위해 불필요한 스레드를 많이 만들어야한다 +```kotlin +showProgressBar() +thread { + // 서버 통신 +}.join() // mainThread 블로킹 +hideProgressBar() +``` +그래서 다음과 같이 하는게 좋음 + +```kotlin +import kotlin.concurrent.thread + +thread { + showProgressBar() + thread { + // 서버 통신 + }.join() // mainThread 블로킹 + hideProgressBar() +} + +thread { + showProgressBar() + thread { + // 서버 통신 + postHideProgressBar() // 만약 해당 작업을 취소하려해보자 -- 어떻게 취소할래? + } +} +``` + +벌써 2개의 스레드를 생성하였음, 게다가 해당 thread 에서 예외가 발생하거나 작업을 취소하기도 어려움 + +코루틴을 활용하면 어떨까? + +```kotlin +launch { + showProgressBar() + withContext(Dispatchers.IO) { + // 서버 통신 + } + hideProgressBar() +} +``` +해당 코드에서는 스레드를 새롭게 생성해서 사용하는 부분은 없다. +IO 디스패처가 담당하는 스레드풀에서 이미 생성되어 있는 스레드에 코루틴을 할당하여 서버 통신을 진행하고 있다. + 코루틴은 IO 작업이 끝날 때까지 중단된다. +이때 스레드가 중단되는 것이 아니라 코루틴이 중단되는 것이다. +즉, main 스레드가 블로킹되지 않는다는 것인데 이것이 가능한 이유는 중단되는 시점에 해당 코루틴을 탈출하기 때문이다. +그리고 IO 작업이 모두 마치면 중단되고 있던 코루틴을 메인 스레드에 분배하고 resume 시켜 중단된 지점부터 다시 작업을 이어나갈 수 있도록 한다. + +즉, 스레드를 블로킹시키는 것이 아닌 코루틴을 중단시키는 것이다. + + + +스레드의 단점) + +- 스레드가 실행되었을 때, 멈출 수 있는 방법이 없어 메모리 누수로 이어질 수 있다 +- 스레드를 많이 생성하면 비용이 많이 든다 (스레드 스택의 기본 크기는 1MB 이다. 즉 많이 잡아먹는다) +- 스레드를 자주 전환하면 복잡도를 증가 +- 코드량이 늘어난다 + +중단 가능하는 개념이 없다 그래서 기존 스레드 방식은 실행 흐름 제어하기가 굉장히 어렵고 +람다를 넘겨주어야 한다. 코루틴의 경우 내부적으로 Continuation 을 활용한 콜백으로 wrapping이 되어 있어 비동기적인 코드를 +개발자 마치 동기적인 방식으로 코드를 작성할 수 있습니다. (개발 편의성 및 가독성이 높다) + +콜백 - 특정 작업이 끝난 후 어떤 작업 실행해야하는 동기적인 코드 흐름을 구현하기 위한 방법, non-blocking 한 방법으로 사용해야함 \ No newline at end of file diff --git "a/coroutine/docs/test/\355\205\214\354\212\244\355\212\270\353\245\274_\354\231\234\355\225\240\352\271\214?.md" "b/coroutine/docs/test/\355\205\214\354\212\244\355\212\270\353\245\274_\354\231\234\355\225\240\352\271\214?.md" new file mode 100644 index 0000000..aa42485 --- /dev/null +++ "b/coroutine/docs/test/\355\205\214\354\212\244\355\212\270\353\245\274_\354\231\234\355\225\240\352\271\214?.md" @@ -0,0 +1,80 @@ +# 테스트 왜 작성할까? + +프로젝트의 지속가능한 성장을 위해 + +테스트가 없는 프로젝트는 처음에는 발목 잡을 일이 없기에 빠르게 성장 가능하지만 +결국에는 많은 문제가 생기면 리팩토링 및 버그 픽스하는데 많은 시간을 할애하게 됩니다. +이때, 테스트가 프로젝트에 안전망 + +만약, 잘못된 테스트 질이 안좋은 테스트를 포함한다면 오히려 테스트가 없는 경우가 더 좋은 경우다 + +# 단위 테스트 +https://www.youtube.com/watch?v=mIO4Rbe_M74 + +> 어플리케이션 안에 있는 `개별적인 코드 단위`가 `의도한 대로 작동`하는지 확인하는 행위 + +여러 기능이 있으면 이 하나하나의 기능이 잘 작동하는지 보는 테스트 + +## 단위 테스트 왜 사용 해야 하는가? + +큰 기능을 작은 기능으로 쪼개 테스트함 +-> 원하는 부분만 테스트함으로 결과를 빠르게 볼 수 있다 (빠르게 검증) +-> 미리 작성한 단위 테스트를 기반으로 프로덕션 코드의 리팩토링을 안정적으로 할 수 있다. (리팩토링 안정적) +-> 단위 테스트가 실패하는 지점에서 문제점을 빠르게 찾을 수 있다. (문제점 빠르게 찾을 수 잇음) + +# 단위 테스트 예시 + +장바구니 .. + +# First 원칙 + +효율적이고 좋은 단위테스트를 위한 5가지 원칙 + +F: 단위 테스트는 빨라야한다. +I: 테스트는 독립적으로 동작해야한다 (각 테스트는 모두 독립적인 모듈이다) +R: 어떤 상황에서든 같은 테스트 결과가 나와야 한다. (멱등성) +S: print or log 가 아닌 테스트 자체적으로 결과가 나와야한다 +T: 적시에 테스트를 철저하게 작성해야 한다. (테스트를 미루지 말자) + +--- + +# Given-When-Then 패턴: 테스트 코드의 가독성이 좋아 유지보수 굿 +테스트 코드 작성의 표현 방식 + +Arrange(준비): 테스트 대상을 준비물 +Act(실행): 테스트 대상 시스템 메서드를 호출하고 결과, 출력값이 있다면 저장 +Assert(검증): 결과를 검증 + +# 안좋은 단위 테스트 예시 + +## 1. 여러 개의 준비, 실행 검증 +준비, 실행, 검증 -> 실행 -> 검증 + +이는 단위 테스트가 아닌 통합테스트다 + +단위테스트는 빠르고 간단하게 작성해야하는 first 원칙을 위배하고 있다. + +"구매를 성공하고 재고가 감소한다" + +실행 구절이 2줄 이상이다. + +비지니스 관점 + +행동: 물품 구매 +-> 결과 1) 재고 감소 +-> 결과 2) 고객이 물품 획득 + +즉 구매 -> 재고 감소 라는 이 사이클이 2개의 프로세스로 분리되어 실수할 위험이 있음 +-> 이는 위험하다 (실수 여지 있음) +-> 프로덕트 코드가 잘못됐나? 의심할 수 있음 + +## 2. 세부 구현에 의존적인 테스트 +세부구현은 언제든지 바뀔 수 있는 것 (만약, 10개까지만 장바구니 추가 가능 -> 12개로 바뀌면?) +공개된 public 한 메서드 만을 사용하여 테스트하는 것이 좋다. + + +[[10분 테코톡] 도도의 좋은 단위 테스트란 +](https://www.youtube.com/watch?v=R7spoJFfQ7U&t=55s) + +[[드로이드나이츠 2021] 강사룡 - Android Testing Best Practices +](https://www.youtube.com/watch?v=D_tWlb2deX8&t=1232s) \ No newline at end of file