▩ 목 차 ▩
1. 비동기
1-1. 동기 vs 비동기
1-2. 비동기가 필요한 이유
2. 코루틴
2-1.코틀린에 대해서
2-1-1. 협력형 멀티태스킹
2-1-2. 동시성 프로그래밍 지원
2-1-3. 비동기 처리에 대한 이점
2-2. rx vs 코루틴
2-3. 사용법
2-3-1. 예시(비동기 처리)
2-3-2. 예시(딜레이 처리)
3. 코루틴에 대한 고찰
나는 어떠한 것을 공부할때마다 코루틴이라는 것을 접하였고, 이것을 정확하게는 무엇인지는 모르지만, 비동기와 관련되어 도와주는 라이브러리 쉽게 말해서, 메인 쓰레드에서 작업할 것이 아닐때 다른 작업 쓰레드에서 무언가를 할 때 쓰레드를 생성하여 사용하는 것이 아닌 이 코루틴이라는 것을 사용하는 것으로 가볍게 생각하고 있었다. 내가 알고 있는것이 맞는지 한번 공부하여 비교해보겠다.
■ 1. 비동기 ■
■ 1-1. 동기 vs 비동기
동기 : 요청을 보낸 후 결과물을 받아야지만 다음 동작이 이루어지는 방식, 모든 일은 순차적으로 실행되며 어떤 작업이 수행중이라면 다음 작업을 대기하게 된다.(순차적,직렬적)
- 장점 : 설계가 간단하고 직관적이다
- 단점 : 결과를 볼 때까지 아무것도 못하고 대기해야 한다.
- 예시(주문) : 첫번째 손님이 아메리카노를 시켰다. 첫번 째 손님의 커피가 나올 때 까지 2번 째 손님은 기다려야 한다.
첫번째 손님이 요청한 아메리카노가 나온 뒤에, 2번째 손님의 주문을 받을 수 있다. 즉, 앞의 손님의 요청과 결과가 다 나올때까지 뒤에 주문하는 손님은 계속 기다려야 한다.
즉, 동기 같은 경우, 손님의 요청이 있다면 그 손님의 요청의 결과를 처리한 후, 다음 손님의 요청을 처리한다.
비동기 : 동시에 일어나지 않는 것을 의미한다. 즉, 요청과 결과가 동시에 일어나지 않는 거라는 약속이다. 요청한 그 자리에서 결과가 주어지지 않는다. 노드 사이의 작업 처리 단위를 동시에 맞추지 않아도 된다.
- 장점 : 결과가 주어지는 데 시간이 걸리더라도 그동안 다른 작업이 가능해 자원의 효율적인 사용이 가능하다.
- 단점 : 설계가 동기보다 복잡하다.
- 예시(주문) : 첫번째 손님이 아메리카노를 시켰다. 첫번째 손님은 진동벨을 가지고 커피를 기다린다.
(첫번째 손님의 커피는 나오지 않았지만) 카운터에서 두번째 손님의 주문을 받는다.
진동벨이 울리면, 첫번째 손님이 카운터로 와서 커피를 받아간다.
즉, 비동기 같은 경우, 일단 손님들의 요청을 계속 받으면서, 요청한 결과를 처리한다.
■ 1-2. 비동기가 필요한 이유
만약 우리가 데이터를 서버에서 받아오는 앱을 만든다고 가정을 해보자.
먼저, 서버로부터 데이터를 받아오는 코드가 실행되어야 할 것이다. 여기서 만약 비동기로 처리하지 않고, 동기적으로 구성을 하게 된다면, 데이터를 받아오기까지 기다린 다음에 앱이 실행될 것이다. 서버에 가져오는 데이터 양이 적으면 시간이 적게 걸리지 않으므로 그렇게 크게 문제가 되지 않을것이다. 하지만 서버에 가져오는 데이터 양이 많다면 ? 앱의 실행속도는 기하급수적으로 느려진다. 그렇기에 데이터를 불러오기까지 앱이 대기하는 상태가 발생할 것이다.
==> 이러한 불편을 없애기 위해 비동기적으로 처리(메인 스레드가 아닌 별개의 스레드에서 작업)를 해야하는데 안드로이드에서는 코루틴 인 것이다.
■ 2. 코루틴 ■
코루틴(Coroutine)은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴이다.
■ 2-1.코틀린에 대해서
■ 2-1-1. 협력형 멀티태스킹
코루틴(CoRoutine)은 Co + Routine 의 말을 합친 말이다. 여기서 Co 라는 접두어는 "협력", "함께" 라는 의미를 지니고 있고, Routine은 하나의 태스크, 함수 정도로 생각하면 될 것이다.
코틀린에 더 알고 싶으면 Routine에 대해 알아야 한다. Routine은 main routine과 sub routine이 존재 하는데, 쉽게 메인 루틴은 메인 스레드라고 생각하면 편할 것이다. 서브 루틴은 함수로서, 함수 안에 있는 코드가 전부 실행이 되면 이 서브루틴(함수)가 빠져 나오게 된다.
즉, sub routine은 루틴에 진입하는 지점과 루틴을 빠져오나는 지점이 명확하다.( 맨 처음 진입 , return문을 통해 빠져나옴)
하지만 코루틴(Coroutine)은 조금 다르다.
==> 코루틴도 routine이기 때문에 하나의 함수로 생각을 해야 한다. 그런데 이 함수에 진입할 수 있는 진입점도 여러개고 함수를 빠져나갈수 있는 탈출점도 여러개다. 즉, 코루틴 함수는 retrun문 뿐만 아니라 언제든지 중간에 나갈 수 있고, 언제든지 다시 나갔던 그 지점으로 들어올 수 있다는 것이다. 분명 이 부분은 sub routine과 많이 다르다.
( 밑에서 사용하는 방법을 알려줄 때 중간에 나갈 수 있는 방법이라던지 나갔던 그 지점으로 들어오는 방법을 소개 할 것이다.)
■ 2-1-2. 동시성 프로그래밍 지원
함수를 중간에 빠져나왔다가, 다른 함수에 진입하고, 다시 원점으로 돌아와 멈추었던 부분부터 다시 시작하는 동시성 프로그래밍을 가능하게 한다. ( 함수를 실행하는 중 딜레이 되는 시간에 다른 함수에 진입해 실행하는 것으로 왔다 갔다 하는 시간이 빨라 사용자가 보기에는 마치 동시에 진행되는것 처럼 보인다. )
■ 2-1-3. 비동기 처리에 대한 이점
RxJava, RxKotlin으로 짜여진 동일한 코드에 비해 너무나 직관적이고 이해하기 쉽다.
■ 2-2. rx vs 코루틴
앱이나 웹이나 비동기처리가 핵심인 클라이언트 프로그래밍에서 가장 핫한 키워드는 rx일 것이다.
구글이 안드로이드 공식 개발 언어를 자바에서 코틀린으로 채택후에 안드로이드의 상당수 프로젝트에서 비동기 처리를 코루틴으로 처리하는 경우가 많아지고 있다. 물론 아직 rx를 쓰고 있는 프로젝트도 있고 코루틴이 rx에 비해 월등히 좋다라고 말할 수는 없을 것이다.
인터넷에 찾아본 결과 Fabio Collini가 다음과 같은 "알고리즘"을 제안을 했다.
- 이미 RxJava를 사용하고 있고 잘 동작 한다면 RxJava 를 사용
- 아키텍처가 Reactive Stream 을 기반으로 하는 경우 RxJava 를 사용
- 프로젝트가 Kotlin Native 를 사용한 멀티플랫폼 인 경우 코루
- 코드 베이스가 Java / Kotlin 인 경우 RxJava를 사용
- 이외에는, 코루틴 을 사용
아직 rx와 코루틴을 배우지 않은 사람이 있다면, 코루틴이 훨씬 더 효율적일 것이다. 왜냐하면 코루틴은 rx에 비해서 비동기 처리를 할 때 상대적으로 훨씬 쉽게 이루어져 있고, 러닝커브가 rx에 비해 비교적 쉽게 사용할 수 있기 때문이다.
나는 아직 둘 다 배우질 않았고, 앞에서 Fabio Collini가 말한 경우에도 속하지 않기에 코루틴을 선택하여 개발을 할 예정이다..^^
■ 2-3. 사용법
■ 2-3-1. 예시(비동기 처리)
gradle에 코루틴을 추가해준다. (최신 버전 정보 : https://github.com/Kotlin/kotlinx.coroutines )
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
grade에 코루틴을 추가했다면 아래와 같은 CorotineScope() 함수를 작성 할 수 있다.
CoroutineScope(Dispatchers.쓰레드종류).메서드 {
}
스레드의 종류는 총 3가지가 있다.
- Main : 메인 스레드, 화면 UI 작업 등을 하는 곳
- IO : 네트워크, DB 등 백그라운드에서 필요한 작업을 하는 곳
- Default : 정렬이나 무거운 계산 작업 등을 하는 곳
메서드의 종류는 총 2가지가 있다.
- lanuch
- 실행하고 잊어버리는 형태의 코루틴 실행
- 즉시 실행되고, 실행결과는 반환하지 않는다.
- 관리를 위한 Job 객체를 반환한다.
- join을 통해서 완료를 대기 할 수 있다.
- async
- 결과나 예외를 반환한다.
- 실행결과는 Deferred<T>를 통해서 반환하며 await을 통해서 받을 수 있다.
- await은 작업이 완료될 때 까지 기다린다.
CoroutineScope(Dispatchers.Default).async {
//무엇의 정렬이나 무거운 계산의 작업 등
}
예를들어 위의 코드는, 정렬이나 무거운 계산의 작업을 하는 스레드로 비동기 작업을 할건데 결과나 예외를 반환해달라 라는 의미가 된다.
CorotineScope(Dispatchers.Main).launch {
val html = CoroutineScope(Dispatchers.IO).async {
getHtml()
}.await()
textView.text = html.toString()
}
예를들어 위의 코드는, await()가 나오는데, 이것은 작업이 완료될 때 까지 기다리겠다는 뜻이다. 작업이 완료되면 html에 값이 들어가고, 그걸 textView에 뿌려주는 UI 작업을 하는 것이다.
==> 아까 위에서 말한 대기상태를 만들어주지 않기 위해 다른 스레드에서 작업을 한다고 했는데, 작업이 완료되는것을 기다리는것은 옳지 않지 않은가..?
맞다. 아래의 예시를 보면 위의 예시를 이해할 수 있을 것이다.
액티비티{
CoroutineScope(Dispatchers.Main).launch {
val html = CoroutineScope(Dispatchers.Default).async {
getHtml()
}.await()
textView.text = html.toString() // 이거보다 밑에 토스트가 더 먼저 실행됨
}
Toast.makeText(this, "이게 먼저 뜨지롱", Toast.LENGTH_SHORT).show() // 다른 UI 작업
}
CoroutineScope 괄호 안에 있는 건 별도의 스레드라고 생각하면 된다. 즉, 괄호 안에서 엄청 늦게 작업이 진행된다고 해도 괄호 밖에서는 별도로 작업을 계속 진행한다는 소리이다.
위 코드를 보게 되면,
코드의 순서에 따라 textView가 Toast메시지 보다 먼저 바뀔 것 같지만 getHtml()작업이 느려서 토스트 메시지가 먼저 뜬다.
==> 이것이 바로 코루틴을 이용한 비동기 실행이다.
■ 2-3-2. 예시(딜레이 처리)
내부 DB 데이터를 삭제하는 코드() // 이 작업이 느려서
recyclerView를 새로고침 하는 코드() // 이게 먼저 실행될 수 있다.
네트워크 작업과 달리 내부 DB를 이용하는 작업은 메인 스레드에서 할 수 있다.
내부 DB를 사용하는 작업은 무거운 작업에 속하고 이것을 recyclerView와 메인스레드에서 같이 사용하다 보면,
DB에 데이터가 많아 삭제 하는데 시간이 오래 걸리는 작업의 경우에 코드상으로는 분명히 삭제를 하고 refresh를 했는데 화면에는 변화가 없는 것처럼, 삭제가 이루어지지 않은 것처럼 보이게 될 것이다. 하지만 실제로 로그를 찍어보면 새로고침이 먼저 되고 DB 삭제가 이루어진 것이다.
==> 이때 핸들러를 이용해 임의로 몇 초 늦췄다가 명령어를 실행하도록 하는 해결 방안이 있다.
내부 DB 데이터를 삭제하는 코드()
val handler = android.os.Handler()
handler.postDelayed({
recyclerView를 새로고침 하는 코드()
}, 1000)
하지만, 이 핸들러를 이용하는 방법은 크게 2가지 단점이 존재한다.
- 데이터량이 적은 경우, 삭제를 빨리 끝내고 새로고침을 빨리 할 수 있는데도 불구하고 지정해둔 시간만큼 기다렸다가 다음 명령어를 실행이 되는 것 즉, 시간을 낭비될 수 있는 것이다.
- 데이터량이 많아져 지정해둔 시간보다 더 오랜 시간이 걸리는 경우, 위에서 말한 문제인 새로고침이 먼저 되고 DB삭제가 이루어지는 상황이 되풀이 된다.
그렇기에 핸들러는 근본적인 해결책이 되지 못한다.
==> 바로 이럴때 코루틴을 사용하면 된다. 쓰레드는 네트워크 작업이 아니니까 Dispatchers.Default로 설정을 해주고, 내부 DB 작업을 하고, 기다렸다가 끝나면 바로 recyclerView를 새로고침 해주는 것이다
CoroutineScope(Dispatchers.Main).launch {
val temp = CoroutineScope(Dispatchers.Default).async {
내부 DB 데이터를 삭제하는 코드()
}.await()
recyclerView를 새로고침 하는 코드()
}
■ 3. 코루틴에 대한 고찰 ■
코루틴은 내가 생각했던 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴인 것은 맞았다. 하지만 이것을 필요로 하는곳은 정말로 천차만별로 다양한 하다는 생각이 들었다. 단순하게 네트워크 통신을 위해 비동기적으로 이용을 할때는 물론이거니와 리사이클러뷰를 사용할 때 데이터가 너무 커 리사이클러뷰가 미리 새로고침 되는 경우 등등 많은 경우에 쓰인다는 것 말이다. 이말은 즉슨.. 쓰임에 따라 난이도는 천차 만별이라는 소리도 같다는 말이다. 프로젝트를 진행해보면서 비동기적인 기능이 필요할 때 적극적으로 코루틴을 사용을 해보겠다.