본문 바로가기

개발 코딩 정보 공유/안드로이드 자바 코틀린

안드로이드에서 사용하는 코루틴 (코틀린)

왜 이런글을?

안드로이드 (코틀린) 에도 코루틴을 통한 비동기 처리를 사용할수 있기 때문이죠. 안드진영에서는 대대손손 물려받은? 자바 계열의 스레드 개념을 활용하여 비동기 처리를 하곤 했습니다. 당연히 그렇게 사용했습니다. 그런데 이 스레드를 컨트롤 한다는건 매우 위험하고 어려운 복잡한 그러한 일이었기에...(대표적으로 메모리 문제 그리고 메모리 문제) 코틀린으로 넘어오면서 코루틴 이라고 하는 개념을 추가하여 비동기 방식의 로직처리를 안정적으로  할수 있게 되었습니다. (과연그럴까요?)

= 정리 + 요약 하여 해당 내용을 공유 합니다.


코루틴? (Coroutine)

이름이 비슷하다고 해서 코루틴은 코틀린에서 새로 등장한 지식이 아닙니다.

아주 오래 전부터 기존에 있던 이론입니다.

 

코루틴(coroutine)은 루틴의 일종으로서, 협동 루틴이라 할 수 있다(코루틴의 "Co"는 with 또는 togather를 뜻한다). 상호 연계 프로그램을 일컫는다고도 표현가능하다

 

코루틴은 기존의 함수(루틴) 호출과는 다른점이 있습니다.

코루틴은 해당 라인으로의 진입과 탈출이 자유자재 입니다.

rx 계열의 사용이나 javascript 의 async await 등을 사용해보셨다면 이해가 빠를것 입니다.

 


이걸 어디다 쓰지? 

안드로이드에서의 대부분의 사용은 시간이 많이 소요되는 로직에 사용 합니다.

Api를 호출 하거나 db작업을 한다거나 데이터를 가공한다거나 ... 등등 한마디로 긴작업, 백그라운드 작업등을 처리할때 사용합니다. 비동기 작업을 위해 기존에 스레드 기반하는 로직을 짜셨다면... 당장 코루틴을 사용하세요.

rx계열을 이용해 처리 하셨다고요? 그래도 코루틴 한번 써보세요. rx... 이제 걷어낼때가 되었습니다.


핵심 키워드

코루틴 사용에 있어서 오늘 해볼 핵심적인 키워드 몇가지를 나열해 보겠습니다. (눌러보지마세요. 링크없습니다 ㅎㅎ)

 

  • runBlocking
  • CoroutineScope
  • launch
  • Dispatcher
  • suspend
  • delay
  • start
  • await
  • async
  • join
  • cancel
  • Deferred
  • Lazy
  • GlobalScope

사용방법

주의 : 이 글을 올바르게 이해하기 위해서는 비동기처리와 스레드에 대한 사전 지식이 필요합니다.

 

자 다짜고짜 아래의 소스를 실행해 봅니다.

runBlocking{
	delay(2000)
	Log.d(“TAG”,”===runBlocking===”)
}
Log.d(“TAG”,”===runBlocking exit===”)

 

저는 버튼을 클릭했을때 저 로직을 실행하도록 하였습니다. 실행 해보면 2초 뒤에 콘솔창에 메세지가 찍히는걸 볼수 있습니다. (코루틴블록은 만나자 마자 바로 실행이 되는 이를테면 rx에서의 Hot ? 같습니다)

 

===runBlocking===
===runBlocking exit===

 

콘솔찍히는걸 봐서는 스레드가 행이 걸렸다는걸 알수 있습니다. 네 맞습니다. runBlocking 은 중단 함수 입니다. 블록안에 로직이 끝날때 까지 해당 스레드(메인) 를 블록 시켜 버립니다. 그래서 실제 사용은 하지 않는게 좋습니다. 코루틴 문서에도 테스트 시에 사용할것을 권장합니다. 또한 실전에서 굳이 메인스레드를 블록처리 해야할  이유가 없죠. 코루틴의 장점은 일시정지. 입니다.

Stop이 아닌 Pause 이죠.

 

그럼 다음 소스를 보시죠.

 

CoroutineScope(Dispatcher.Main).launch{
	delay(2000)
	Log.d(“TAG”,”===runBlocking===”)
}
Log.d(“TAG”,”===runBlocking exit===”)

 

소스가 길어졌지만 runBlocking을 CoroutineScope로 대체한것 뿐입니다 당황하지마세요.

