일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- 안드로이드
- Android Jetpack
- 안드로이드 fcm
- 프로그래머스 알고리즘
- android recyclerview
- Android
- Android Interceptor
- 영어공부
- Android ViewPager2
- Kotlin
- 알고리즘 자바
- 카카오 알고리즘
- Android ProgressBar
- Android WebView
- 코틀린 코루틴
- MVP Architecture
- OkHttp Interceptor
- Java
- Kotlin FCM
- 안드로이드 카카오 로그인
- DataBinding
- 안드로이드 갤러리 접근
- WebView
- Android 12 대응
- Android 12
- Android Navigation
- 영어독립365
- 습관만들기
- scope function
- 66챌린지
- Today
- Total
나미래 Android 개발자
코틀린 supervisorScope로 안전한 병렬 처리하기 - 코루틴 스코프 함수 완벽 가이드 본문
코틀린 supervisorScope로 안전한 병렬 처리하기 - 코루틴 스코프 함수 완벽 가이드
Moimeme Futur 2025. 8. 18. 19:56이 글은 마르친 모스카와의 ⟪코틀린 코루틴⟫ 책을 기반으로 작성하였습니다.
여러분은 코루틴 스코프 함수를 어디까지 사용해보셨나요?
코루틴 스코프 함수의 기본이 되는 coroutineScope 함수, 아래 정의와 같은 함수를 사용해 보셨나요?
suspend fun <R> coroutineScope(
block: suspend CoroutineScope.() -> R
): R
혹시 coroutineScope 함수를 사용하면서 자식 코루틴이 취소/예외가 발생했을 때, coroutineScope의 Job이 종료되지 않도록 하려면 어떻게 해야 하는지 동료에게 또는 면접에서 질문을 받았다면 답변할 수 있으신가요?
만약 답변하기 어렵다고 생각이 든다면 이 글을 통해서 자신 있게 답변할 수 있도록 도와드리겠습니다.
이 글을 통해서 여러분은 코루틴 스코프 함수의 개념적 지식과 supervisorScope의 특징 그리고 실제 사용 사례를 습득할 수 있으며 질문을 받았을 때, 자신 있게 답변하실 수 있게 될 것입니다.
코루틴 스코프 함수란?
먼저 기본 배경 지식으로 코루틴 스코프 함수에 대해서 간략하게 설명하겠습니다.
코루틴 스코프 함수는 코틀린에서 제공하는 코루틴 스코프를 만드는 중단 함수입니다.
코루틴 스코프 함수에는 coroutineScope, supervisorScope, withContext, withTimeout 이 있습니다.
참고로 중단 함수이기 때문에 코루틴 내에서 호출이 되어야 합니다.
이번 글에서는 코루틴 스코프 함수 중 자식 코루틴에서 취소/예외가 발생하더라도 코루틴 스코프 함수가 종료되지 않고 작업을 완수하는 함수인 supervisorScope 함수에 대해서 이야기하도록 하겠습니다.
coroutineScope 함수에 대한 배경 지식이 필요하신 분은 아래 제가 작성했던 "코틀린 coroutineScope 사용법과 예제 - 코루틴 스코프 함수 완벽 가이드" 글을 먼저 읽고 오시는 것을 추천합니다.
코틀린 coroutineScope 사용법과 예제 - 코루틴 스코프 함수 완벽 가이드
이 글은 코틀린 코루틴 마르친 모스카와의 ⟪코틀린 코루틴⟫ 책을 기반으로 작성하였습니다. 여러분은 코루틴 스코프 함수는 어떤 것들이 있고, 어떤 경우에 사용해야 하는지 질문을 받는다면
devgeek.tistory.com
문제 상황
먼저 supervisorScope 함수를 이해하기 위해서 간단한 개발 요구사항을 이야기하겠습니다.
만약 여러분이라면 다음과 같은 상황에서 어떻게 개발을 할 것인지 생각하면서 읽으시면 supervisorScope 함수를 더 깊게 이해하고 오랫동안 기억하실 수 있으실 것입니다.
"여러 개의 엔드포인트에서 데이터를 동시에 얻어야 하는 중단 함수에서 한 개 이상의 엔드포인트에서 데이터를 읽어오는 데 실패하더라도 데이터를 만들어서 반환해야 하는 경우 어떻게 중단 함수를 만들어야 코루틴을 효과적으로 사용할 수 있을까요?"
생각해 보셨나요?
여기서 요구사항의 주요 포인트는 다음과 같습니다.
- 중단 함수를 만든다.
- 여러 개의 엔드포인트에서 데이터를 동시에 얻는다.
- 한 개 이상의 엔드포인트에서 데이터를 얻는 데 실패하더라도 중단 함수를 종료하면 안 된다.
구체적인 문제 상황
문제 상황에 대한 이해를 돕기 위해서 좀 더 구체적인 상황을 제시하도록 하겠습니다.
만약 인스타그램 앱 사용자가 프로필 화면에 진입했을 때 사용자 정보, 게시물 수, 팔로워 수 그리고 팔로우 수들을 하나의 중단함수에서 각각의 API를 통해 데이터를 얻어서 유저 정보를 보여줘야 하는 경우를 생각해 보겠습니다.
그리고 각각의 API 통신이 실패하는 경우, 실패 케이스에 대한 UI가 정의되어 있는 경우 어떻게 중단 함수를 효과적으로 구현할 수 있을까요?
해결 방안 아이디어
4가지 데이터(사용자 정보, 게시물 수, 팔로워 수, 팔로우 수)를 받아오는 API를 호출하는 코루틴들에서 예외가 발생하더라도 부모 코루틴에 예외를 전파하지 않으면 됩니다.
이와 같은 개념으로 코루틴 컨텍스트의 Job 중 SupervisorJob이 있습니다.
coroutineScope와 같은 맥락으로 코루틴 스코프를 만들고 Job을 SupervisorJob으로 설정하면 됩니다.
supervisorScope 함수의 등장
그렇다면 위와 같은 아이디어를 이용해서 어떻게 해결할까요?
맞습니다. supervisorScope 함수를 사용하면 됩니다.
supervisorScope는 어떤 특성을 가지고 있어서 coroutineScope에 SupervisorJob을 더한 것처럼 동작할 수 있을까요?
suspend fun <R> supervisorScope(
block: suspend CoroutineScope.() -> R
): R
supervisorScope 함수의 특성
- supervisorScope는 코루틴 스코프를 생성하는 중단 함수입니다.
- 생성된 코루틴 스코프의 코루틴 컨텍스트는 함수를 호출한 코루틴 컨텍스트를 상속 받습니다.
- 단, 코루틴 컨텍스트 중 Job은 SupervisorJob으로 override 합니다. (새로운 Job을 만듭니다.)
- supervisorScope는 파라미터로 전달받은 함수가 생성한 값을 반환합니다.
supervisorScope 함수의 핵심 특징
supervisorScope는 위와 같은 2가지 특성을 가지고 있어서 다음과 같은 특징을 갖습니다.
1. 독립적인 자식 코루틴 관리
Job을 SupervisorJob으로 override 하기 때문에 자식 코루틴 간의 실패가 서로에게 영향을 주지 않습니다.
- 하나의 자식 코루틴이 실패해도 다른 형제 코루틴들은 계속 실행됩니다.
- 자식 코루틴의 예외가 supervisorScope 자체를 취소시키지 않습니다.
2. 예외 처리의 특수성
supervisorScope 내부에서 async를 사용하는 경우 주의가 필요합니다.
- await() 호출 시점에서 예외가 다시 throw 되기 때문에 반드시 try-catch로 처리해야 합니다.
- 예외 처리를 하지 않으면 supervisorScope 블록 내부에서 예외가 발생한 것으로 간주되어 전체 스코프가 종료됩니다.
3. 컨텍스트 상속과 Job 교체
- 부모의 코루틴 컨텍스트를 상속받되, Job만 새로운 SupervisorJob으로 교체합니다.
- 이를 통해 구조적 동시성을 유지하면서도 독립적인 실행 환경을 제공합니다.
주로 서로 독립적인 작업(코루틴)을 시작하는 중단 함수에서 사용됩니다.
실제 사용 예시
그렇다면 제가 정리했던 요구사항을 충족하기 위해서 어떻게 사용할 수 있는지 코틀린 코드를 이용해서 보여드리겠습니다.
[요구사항 3가지 포인트]
- 중단 함수를 만든다.
- 여러 개의 엔드포인트에서 데이터를 동시에 얻는다.
- 한 개 이상의 엔드포인트에서 데이터를 얻는 데 실패하더라도 중단 함수를 종료하면 안 된다.
data class UserProfileData(
val user: UserData?,
val posts: PostsData?,
val follows: FollowsData?,
val followers: FollowersData?
)
suspend fun getUserProfile(): UserProfileData = supervisorScope {
// 동시에 실행되는 비동기 작업들
val userDeferred = async { getUserData() }
val postsDeferred = async { getUserPosts() }
val followsDeferred = async { getUserFollows() }
val followersDeferred = async { getUserFollowers() }
// 각 결과를 안전하게 처리
val user = try {
userDeferred.await()
} catch (e: Exception) {
Log.e("UserProfile", "Failed to get user data", e)
null
}
val posts = try {
postsDeferred.await()
} catch (e: Exception) {
Log.e("UserProfile", "Failed to get posts data", e)
null
}
val follows = try {
followsDeferred.await()
} catch (e: Exception) {
Log.e("UserProfile", "Failed to get follows data", e)
null
}
val followers = try {
followersDeferred.await()
} catch (e: Exception) {
Log.e("UserProfile", "Failed to get followers data", e)
null
}
// 모든 결과를 기다렸다가 객체 생성
UserProfileData(
user = user,
posts = posts,
follows = follows,
followers = followers
)
}
어떠신가요? 위 코드가 어떻게 요구사항을 충족하는지 머릿속에 그려지시나요?
이 코드에서는 4개의 API 호출이 동시에 시작되고, 모든 결과가 완료되면 UserProfileData 객체를 생성해서 반환합니다. 그리고 만약 특정 코루틴에서 예외가 발생하더라도 supervisorScope는 종료되지 않고 UserProfileData 객체를 반환할 수 있습니다.
coroutineScope vs supervisorScope 비교
이해를 돕기 위해 두 함수의 핵심 특징과 차이점을 명확히 비교해보겠습니다.
supervisorScope vs coroutineScope 실제 비교
coroutineScope 사용 시
suspend fun getUserProfileWithCoroutineScope(): UserProfileData = coroutineScope {
val userDeferred = async { getUserData() } // 만약 이 API가 실패하면
val postsDeferred = async { getUserPosts() }
val followsDeferred = async { getUserFollows() }
val followersDeferred = async { getUserFollowers() }
UserProfileData(
user = userDeferred.await(), // 여기서 예외가 발생하고
posts = postsDeferred.await(), // 이 라인들은 실행되지 않음
follows = followsDeferred.await(),
followers = followersDeferred.await()
)
// 함수 자체가 예외를 던지며 종료됨
}
supervisorScope 사용 시
suspend fun getUserProfileWithSupervisorScope(): UserProfileData = supervisorScope {
val userDeferred = async { getUserData() } // 이 API가 실패해도
val postsDeferred = async { getUserPosts() } // 다른 API들은 계속 실행됨
val followsDeferred = async { getUserFollows() }
val followersDeferred = async { getUserFollowers() }
val user = try { userDeferred.await() } catch (e: Exception) { null }
val posts = try { postsDeferred.await() } catch (e: Exception) { null }
val follows = try { followsDeferred.await() } catch (e: Exception) { null }
val followers = try { followersDeferred.await() } catch (e: Exception) { null }
UserProfileData(user, posts, follows, followers) // 항상 객체를 반환
}
실무에서의 활용 팁
1. 로깅 및 모니터링
실제 프로덕션 환경에서는 어떤 API가 실패했는지 추적할 수 있도록 로깅을 추가하는 것이 좋습니다.
suspend fun getUserProfile(): UserProfileData = supervisorScope {
val results = mutableMapOf<String, Boolean>()
val user = try {
async { getUserData() }.await().also {
results["user"] = true
}
} catch (e: Exception) {
results["user"] = false
FirebaseCrashlytics.getInstance().recordException(e)
null
}
// 다른 API 호출들...
// 성공/실패 통계 로깅
Analytics.track("profile_load_result", results)
UserProfileData(user, posts, follows, followers)
}
2. 타임아웃 설정
네트워크 요청에는 타임아웃을 설정하여 무한 대기를 방지하는 것이 좋습니다.
suspend fun getUserProfile(): UserProfileData = supervisorScope {
val userDeferred = async {
withTimeout(5000) { getUserData() }
}
// 다른 API 호출들...
}
3. 부분적 재시도 로직
중요한 데이터의 경우 재시도 로직을 추가할 수 있습니다.
suspend fun getUserData(): UserData? {
repeat(3) { attempt ->
try {
return apiService.getUserData()
} catch (e: Exception) {
if (attempt == 2) throw e // 마지막 시도에서는 예외를 던짐
delay(1000 * (attempt + 1)) // 점진적 지연
}
}
return null
}
주의사항
1. 메모리 누수 방지
supervisorScope 내에서 생성된 코루틴들이 장시간 실행될 수 있으므로, 적절한 타임아웃 설정이 중요합니다.
2. 예외 처리의 중요성
supervisorScope를 사용할 때는 반드시 각 await() 호출을 try-catch로 감싸야 합니다. 그렇지 않으면 supervisorScope의 장점을 활용할 수 없습니다.
3. 성능 고려사항
모든 API가 실패해도 함수가 완료될 때까지 기다리므로, 불필요한 지연이 발생할 수 있습니다. 필요에 따라 조기 종료 로직을 고려해보세요.
정리
supervisorScope는 안드로이드 개발에서 매우 유용한 코루틴 스코프 함수입니다. 특히 여러 독립적인 네트워크 요청을 병렬로 처리하면서도 일부 실패에 대해 관대하게 처리해야 하는 상황에서 빛을 발합니다.
핵심 포인트를 다시 정리하면 다음과 같습니다.
supervisorScope의 특징
- 자식 코루틴의 실패가 다른 형제 코루틴에 영향을 주지 않음
- 부분적 실패를 허용하면서도 전체 작업을 완료할 수 있음
- SupervisorJob을 내부적으로 사용하여 구조화된 동시성을 제공
언제 사용해야 할까?
- 여러 API를 병렬로 호출하되, 일부 실패를 허용해야 할 때
- 사용자 경험을 위해 가능한 데이터라도 보여줘야 할 때
- 독립적인 작업들을 동시에 실행해야 할 때
주의사항
- 반드시 각 await() 호출을 try-catch로 감싸기
- 적절한 타임아웃과 로깅 추가하기
- 메모리 누수 방지를 위한 적절한 생명주기 관리
이제 면접에서 "supervisorScope는 언제 사용하나요?" 또는 "부분적 실패를 허용하는 병렬 처리는 어떻게 구현하나요?"라는 질문을 받으면 자신 있게 답변하실 수 있을 것입니다.
'안드로이드 > Coroutine' 카테고리의 다른 글
코틀린 coroutineScope 사용법과 예제 - 코루틴 스코프 함수 완벽 가이드 (3) | 2025.08.07 |
---|---|
[Android Coroutine] 구조적 동시성을 완성하는 마지막 퍼즐: 코루틴 취소(Cancellation) (0) | 2025.05.03 |
[Android Coroutine] 구조적 동시성 쉽게 이해하기 – Job과 launch의 관계 (0) | 2025.05.01 |
Android 개발자를 위한 CoroutineContext 깊이 이해하기 (0) | 2025.04.29 |