요즘 안드로이드에서 네트워크 통신은 필수사항이 되었습니다. 네트워크 통신이 많아짐에 따라서 비동기 처리에 대한 관심이 자연스레 높아지고 있습니다. 비동기 처리 구현방법 중에 많이들 사용하시는 방법에는 Rx가 있는데, Rx의 경우 학습에 오랜 시간이 걸려 진입 장벽이 매우 높은 것으로 알려져있습니다. Rx외 또다른 비동기 처리 구현방법으로 코루틴(Coroutine)이 있는데, 오늘은 코루틴에 대해 간단하게 알아보도록 하겠습니다.
코루틴(Coroutine)이란?
코루틴(Coroutine)은 안드로이드에서 제공하는 백그라운드 스레드 처리방법 중 하나입니다. 기존 안드로이드에서는 AsyncTask라는 비교적 간편한 방법을 이용했었는데, 이는 성능도 낮고 현재 Deprecated 되어 점점 사용하지 않는 것이 추세가 되었습니다. 그래서 AsyncTask에 비해 성능도 좋고, 구현도 간단한 코루틴의 사용이 점점 늘고 있습니다.
코루틴은 안드로이드에서 나오기전부터 C#, Python등에서 이미 지원하고 있던 개념입니다. 저 역시도 Unity(C#)를 이용해 졸업작품으로 게임을 만들때 코루틴을 사용했던 기억이 새록새록 떠오르는 것 같습니다:)
그렇다면 코루틴은 언제 사용할까요?
코루틴은 간단한 코드로 구현이 가능하고, 블록구조로 처리가 가능하기 때문에 하나의 요청(Request)-응답(Response) 후에 순차적으로 다른 작업을 처리하게 최적화되어 있으므로 네트워크 통신(Retrofit, Volley)이나 내부저장소 접근(Room, SQLite)등에 주로 사용되곤 합니다.
Coroutine vs. Thread
코루틴은 스레드와 조금은 다른 개념으로 코루틴을 좀 더 잘 이해하기 위해서 Coroutine과 Thread의 차이에 대해 알아야합니다.
코루틴을 간단하게 표현하면 하나의 실행 및 종료되어야 하는 일(Job)이라고 할 수있고, Thread는 그 일(Job)이 발생하는 장소라고 생각하시면 간단합니다.
하나의 Thread가 일을 마치기전까지 다른 스레드는 Blocking되어 사용할 수 없는 반면, 코루틴은 실행 중간에 다른 작업을 하러 갔다가 돌아와서 다시 작업을 할 수 있습니다. 즉 코루틴은 Blocking없이 자유롭게 작업을 중간중간 실행했다 종료했다를 반복할 수 있습니다.
여기서 코루틴은 Blocking 되지않는 점이 중요합니다. 우리가 흔히 UI 변경은 메인 스레드에서 하고, 네트워크 처리는 백그라운드 스레드에서 한다는 것은 안드로이드를 개발한다면 잘 아는 사실 입니다. 그래서 네트워크 통신이후 그에 따른 UI 변경을 위해서는 스레드가 백그라운드에서 메인으로 바꿔 사용되는데, 이때 바로 Context-Switching이 일어나게 됩니다. 그에 반해 코루틴을 사용하면 스레드가 아닌 서브 루틴을 일시정지(suspend)시키는 방식을 동작하기 때문에 Context-Switching으로 인한 비용은 발생하지 않습니다!
Context-Switching란?
스레드 실행이나 종료시 스레드의 상태를 저장하거나 복구하는 프로세스로, 두개의 스레드에서 CPU가 스레드를 점유하고 놓아주는 것을 반복할때 발생합니다. 이 때문에 CPU에서 매우 큰 메모리 소모가 발생해 비효율적인 행위라고 볼 수 있습니다.
주요개념 및 사용방법
1. 의존성 추가하기
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
2. Scope
Scope란 코루틴의 제어범위를 뜻합니다. 종류로는 GlobalScope와 CoroutineScope 두가지가 있습니다.
먼저 GlobalScope는 앱이 종료될때까지 실행됩니다. 예를 들어 한 액티비티내에서 실행한 GlobalScope내 코루틴은 액티비티가 종료되어도 동작합니다. 이는 앱이 종료되고 나서야 종료됩니다. 이렇듯 GlobalScope는 별도의 생명주기가 뚜렷하지않는 작업이나, 파일 다운로드와 같이 화면과 상관없는 작업에 적절합니다. 다음으로 CorouineScope의 경우 GlobalScope와 반대로 생명주기가 뚜렷하거나 특정목적이 있는 경우 사용합니다. 그래서 특정목적을 따로 지정해줘야하는데 바로 Dispachers(스레드 지정)입니다.
<Dispachers 종류>
1. Main : 메인스레드에서 동작하며 UI 업데이트나 즉각적인 작업에 사용됩니다.
2. IO : 네트워크 통신이나 내부저장소 접근과 같이 가벼운 작업에 사용됩니다. (메인스레드에 영향없이 사용됩니다.)
3. Default : CPU를 많이 사용하는 무거운 연산 작업에 사용됩니다.
GlobalScope.launch {
println("xxx GlobalScope")
}
CoroutineScope(Main).launch {
println("xxx CoroutineScope Main")
}
CoroutineScope(IO).launch {
println("xxx CoroutineScope IO")
}
CoroutineScope(Default).launch {
println("xxx CoroutineScope Default")
}
3. Builder
위 Scope 사용예시를 보면 계속해서 launch가 Scope(영역)과 함께 사용된 것을 보실 수 있는데, launch는 Builder의 한종류입니다. Builder는 즉, 코루틴의 작업 범위를 지정할때 사용되며 종류로는 크게 launch, async, withContext, runBlocking 4가지가 있습니다.
1) launch
- job 객체를 리턴합니다.
- job은 코루틴의 작업 단위를 뜻하고, job을 이용해 코루틴을 취소하거나 join()과 함께 사용해서 job이 완료될때까지 기다리게 할 수도 있습니다.
CoroutineScope(Main).launch {
println("xxx 1")
var job = launch {
delay(3000)
println("xxx 2")
}
job.join() // job이 끝날때까지 대기(blocking은 아님!)
println("xxx 3")
}
delay()를 이용하면 원하는 시간만큼 딜레이를 줄 수 있습니다. join()은 위에서 설명한 것과 같이 job이 끝날때까지 기다리게 해줍니다. 다만 delay()와 join() 모두 코루틴 작업 범위 내에서만 사용할 수 있습니다.
위의 코드에서 로그가 찍히는 순서는 어떻게 될까요? 1이 먼저 찍히고 3초 뒤에 2가 찍히고 그 다음에 3이 찍힐 것입니다(1->2->3). 이는 join()으로 인해 로그 2를 찍는 job이 끝날때까지 기다린 뒤에 로그 3을 찍기 때문에 발생한 결과입니다.
만약 join()이 없다면 어떻게 될까요? 아마도 1이 먼저 찍힌 뒤에 launch안의 3초 딜레이 카운트가 시작되고 바로 해당 launch를 탈출할 것입니다. 그리고 3 로그가 찍힐 것이며, 딜레이 카운트가 끝나고 나서야 마지막으로 2가 찍힐 것입니다!(1->3->2)
2) async
- 실제값을 반환합니다.
- await()와 함께 사용하면 결과를 반환할때까지 기다립니다.
..
CoroutineScope(Main).launch {
val res = CoroutineScope(IO).async {
// networking
// ..
}.await() // 네트워킹이 끝날때까지 대기
binding.tv.text = res // 메인스레드에서 UI 적용
}
Toast.makeText(this, "hi!", Toast.LENGTH_SHORT).show() // 얘는 언제?
..
위 예시처럼 코드를 작성하면 네트워킹이 반드시 끝난 뒤에 UI를 수정할 수 있습니다. 이처럼 비동기 처리를 간단히 구현할 수 있는 것입니다. 이는 async와 await()를 함께 썼기 때문에 가능한 것입니다. 그렇다면 코틀린의 장점이 Blocking 없이 효율적으로 작업을 할 수 있다는 것인데, await()를 통해 작업이 끝날 때까지 대기한다면 앞에서 말했던 코루틴 개념과 다른 부분이 아닌가하는 의문이 들 수 있습니다. 하지만 코루틴은 스레드와 다르게 Blocking 되지않기 때문에 await()로 작업이 끝날때까지 대기하는 동안, 해당 launch를 탈출해 다른 부분의 작업을 처리할 수 있습니다. 그렇기 때문에 위의 예시코드 마지막 줄의 토스트 호출 부분이 UI 적용 부분보다 먼저 실행됩니다!
3) withContext
- Dispacher를 변경할 수 있습니다.
- 결과가 반환될때까지 기다립니다.(asyc + await())
..
CoroutineScope(Main).launch {
withContext(IO) {
// networking
// ..
} // 네트워킹이 끝날때까지 대기
binding.tv.text = res // 메인스레드에서 UI 적용
}
Toast.makeText(this, "hi!", Toast.LENGTH_SHORT).show() // 얘는 언제?
..
위의 코드는 async에서 작성한 예시코드와 같은 역할을 합니다. 다만 차이가 있다면 withContext는 파라미터로 Dispacher를 받아 변경하실 수 있습니다.
4) runBlocking
- Scope 및 Builder 없이도 코루틴을 생성합니다.
- 결과값을 반환할때가지 스레드를 중지시킵니다.(사용에 유의!)
..
println("xxx 1")
runBlocking {
println("xxx 2")
delay(3000)
}
println("xxx 3")
runBlocking은 별도의 코루틴 영역 설정없이 사용가능합니다. 다만, 앞에 소개했던 withContext와 다르게, 결과값을 반환할때까지 스레드를 중지(Blocking)시키기 때문에 디버깅이나 특별한 이유가 아니라면 사용을 자제하는 것이 좋습니다.
(메인스레드에서 잘못 사용하면 메인 스레드가 죽어버릴 수 있습니다😢)
4. suspend
suspend 함수는 일시 중단 가능 함수를 뜻하며, 반드시 코루틴 내에서만 호출이 가능합니다. 함수 안에는 delay()와 같이 일시 중단이 가능한 작업이 있어야합니다. suspend 함수를 호출하면 해당 함수를 호출한 코루틴은 멈추게됩니다. 그리고 suspend 함수의 작업이 종료되면 이어서 코루틴 내 작업이 재개됩니다.
...
var job = CoroutineScope(Main).launch {
val subJob1 = launch {
for(i in 1..5) {
println("코루틴>>$i")
delay(500)
}
}.join() // subJob1이 끝날때까지 대기
helloSuspend()
val subJob2 = launch {
for(i in 6..10) {
println("코루틴>>$i")
delay(500)
}
}
}
...
suspend fun helloSuspend() {
println("Hello suspend function!!")
delay(1500)
println("Bye suspend function!!")
}
위 예시코드를 보면 job이라는 하나의 코루틴 작업 안에 subJob1, subJob2과 suspend함수인 helloSuspend()가 존재하고 있습니다. subJob1은 join()을 호출하고 있기때문에 해당 작업이 끝날때까지 코루틴은 대기할 것이므로 0.5초마다 로그를 찍을 것입니다.
그렇게 subJob1이 종료되면 suspend 함수인 helloSuspend()를 호출하여 메인이되는 job을 호출한 코루틴이 일시정지하고 helloSuspend()의 작업이 종료될때까지 대기하게됩니다. helloSuspend()내 1.5초간 작업이 모두 끝났다면 다시 코루틴내 작업인 subJob2를 재개하게 됩니다.
이렇듯 suspend function은 코루틴안에서 호출되야하며 호출시, 함수를 호출한 코루틴을 일시정지하고 suspend function내 작업을 기다리게됩니다!
(코루틴을 일시정지하면 Blocking이 된 것이 아니기때문에 코루틴 영역외의 다른 일을 하게됩니다.)
마무리하면서..
오늘은 간단하게 남아 코루틴에 대해 알아보았습니다.
코루틴이 요즘 개발에 있어서 핫한 개념이기때문에 저도 최근들어 공부를 시작했습니다. 코루틴에 대해 알아갈수록 정말 마법과 같은 친구라고 생각되고, 계속해서 너무나도 편리하고 좋다고 느끼게되네요..ㅎㅎ
다만 제가 알고있는 상식선으로는 코루틴의 겉핥기 정도뿐이기에 더더욱 열심히 공부해야겠다고 느꼈습니다!
이후에 코루틴의 이해 및 활용능력이 어느정도 정리되면 Rx에 대한 공부도 하고 싶네요🙂
지금까지 부족한 글 읽어주셔서 감사합니다🥰
참고 :
코틀린 코루틴(coroutine) 개념 익히기 · 쾌락코딩
코틀린 코루틴(coroutine) 개념 익히기 25 Aug 2019 | coroutine study 앞서 코루틴을 이해하기 위한 두 번의 발악이 있었지만, 이번에는 더 원론적인 코루틴에 대해서 알아보려 한다. 코루틴의 개념이 정확
wooooooak.github.io
https://todaycode.tistory.com/23
[kotlin] 코루틴 공부하기 (비동기 처리, 서버 딜레이 처리)
빌어먹을 코루틴... 저번에 공부하다가 도저히 못해먹겠어서 포기했다가 오늘 다시 도전했다. 항상 느끼는 거지만 아무것도 모를 땐 그렇게 어렵게 느껴지다가 또 막상 성공하면 아 이걸 왜 이
todaycode.tistory.com
https://superwony.tistory.com/125
[코틀린]Kotlin Coroutine 사용하기
안드로이드 프로그래밍에서 비동기 프로그래밍은 서버 통신 및 디비 접근과 같이 많은 부분에서 사용되는데, 그 부분을 조금 더 유연하고 간편하게 사용하도록 도와주는게 코루틴인것 같습니
superwony.tistory.com
'Android' 카테고리의 다른 글
[안드로이드 / Kotlin] ViewModel(뷰모델) (0) | 2022.04.12 |
---|---|
[안드로이드 / Kotlin] 키해시(Key Hash) 얻기 (0) | 2022.04.06 |
[안드로이드 / Kotlin] 타이머(Timer) 구현하기 (0) | 2022.03.18 |
[안드로이드 / Kotlin] BottomSheetDialogFragment (0) | 2022.03.16 |
[안드로이드 / Kotlin] RecyclerView Drag&Drop / Swipe 구현 (0) | 2022.03.15 |