실행해보면 어떤가요? 어라? 결과가 좀 다르게 나왔습니다.

 

===runBlocking exit===
===runBlocking===

 

왜 이런결과가 나왔을까요? 흐름을 따라가보면 Dispatcher.Main 으로 사용할 스레드를 지정해주고 launch되며 코루틴 라인으로 들어가서 delay 에 걸렸습니다. 그리고는 코루틴 블록을 빠져나가게 됩니다. “===runBlocking exit==“ 가 찍히고 나서는 스레드는 어디로 갔을까요??? 네 2초가 지난후 Log.d(“TAG”,”===runBlocking===”) 를 실행하러 다시 코루틴 블록에 진입 합니다. 그리곤 다시 할일하러 탈출. 어떤가요? 이게 바로 코루틴의 장점 입니다. 해당 코루틴블록을 왔다리 갔다리! (goto문 같은건 아님) 한다는 것이죠. 스레드가 할일을 하다가 일시정지 하고 갔다가 필요하면 다시 이쪽 할일을 하러 옵니다!

 

해당글은 안드로이드 기반에서 구현하였습니다. Main 함수등에서 테스트시에는 CoroutineScope 만 사용하면 메인스레드가 죽어버리니 runBlocking 과 같이 사용하여 테스트 해봐야합니다.

 

물론 스레드로 이러한 로직에 구현도 가능합니다(다만 멘붕이 올뿐)

스레드를 몇개를 만들고 돌리고 돌리고 wait과 sleep 과 syncronize... 등등 신경쓸게 한두가지가 아니죠.

생각만 해도 피곤합니다. 그 모든걸 코루틴을 통하면 아주 쉽고 편하게 해결할수 있습니다.

그것도 아주 싼값에 말이죠(자원 적으로) 실제로 스레드와 코루틴의 성능 비교 시에 코루틴이 월등한 퍼포먼스를 보였습니다.

스위칭시 생기는 오버해드도 없고 스레드도 단 하나면 충분하죠.한마디로 싱글 스레드만으로 멀티스레딩적인 사용이 가능합니다.

 

위에서도 이야기 했지만 자바스크립트의 async await 를 써보셨다면 이해가 빠르겠습니다. (자바스크립트는 싱글스레드)

처음에는 코루틴과 스레드의 상관 관계를 몰라서 한참 해메였습니다. 스레드는 스레드 이고 코루틴은 코루틴 입니다. 결국에 실행 주체는 스레드 이며 코루틴은 그 스레드가 왔다 갓다 하며 로직을 실행하는 특수한 블록(혹은 함수) 정도로 생각하시면 쉬울것 같습니다.

 

자 그럼 여러가지를 조합해서 메인스레드와 새로운 스레드를 돌려보며 테스트 해보겠습니다.

 

runBlocking{ //Main쓰레드가 블록을 실행
	CoroutineScope(Dispatchers.IO).launch{
	//새로운 스레드에서 (worker) 해당 로직처리
	delay(2000)
	Log.d(“TAG”,”===run===”)
}
Log.d(“TAG”,”===exit===”)
}

 

기존로직에서 + 중간에 새로운 스레드로 일을 시켰습니다. 어떤가요? 예상이 되시나요? 간단하게 생각 해보면 runBlocking 을 사용했으니 메인 쓰레드가 코루틴 로직이 처리되길 기다리겠죠? 그리곤 exit를 찍을 겁니다. 그러나 답은 땡 입니다. CoroutineScope(Dispatchers.IO) 에서 새로운 스레드를 사용하기로 했으니 Main 스레드는 삐져서 그냥 가버립니다. ^^; 그러나 이렇게 되면 runBlocking을 쓰는 이유가 무색해져 버리겠죠? (runBlocking은 해당 로직이 처리 될때까지 블록됨을 명시적으로...) 이해를 위한 테스트 임을 알아주세요.

자 그럼 좀더 심화해서 가 보겠습니다. runBlocking안에서 IO 쓰레드를 돌리면서 해당 작업을 기다린 후 탈출하는 로직을 짜고 싶다면 어떻게 할까요? job 을 사용해서 잘 버무려 보죠.

 

runBlocking{
	val cs = CoroutineScope(Dispatchers.IO)
	val job = cs.launch{
		delay(3000)
		Log.d(“TAG”,”===CoroutineScope run===”)
    }
	Log.d(“TAG”,”===runBlocking run===”)
	job.join()
}
Log.d(“TAG”,”===runBlocking exit===”)
===CoroutineScope run===
===runBlocking run===
===runBlocking exit===

 

