코루틴은 안드로이드 앱에서 메인 스레드에서 수행되는 XML 구문 분석, 뷰 인플레이션 및 클릭 이벤트와 같은 사용자 상호 작용과 같은 작업으로 인해 실행 시간이 짧은 두 새로고침 간의 시간보다 길어질 때 앱이 성능 오류를 보여줄 수 있는 문제를 해결하기 위해 사용됩니다. 즉, 앱이 응답하지 않는 오류가 발생할 수 있습니다. 또한, 기술적 발전으로 인해 새로고침 주파수가 매년 높아지고 있습니다.
따라서 안드로이드 개발자는 항상 긴 작업을 별도의 스레드에서 비동기적으로 실행하도록 노력해야 합니다. 이 때, 코루틴은 rxjava, AsyncTask, executors, HandlerThreads, IntentServices와 같은 다른 방법으로 처리하던 귀찮은 멀티스레딩 작업을 쉽고 효율적으로 수행할 수 있도록 합니다.
코루틴은 비동기 코드를 순차적으로 작성할 수 있도록 하여 콜백으로 인한 불필요한 보일러플레이트 코드를 피하고, 코드를 더 읽기 쉽고 유지보수하기 쉽게 만듭니다.
코루틴을 사용하기 위해서는 build.gradle 파일에 아래와 같은 내용을 추가해야 합니다.
https://github.com/Kotlin/kotlinx.coroutines
GitHub - Kotlin/kotlinx.coroutines: Library support for Kotlin coroutines
Library support for Kotlin coroutines . Contribute to Kotlin/kotlinx.coroutines development by creating an account on GitHub.
github.com
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
간단하게 코루틴을 사용해보자
private fun UserData() {
for (i in 1..200000) {
Log.i("test", "유저 $i 번 ${Thread.currentThread().name}")
}
}
btnDownloadUserData.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
downloadUserData()
}
}
코틀린 코루틴에서는 모든 코루틴을 스코프 내에서 시작해야 합니다. 스코프에 속하는 프로퍼티들을 사용하여 코루틴을 쉽게 추적하고, 취소하며, 코루틴에서 발생하는 오류나 예외를 처리할 수 있습니다.
- CoroutinScope는 코루틴에 대한 범위를 제공하는 데 사용되는 인터페이스입니다.
- GlobalScope는 애플리케이션 전체 수명주기에서 작동하는 상위 코루틴 인터페이스입니다.
스코프를 사용한 다음 CoroutineScope의 경우 Dispatchers를 지정하여 코루틴이 실행될 스레드를 지정해줘야 합니다.
- Dispatcher.Main : 코루틴의 주 스레드 (UI 스레드), 이것은 주로 가벼운 작업에서 사용합니다. UI 함수 호출, LiveData 업데이트 가져오기 등
- Dispatchers.IO : 백그라운드 스레드에서 실행되고, 로컬 데이터베이스 작업, 네트워크 통신 및 파일 작업에서 주로 사용됩니다.
- Dispatchers.Default : JSON 파일 파싱과 같은 CPU 집약적인 작업에 주로 사용됩니다.
- Dispatchers.Unconfined : GlobalScope에서 사용하는 Dispatcher입니다. 이것을 사용하면 코루틴은 기본 스레드에서 실행되지만 일시 중지되고 다시 시작 될 때는 일시 중지된 함수가 실행 중인 스레드에서 실행됩니다.
다음은 코루틴 빌더입니다. 코루틴 빌더는 코루틴을 실행하기 위해 사용되는 스코프의 확장 함수입니다.
- launch : 새 코루틴을 시작하고 호출자에게 결과를 반환하지 않습니다.
- async : 새 코루틴을 시작하고 await라는 정지 함수로 결과를 반환하도록 허용합니다.
- withContext : CoroutineScope에서 실행되는 suspending 함수로, 코루틴에서 어떤 작업을 수행할 때 해당 작업을 실행하는 코루틴의 문맥(Context)을 변경할 때 사용됩니다. 사용하면 콜백을 도입하지 않고도 코드 줄의 스레드 풀을 제어할 수 있으므로 데이터베이스에서 읽기, 네트워크 요청 실행과 같은 매우 작은 함수에 이를 적용할 수 있습니다.
우리는 여기서 suspend 키워드를 사용해야합니다. suspend는 코루틴에서 사용되는 함수에 붙이는 키워드로, 해당 함수가 일시적으로 중단되고 나중에 다시 재개될 수 있도록 해줍니다.
private suspend fun UserData() {
for (i in 1..200000) {
withContext(Dispatchers.Main) { // UI 스레드에서 실행되도록 보장하기 위해
tvUserMessage.text = "유저 $i 번 ${Thread.currentThread().name}"
}
}
}
Async & Await
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.Main).launch {
Log.i("MyTag", "시작...")
val stock1 = get1()
val stock2 = get2()
val total = stock1 + stock2
Log.i("MyTag", "총 $total")
}
}
}
private suspend fun get1() : Int {
delay(10000)
Log.i("MyTag", "1번 끝 ")
return 55000
}
private suspend fun get2() : Int {
delay(8000)
Log.i("MyTag", "2 번 끝 ")
return 35000
}
이런 식으로 코드를 짜다보면 총 걸리는 시간은 약 18초가 걸릴것이다. 그러나 이것을 동시에 처리한다면 더 빠르게 작업을 할 수 있을것이다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Main).launch {
Log.i("MyTag", "시작...")
val stock1 = async(IO) {
get1()
}
val stock2 = async(IO) {
get2()
}
val total = stock1.await() + stock2.await()
Toast.makeText(applicationContext, "총 $total", Toast.LENGTH_SHORT).show()
}
}
}
private suspend fun get1() : Int {
delay(10000)
Log.i("MyTag", " 1번 끝 ")
return 55000
}
private suspend fun get2() : Int {
delay(8000)
Log.i("MyTag", " 2번 끝 ")
return 35000
}
이런 식으로 구현을 한다면 각각의 get1()과 get2() 함수가 각각 10초와 8초의 시간이 걸리므로, 병렬로 처리한다 해도 최소 10초 이상 걸립니다. 이것이 코투린의 동시성 구현하는 방법입니다.
서도 다른 데이터를 병렬로 가져오고 결과를 결합하기 위해 async 와 await를 사용하는 방법입니다.
구조화된 동시성이란 코틀린 누출을 방지하고 코루틴을 생산적으로 관리하기 위해 코틀린 코루틴에 도입되었습니다.
suspend fun getTotalUserCount() : Int {
var count = 0
CoroutineScope(IO).launch {
delay(1000)
count = 50
}
val deferred = CoroutineScope(IO).async {
delay(3000)
return@async 70
}
return count + deferred.await()
}
// 구조화 되지 않은 동시성
이 결과 값은 70이 나온다.
50이 반환되지 않은 이유는, launch 함수는 비동기적으로 코루틴을 실행하며, 바로 Job 객체를 반환합니다. 이 때, Job 객체는 코루틴의 실행 상태를 추적하는 데 사용됩니다. 따라서, launch 함수에서 생성한 코루틴이 완료되기 전에 launch 함수가 이미 반환되므로, 코루틴의 실행 결과를 직접 가져올 수는 없습니다.
70이 반환된 이유는 async 함수가 반환한 Deferred 객체의 await() 메서드를 호출하여, async 함수에서 반환한 값을 대기하고 가져온 결과입니다.
async 함수는 비동기적으로 실행되며, 반환된 Deferred 객체에 대해서는 await() 메서드를 호출하기 전까지 실행이 중단됩니다. 따라서, deferred.await() 문장은 async 함수가 실행되어 70을 반환한 후에 실행되므로, 70이 올바르게 반환됩니다.
suspend fun getTotalUserCount(): Int {
coroutineScope {
launch(IO) {
delay(1000)
count = 50
}
deferred = async(IO) {
delay(3000)
return@async 70
}
}
return count + deferred.await()
}
// 구조화된 동시성
coroutineScope 블록 내부에서 launch와 async 함수를 호출하면, 두 함수가 생성한 코루틴이 완료될 때까지 기다렸다가, coroutineScope 블록을 빠져나갈 때, 결과를 반환합니다. 그렇게 120을 출력할 수 있습니다.
coroutineScope와 CoroutinScope 모두 코루틴에서 사용되는 인터페이스이지만, 차이점이 있습니다.
CoroutinScope는 코루틴 빌러를 사용하여 코루틴을 생성하는 기본 인터페이스입니다. 코루틴 범위를 만들고, 그 범위 내에서 생성된 모든 코루틴을 추적하고 관리합니다. 또한, 코투린이 완료되거나 취소될 때 범위 내에서 발생한 예외를 처리할 수 있도록 합니다.
coroutineScope는 CoroutineScope의 확장 함수이며, CoroutineScope를 상속한 클래스나 인터페이스에서 사용됩니다. coroutineScope 함수는 새로운 코루틴 범위를 만들고, 그 범위 내에서 새로운 코루틴을 시작합니다. 이 함수는 부모 코루틴의 코루틴 범위를 상속받아 사용하므로, 부모 코루틴 범위에서 생성된 모든 코루틴들을 추적하고 관리할 수 있습니다. 또한, coroutineScope 함수는 자식 코루틴 중 하나라도 실패하면, 해당 범위 내의 모든 코루틴들을 취소하고 예외를 발생시킵니다.
따라서, CoroutineScope는 코루틴 범위를 만들고, 관리하며, coroutineScope는 CoroutineScope의 확장 함수로, 부모 코루틴 범위를 상속받아 하위 코루틴을 시작하고, 관리합니다.
'Android' 카테고리의 다른 글
[Android] Qr Zxing 라이브러리 적용하기 (0) | 2023.11.25 |
---|---|
[Android] App Main Icon 적용하기 (0) | 2023.03.24 |
[Android] Lottie Splash (0) | 2023.02.24 |
[Android] 양방향 DataBinding (0) | 2023.02.09 |
[Android] LiveData (0) | 2023.02.06 |