모든 코루틴은 CoroutineScope 안에서 실행된다. Scope는 코루틴의 생명주기를 관리한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
classUserRepository{// 자체 스코프 — 필요 시 전체 취소 가능privatevalscope=CoroutineScope(SupervisorJob()+Dispatchers.IO)funfetchUsers(){scope.launch{valusers=apiService.getUsers()// ...}}funclear(){scope.cancel()// 소속 코루틴 전체 취소}}
Android에서는 viewModelScope, lifecycleScope 등 미리 정의된 스코프를 활용한다.
2.2 CoroutineContext
CoroutineContext는 코루틴의 실행 환경을 정의하는 불변 요소 집합이다.
주요 요소:
Job: 코루틴의 생명주기 관리
Dispatcher: 어떤 스레드에서 실행할지
CoroutineName: 디버깅용 이름
CoroutineExceptionHandler: 예외 처리기
1
2
3
4
valcontext=Dispatchers.IO+CoroutineName("data-loader")+exceptionHandlerlaunch(context){// IO 디스패처에서 "data-loader"라는 이름으로 실행}
2.3 Dispatcher 종류
Dispatcher
스레드 풀
용도
Dispatchers.Main
메인(UI) 스레드
UI 업데이트, 가벼운 작업
Dispatchers.IO
공유 스레드 풀 (64개)
네트워크, DB, 파일 I/O
Dispatchers.Default
CPU 코어 수만큼
CPU 집약적 연산 (정렬, JSON 파싱)
Dispatchers.Unconfined
호출 스레드 → 재개 스레드
특수 케이스, 테스트
1
2
3
4
5
6
launch(Dispatchers.IO){valdata=fetchFromNetwork()// IO 스레드withContext(Dispatchers.Main){updateUI(data)// 메인 스레드로 전환}}
3. Structured Concurrency 패턴 및 Job 계층 구조
3.1 Structured Concurrency 원칙
Kotlin 코루틴의 핵심 설계 철학은 Structured Concurrency다:
모든 코루틴은 부모 스코프 안에서 실행된다.
부모가 취소되면 자식 코루틴도 모두 취소된다.
자식이 실패하면 부모에게 전파된다.
부모는 모든 자식이 완료될 때까지 완료되지 않는다.
1
2
3
4
5
6
7
8
9
10
11
funmain()=runBlocking{// 부모launch{// 자식 1delay(2000L)println("자식 1 완료")}launch{// 자식 2delay(1000L)println("자식 2 완료")}// runBlocking은 두 자식이 모두 완료될 때까지 대기}
// coroutineScope: 자식 하나가 실패하면 나머지도 취소suspendfunfailFast()=coroutineScope{launch{throwRuntimeException("실패!")}// 전체 스코프 취소launch{delay(Long.MAX_VALUE)}// 같이 취소됨}// supervisorScope: 자식 실패가 형제에게 전파되지 않음suspendfunfailIsolated()=supervisorScope{launch{throwRuntimeException("실패!")}// 이것만 실패launch{delay(1000L)println("나는 계속 실행됨")// 정상 실행}}
4. 실전 예제
4.1 여러 API 병렬 호출 (async/await)
1
2
3
4
5
6
7
8
9
10
11
12
13
suspendfunloadDashboard(userId:String):Dashboard=coroutineScope{// 세 API를 병렬로 호출valuserDeferred=async{userApi.getUser(userId)}valordersDeferred=async{orderApi.getOrders(userId)}valrecommendsDeferred=async{recommendApi.getRecommendations(userId)}// 모든 결과를 모아서 반환Dashboard(user=userDeferred.await(),orders=ordersDeferred.await(),recommendations=recommendsDeferred.await())}
순차 실행 시 3초 걸리는 작업이 병렬로 1초에 완료된다 (각 API가 1초라고 가정).
4.2 타임아웃 처리 (withTimeout)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
suspendfunfetchWithTimeout():String{returntry{withTimeout(3000L){// 3초 안에 완료되지 않으면 TimeoutCancellationExceptionslowApi.fetchData()}}catch(e:TimeoutCancellationException){"기본값 (타임아웃)"}}// null 반환 버전suspendfunfetchOrNull():String?{returnwithTimeoutOrNull(3000L){slowApi.fetchData()}}
suspendfun<T>retry(times:Int=3,initialDelay:Long=100L,factor:Double=2.0,block:suspend()->T):T{varcurrentDelay=initialDelayrepeat(times-1){try{returnblock()}catch(e:Exception){println("재시도 ${it + 1}/$times — ${e.message}")}delay(currentDelay)currentDelay=(currentDelay*factor).toLong()}returnblock()// 마지막 시도 — 실패 시 예외 전파}// 사용valresult=retry(times=3){apiService.getUser("user-123")}
5. Flow 기초
Flow는 비동기 데이터 스트림이다. RxJava의 Observable과 유사하지만 코루틴 기반이다.
classUserViewModel:ViewModel(){// StateFlow — 항상 최신 값을 가지고 있음 (LiveData 대체)privateval_uiState=MutableStateFlow(UiState.Loading)valuiState:StateFlow<UiState>=_uiState.asStateFlow()// SharedFlow — 이벤트 전달 (일회성 이벤트)privateval_events=MutableSharedFlow<Event>()valevents:SharedFlow<Event>=_events.asSharedFlow()funloadUser(id:String){viewModelScope.launch{_uiState.value=UiState.Loadingtry{valuser=userRepository.getUser(id)_uiState.value=UiState.Success(user)}catch(e:Exception){_uiState.value=UiState.Error(e.message)_events.emit(Event.ShowSnackbar("로드 실패"))}}}}
6. 코루틴 예외 처리
6.1 launch vs async 예외 전파 차이
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
funmain()=runBlocking{// launch: 예외가 즉시 부모로 전파됨valjob=launch{throwRuntimeException("launch 에러")// → 부모 코루틴까지 전파, try-catch로 잡을 수 없음}// async: await() 호출 시 예외 발생valdeferred=async{throwRuntimeException("async 에러")}try{deferred.await()// 여기서 예외 발생}catch(e:RuntimeException){println("잡았다: ${e.message}")}}
6.2 CoroutineExceptionHandler
launch의 예외를 잡기 위해 CoroutineExceptionHandler를 사용한다. 이 핸들러는 루트 코루틴에만 적용된다.
valhandler=CoroutineExceptionHandler{_,exception->println("예외 발생: ${exception.message}")// 로깅, 알림 등 처리}funmain()=runBlocking{valscope=CoroutineScope(SupervisorJob()+handler)scope.launch{throwRuntimeException("문제 발생!")// → handler에서 처리됨}scope.launch{delay(1000L)println("나는 정상 실행")// SupervisorJob 덕분에 영향 없음}delay(2000L)}
6.3 SupervisorJob
SupervisorJob은 자식의 실패가 다른 자식에게 전파되지 않게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
valscope=CoroutineScope(SupervisorJob()+Dispatchers.IO)scope.launch{// 자식 1 — 실패해도 자식 2에 영향 없음throwRuntimeException("자식 1 실패")}scope.launch{// 자식 2 — 정상 실행 계속delay(1000L)println("자식 2 완료")}