kotlin Coroutine: supervisorScope vs SupervisorJob 어떤걸 사용하라는거지?

Murjune·2024년 9월 2일
3

Coroutine

목록 보기
6/8
post-thumbnail

테코톡에서는 [8:13 ~ 9: 00] 에 해당하는 내용입니다.

지난 시간에 배운 내용 정리~

  • SupervisorJob
    1) SupervisorJob 은 자식 코루틴의 예외 전파를 방지한다.
    2) SupervisorJob() 로 생성된 SupervisorJob 은 root Job 이 된다.
    3) SupervisorJob() 로 생성된 SupervisorJob 은 항상 active 하다.

  • supervisorScope
    1) 자식 코루틴의 예외 전파를 방지한다.
    2) 호출자의 코루틴 컨텍스트를 받아, 호출자 코루틴과 부모-자식관계를 보장한다.
    3) 호출자 코루틴은 끝날때까지 일시 중단된다.
    4) 자식 코루틴의 작업이 끝날때까지 대기한다.

Intro

지난 포스팅에서 SupervisorJob()supervisorScope 에 대해 알아보았습니다.
supervisorScope 이 자동으로 호출자 코루틴과의 부모-자식 관계를 보장해주기에 사용하기 훨씬 편합니다.

supervisorScope 사용하기 편하다는 거 알겠어 그럼 SupervisorJob()는 언제 씀? 🤔

그럼 위와 같은 생각이 들 수도 있는데요, supervisorJobsupervisorScope의 차이점을 비교하고, 각각을 적절하게 사용하는 방법과 실제 사용 사례를 소개해드리겠습니다. 👋

1. 일반적인 경우에는 supervisorScope 를 사용하자

suspend fun foo() = coroutineScope {
    val job = SupervisorJob(coroutineContext.job) // 1. coroutineScope 와 구조화
    launch(job) { error("Error") }
    launch(job) { println("foo") }
    job.complete() // 2. job 명시적으로 종료
}

suspend fun bar() = supervisorScope {
    launch { error("Error") }
    launch { println("bar") }
}

지난 시간 에서 배웠죠? SupervisorJob() 을 사용하면 구조화가 무너지기에 추가적인 설정들이 필요합니다. 확실히 supervisorScope 를 사용하는 것이 편해보입니다.

그래서, 코루틴 내부(코루틴 스코프 내부)나 suspend 함수에서는 supervisorScope 를 활용하여 예외 전파 방지하는 것이 좋습니다.

2. CoroutineScope 를 생성할 때 SupervisorJob 을 사용하자

supervisorScope 는 suspend 함수이기에, 일반함수에서는 사용할 수 없다는 제약이 있습니다.

참고) suspend 함수는 같은 suspend 함수나 코루틴 내부(코루틴 스코프 내부)에서만 호출할 수 있습니다.

그래서, 일반 함수에서 새롭게 코루틴을 생성하고 사용할 때는 suspervisorScope 를 사용 할 수 없습니다. CoroutineScope 의 coroutineContext 에 SupervisorJob() 을 지정해준 후 사용해야합니다.

CoroutineScope를 생성할 때 반드시 SupervisorJob() 을 지정해주는 것을 권장드립니다 ‼

CoroutineScope 에 SupervisorJob() 을 지정안해주면 어떤 문제가 있을까요? 🤔

val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> println("예외 발생") })

fun loadImages() = scope.launch { println("이미지..") }

fun loadUsers() = scope.launch { error("error 😵") }

fun loadCustomers() = scope.launch { println("손님..") }

이미지, 유저, 손님 정보를 비동기적으로 동시에 불러오고 있습니다. 그리고, 다음 시간에 배울 CoroutineExceptionHandler 를 통해 예외 처리도 해주고 있습니다.

loadImages()
loadUsers()
loadCustomers()

실행하면 어떻게 될까요?

이미지와 손님 정보를 불러오는 코루틴들이 모두 취소가 되었습니다 😨
이는 loadUsers() 에서 발생한 예외가 CoroutineScope 내부에 있는 coroutineContext 의 Job 에 전파되어 CoroutineScope 가 관리하는 모든 코루틴이 취소되었기 때문입니다.

참고로 CoroutineExceptionHandler 는 예외만 처리하는 것이지 예외 전파는 막지 못합니다. CoroutineScope 내부에 있는 CoroutineContext 가 취소 요청을 보내 모든 코루틴 취소 된 것입니다.

CoroutineScope 가 담당하는 코루틴들 중 하나의 코루틴에서 예외가 발생했다고 모든 코루틴이 취소 되는 것은 아무래도 이상합니다.😨 그래서, 이런 경우에 CoroutineScope() 의 인자에 SupervisorJob() 을 넣어주어 예외 전파 제한해주어야 합니다.