네. 어떤가요? 원하는대로 작업을 기다린후 메인쓰레드가 탈출하죠?

실전에서는 메인스레드를 기다리게 할일은 아마 거의 없을겁니다. runBlocking 이 아닌 CoroutineScope를 중첩해서 돌리고 해당 작업을 기다린후 순차로 처리할게 있다면? 그땐 join 도 고려해볼수 있을겁니다.

지금까지는 Coroutine 블록의 실행이 바로 시작되는 경우였구요. 이번에는 코루틴의 실행 시점을 우리가 제어해 보겠습니다. 바로 아래처럼 말이죠.

 

val myJob = GlobalScope.launch(context = Dispatchers.Main, start = CoroutineStart.LAZY) { delay(3000) print("Lazy 사용") }
myJob.start() //start 는 deffed 값리턴이 없고 기다리지 않는다. (행걸림 x)

 

CoroutineStart.LAZY 라고 하는 파라메터를 추가해줬는데요. 말 그대로 게으른. 코루틴블록 입니다.

바로 실행이 되지 않고 우리가 시작하고자 하는 시점에서 시작할수 있죠. 시작할수 있는 방법은 start() 가 있구요. 다른방법으로는 async/await을 사용하는 방법이 있습니다.

 

val myJob2:Deferred<String> = GlobalScope.async(context = Dispatchers.Main, start = CoroutineStart.LAZY) { 
	delay(3000) 
    print("Lazy async await 사용")
	"aaa" //return 값 
}
suspend { 
	val msg = myJob2.await() print("${msg}") 
}

 

lauch 를 사용하여 코루틴 블록을 만들지 않고 이번엔 async 를 사용해서 만들었습니다. 그리고 start = CoroutineStart.LAZY 를 사용해 게으른 코루틴을 만들어주었습니다. 그런데 이상한게 보입니다. 마지막 리턴값이 보이시나요? "aaa" 라는 String 리턴값을 주었는데요. async 를 사용한 코루틴 블록은 Deferred<?> 타입으로 생성되게 됩니다. 그래서 코루틴 로직을 실행하고 마지막엔 값을 리턴하도록 할수 있습니다. start()로 실행할경우 이런게 없었죠?

실행시점은 await() 을 통해서 시작됩니다. 그리곤 해당 작업이 끝나길 기다리고 (누가? 코루틴 블록을 만든 스레드가 기다리겠죠?) 그리곤 해당 리턴값을 받아서 콘솔에 찍어 보았습니다.

그런데 중간에 이상한게 보이는데요. suspend 블록이 있네요? 이건 await 함수를 쓰기 위함입니다. async await 을 쓰기 위해서는 suspend 블록에서 사용해서 명시적으로 중단 가능이 있다는걸 표시해줘야합니다. 규칙입니다. 왜?? 는 없습니다. (runBlocking도 마찬가지였죠?) async await 에서 뿐만 아니라 중단코드(delay등)가 있거나 네트워크 통신으로 리턴값을 기다린다거나, 디비작업같은 긴 작업이 있다면... 중단 가능성이 있다면 무조건 써주면 됩니다. 아래와 같이 lauch 코루틴블로에서도 사용합니다. 이번에는 함수로 묶어서 사용도 가능합니다. 사용은 아래와 같습니다.

 

CoroutineScope(Dispatchers.Main).launch {
	longTimeWork()
}

private suspend fun longTimeWork(){ 
	delay(3000L) 
	print("longTimeWorkComplete !") 
}

 

이렇게 해서 매우 두서없이... 코루틴 사용에 대해 알아보았는데요. 시간이 되면 좀 더 정리해서 다시 올려보도록 하겠습니다. 어렵게 생각하면 골치 아픕니다. 스레드가 있고 코루틴이라고 하는 블록이 있는데 그 블록을 실행하기 위해 스레드가 언제든 왔다갔다 할수 있다. 그리고 그걸 우리가 제어할수 있다... 정도로 생각하시면 이해가 쉬울것 같네요. 모바일로 작성하느라 매우 혼란한 글이 되었습니다. 다시 한번 정리해서 rx와 비교도 하고 코루틴 실사용 2탄을 올려보겠습니다.

 


꿀팁

지금 어떤쓰레드가 이 라인을 실행하고 있는건지? 너무 궁금하지 않습니까?

그렇다면 바로 아래와 같이 확인 하세요.

 

Log.d("TAG", "===${Thread.currentThread().name}===")

 

참고 문서