Kotlin 코루틴으로 앱 성능 향상
kotlin 코루틴은 네트워크 호출이나 디스크 작업같은 장기 실행 작업을 관리하면서 앱의 응답성을 유지하는 깔끔하고도 간소화된 비동기 코드를 작성할 수 있다.
장기 실행 작업 관리
코루틴은 장기 실행 작업을 처리하는 두 개의 작업을 추가하여 일반 함수를 기반으로 Build 된다.
기존 invoke(call) 과 return 이외에도
- suspend (정지) : 현재 코루틴 실행을 일시 중지 하고 모든 로컬 변수를 저장.
- resume (재개) : 정지된 위치부터 정지된 코루틴을 계속 실행.
suspend 함수는 다른 suspend함수에서 호출하거나 코루틴 빌더 (launch, async) 를 사용하여 새 코루틴을 시작하는 방법으로만 호출이 가능하다.
suspend fun fetchDocs() { // MAIN THREAD
val result = get("https://developer.android.com") // BACKGROUND THREAD
show(result) // MAIN THREAD
}
suspend fun get(uri:String) = withContext(Dispatchers.IO){...}
get() 함수는 여전히 메인 스레드에서 실행된다. 하지만 get()을 통해 네트워크 요청을 시작하기 전에 코루틴은 정지(suspend)된다. 이 후 네트워크 요청이 완료되면 get()은 단지 정지된 코루틴을 재개(Resume)한다.
(기존에는 callback을 사용하여 메인 스레드에 알렸다면, 지금은 단순히 정지와 재개 작업을 이용하여 손쉽게 I/O작업을 완료하고 메인으로 이동)
코루틴 전개 시, Kotlin 활동
Kotlin은 Stack Frame을 통해 로컬 변수와 함께 실행 중인 함수를 관리한다. 코루틴이 정지되면 Stack Frame이 복사되고 저장된다. 재개되면 Stack Frame이 저장된 위치에서 다시 복사 되고 정지된 함수가 실행된다
이는 일반적인 순차 차단 요청 처럼 보일 수 있지만, 중요한 점은 순차를 지키면서 메인 스레드를 차단하지 않는 다는 점이다.
코루틴과 디스패처 Dispatcher
Kotlin 코루틴은 디스패처를 사용하여 코루틴 실행에 사용되는 스레드를 (메인인지 백그라운드인지) 확인한다.
만약 해당 작업을 메인 스레드 외부(I/O 또는 Default) 에서 실행하고 싶다면 해당 디스패처를 통해 작업을 실행하도록 코루틴에 지시하여야 한다.
Kotlin에서 모든 코루틴은 반드시 디스패처에서 실행된다. 그것이 메인 스레드일지라도 디스패처를 통해 코루틴은 실행되어야 한다. 코루틴은 그들 스스로 정지(Suspend)하고, 디스패처가 코루틴을 다시 재개(Resume)시키기 때문이다.
- Dispatchers.Main : 이 디스패처를 사용하여 메인 안드로이드 스레드에서 코루틴을 실행. 이 디스패처는 UI와 상호작용하고, 빠른 작업을 실행하기 위해서만 사용해야 한다.
ex) suspend 함수를 호출하고, UI 프레임워크 작업을 실행하며, LiveData 객체를 업데이트 한다. - Dispatchers.IO : 해당 디스패처는 메인 스레드 외부에서 Disk 작업 또는 Network I/O를 실행하도록 최적화.
ex) repositery component를 사용하여 File에서 읽고 쓰는 작업을 실행. - Dispatchers.Default : 해당 디스패처는 CPU를 많이 사용하는 작업을 메인 스레드 외부에서 실행 하도록 최적화.
ex) 목록 정렬, JSON parsing
코루틴을 사용하면 스레드를 세부적으로 제어하여 전달할 수 있다.
withContext()를 사용하면 모든 함수가 main-safe한 지 확인할 수 있다.
※주의
withContext()는 대부분 디스크 read/write, network작업, cpu집약사용과 같이 외부 백그라운드 스레드에서 실행되어야 하는 코드들이 구현된다. 따라서 반드시 main-safe할 수 있도록 suspend 키워드가 붙은 함수 내에서 사용되어야 한다.
suspend함수를 사용한다고 해서 백그라운드 스레드에서 함수가 진행되도록 Kotlin에 지시하는 것은 아니다.
일반적으로 suspend함수는 메인 스레드에서 작동하며 코루틴 역시 일반적을 메인 스레드에서 실행된다.
코루틴 시작
- launch : 새로운 코루틴을 시작하고 호출자에게 결과를 반환하지 않는 시작함수.
'실행 후 삭제'로 간주되는 작업들은 launch를 사용하여 시작할 수 있다. - async : 새로운 코루틴을 시작하고 await라는 정지 함수를 결과로 반환하도록 허용
launch와 async는 예외 처리 방식이 다르다. async는 최종 await호출을 예상하기에 예외를 보유할 수 있고 await호출의 일부로 예외를 다시 발생시킨다.
CoroutineScope
launch 또는 async 를 통해 새로운 코루틴을 시작한다.
CoroutineScope는 이렇게 만들어진 코루틴을 추적하며 언제든지 scope.cancel()을 호출하여 범위 내에 코루틴을 취소할 수 있다.
Android 일부 KTX라이브러리는 특정 수명 주기 클래스에 자체 코루틴Scope를 제공한다. ViewModelScope, LifeCycleScope 등등. 단, 코루틴 스코프는 디스패처와 달리 코루틴을 실행 시키지 않는다. (추적의 역할)
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMehod() {
scope.launch{
fetchDocs()
}
}
fun cleanUp() {
scope.cancel()
}
}
이처럼 CoroutineScope를 자체 제작할 수 있다.
- Job
Job은 코루틴의 핸들링이다. launch 또는 async로 만들어진 코루틴은 자신을 고유하게 식별하고 수명주기를 관리해줄 Job인스턴스를 반환한다.
fun exampleMehod() {
val job = scope.launch{
}
if(...) {
job.cancel()
}
}
CoroutineContext
다음과 같은 요소를 사용하여 코루틴의 동작을 정의할 수 있다.
- Job : 코루틴의 수명주기를 제어
- CoroutineDispatchers : 적절한 스레드에 작업을 전달
- CoroutineName : 디버깅에 유용하게 사용되는 코루틴의 이름
- CoroutineExceptionHandler : 포착되지 않는 예외를 처리.
fun exampleMehod() {
val scope = CoroutineScope(Job() + Dispatchers.Main))
val job1 = scope.launch {}
val job2 = scope.launch(Dispatchers.Default + "BackgroundCoroutine") {}
}
job1의 scope.launch는 scope의 CoroutineContext를 따른다. 하지만 Job2는 launch에 새로운 CoroutineContext를 전달했기에 이제 Main스레드가 아닌 Default 스레드를 사용한다. 그리고 새로운 코루틴의 이름을 가진다.
새로운 CoroutineContext를 전달할 때 Job을 인자로 전달할 필요는 없다. Job의 역할은 코루틴의 수명주기를 제어하는 것인데, 새로운 코루틴은 새로운 자신의 수명주기를 컨트롤할 새로운 Job 인스턴스를 반환하기 때문이다.
'안드로이드' 카테고리의 다른 글
[Android/Kotlin] 대화상자(Dialog) (1) AlertDialog (0) | 2022.03.16 |
---|---|
[Android/Kotlin] 앱 데이터 및 파일 (1) MediaStore (0) | 2022.03.10 |
[Android/Kotlin] Kotlin Coroutine (1) 안드로이드 코루틴 (0) | 2022.03.07 |
[Android/Kotlin] FragmentTransaction 프래그먼트 트랜젝션 (0) | 2022.02.26 |
[Android/Kotlin] SharedViewModel (ViewModel을 이용해 데이터 공유) (0) | 2022.02.24 |