val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> println("예외 발생") })

이제는 loadUsers() 에서 예외가 발생해도 다른 코루틴에 영향을 주지 않습니다 😁

이렇듯 CoroutineScope 를 생성할 때, Root 코루틴 컨택스트에 SupervisorJob 을 설정해두어 예외 전파 방지하는 것이 좋습니다.

그럼 이제 안드로이드에서는 SupervisorJob() 을 어떻게 사용하는지 볼까요??

3. lifecycleScope, viewModelScope

  • lifecycleScope
  • viewModelScope

안드로이드에서는 viewModelScopelifecycleScope 를 생성할 때 coroutineContext 에 SupervisorJob() 를 설정해줍니다. 그래서 지금까지 ViewModel 작업할 때 하나의 코루틴에서 예외가 발생해도 다른 작업들이 취소되지 않았던 것이에요 🤭

4. 실 사례) analyticsScope

이번에 우테코에서 진행한 프로젝트에서 로깅 분석을 비동기 처리하기 위해 CoroutineScope 를 만들었는데요. 이를 재구성한 사례를 소개해드리겠습니다 😎
analyticsScope를 적용한 프로젝트 PR

안드로이드에서는 사용자의 행동 분석, 에러 모니터링을 위해 Firebase Analytics, Crashlytics 를 사용합니다. 로그를 남기거나 분석하는 작업은 실 서비스의 성능에 영향을 주면 안됩니다. 따라서, 로깅 작업 같은 경우 비동기 처리하는 것이 적절합니다.

특정 id 에 해당하는 유저 정보를 받아오고 있고, 조회한 User id를 analytics 에 로그를 남기는 작업을 하고 있다고 해봅시다.
먼저, 잘못된 로깅 처리 방식입니다.

suspend fun userDetail(id: String): User = coroutineScope {
     userDataSource.userDetail(id).also {
         launch { analytics.logUserEvent(id) }
     }
 }

해당 코드에는 2가지 문제점이 있습니다.

1) 성능 저하

비동기 처리를 하기 위해 coroutineScope 와 launch 를 사용했지만, coroutineScope 은 모든 자식이 끝날때까지 대기합니다. 따라서,launch 내부 로그 분석 작업이 끝나야 userDetail()가 종료됩니다.

해당 코드는 비동기 처리 작업을 하느니만 못한 잘못된 코드입니다..😨

2) 예외 전파

suspend fun userDetail(id: String): User = coroutineScope { // 2. 예외 전파 ❌
     userDataSource.userDetail(id).also {
         launch { analytics.logUserEvent(id) } // 1. 예외 발생! 💀
     }
 }

만약 analytics.logUserEvent(id) 에서 예외가 발생하면 coroutineScope 로 예외가 전파됩니다.
user 정보를 불러오는데 성공했는데 로그 분석에 실패했다고 실 서비스 코드가 실패하는 것은 절대 안될 일입니다 😨

그래서 다음과 같이 analyticsScope라는 새로운 코루틴 스코프를 만들었습니다.

private val analyticsExcpetionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
	...
}
val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + analyticsExcpetionHandler)

그리고 analyticsScope를 활용해 다음과 같이 수정하였습니다.

suspend fun userDetail(id: String): User = coroutineScope {
     userDataSource.userDetail(id).also {
         analyticsScope.launch { analytics.logUserEvent(id) }
     }
 }

이제 로깅 분석 작업는 userDetail와 완전히 독립적인 작업이 되었습니다. userDetail()analyticsScope.launch{..}을 호출하자마자 종료될 것이며, analyticsScope.launch 에서 발생한 예외와도 무관합니다.

이로써 실 서비스에 영향을 주지도 않고, 안전하고 효율적으로 모니터링을 할 수 있게 되었습니다 😎

🚨 일반적으로, 코루틴의 구조화를 깨는 것은 비동기 작업을 안전하게 처리할 수 없도록 하기에 최대한 지양 해야합니다. 해당 코드는 로깅&모니터링 이라는 특수한 경우이기에 구조화를 깨고 독립적인 작업으로 실행한 것입니다

5. 정리

  • SupervisorJob
    CoroutineScope 를 생성할 때 coroutineContext에 SupervisorJob 을 지정해주자.
  • supervisorScope
    그 외, 예외 전파 제한할 경우 사용하자. (CoroutineScope {} 내부 or suspend 함수)

그럼 다음 포스팅 때는 코루틴 예외 처리하는 방법(CoroutineExceptionHandler)에 대해 소개해드리겠습니다~

profile
열심히 하겠슴니다:D

0개의 댓글

관련 채용 정보