본문 바로가기

[Kotlin] 코루틴 #1 - 기본

코루틴 #1 - 기본 

Kotlin Coroutine 공식문서 바로가기

 

Coroutines Overview - Kotlin Programming Language

 

kotlinlang.org

 

코루틴 Coroutine?

CoroutineThread / AsyncTask / Rx Background 작업을 대신할 수 있는 Asynchronous/Non-Blocking Programming 제공하는 경량스레드(Light-Weight Threads)

 

Background Task가 필요한 대표적인 경우 

    1. 네트워크 Request (Retrofit, OkHttp3, UrlConnection 등)
    2. 내부 DB 접근 (Room, SQLite 등)

 

 

Coroutine - Thread 차이점

둘 모두 Background-Task 작업이라는 점은 동일하지만, 서로의 개념은 다르다

  • Coroutine - 하나의 실행-종료 되어야 하는 일(Job) = 루틴(Routine) 개념
                    Co(협력,같이) + Routine(특정한 일의 실행위한 명령) 합성어
  • Thread - 루틴/일이 실행되는 곳(영역)

즉, 하나의 Thread에 여러 개의 Coroutine들이 동시에 실행할 수 있다는 점

Thread와 Coroutine

 

Thread

  • OS의 Native Thread에 직접 링크되어 동작하므로 많은 시스템 자원을 사용
  • Thread간 전환 시에도 CPU의 상태 체크가 필요하므로 그만큼의 비용이 발생

Coroutine

  • Coroutine은 즉시 실행하는게 아니며, Thread와 다르게 OS의 영향을 받지않기 때문에 그만큼의 비용 절약
  • Coroutine 전환 시 Context Switch가 발생하지 않음
  • 개발자가 직접 루틴을 언제 실행할지, 언제 종료할지 모두 지정이 가능
  • 이렇게 생성한 루틴은 작업 전환 시에 시스템의 영향을 받지 않아, 그에 따른 비용이 발생하지 않음

Thread와 Coroutine 차이 : Difference between Thread and Coroutine in Kotlin> 

 

 

핵심 키워드

CoroutineScope & GlobalScope

              : CoroutineScope는 코루틴의 범위, 코루틴 블록을 묶음으로 제어할 수 있는 단위
                GlobalScope
는 CoroutineScope의 한 종류로 Top-Level Coroutine,
                Application의 생명주기에 종속적으로 존재

 oroutineContext

              : CoroutineContext는 코루틴을 어떻게 처리할 것인지에 대한 정보 집합
             주요 요소로는 Job & dispatcher가 존재

 Dispatcher

              : Dispatcher는 CoroutineContext의 주요 요쇼
               CoroutineContext를 상속받아 어떤 스레드를 어떻게 동작할 것인가에 대한 정의

  어떻게란? Main / IO / Default 중의 어떤방식으로 처리할 것인지
   Dispatchers.Main - 메인(UI) 스레드에서 동작하는 방식
   Dispatchers.IO - 네트워크 / 디스크(파일) 작업에 사용하는 방식으로 File의 읽기/쓰기 & 소켓 읽기/쓰기 최적화
   Dispatchers.Default - CPU 사용량이 많은 작업에  사용, 메인 스레드에서 하기엔 긴 작업들에 적합 
          -> Main을 제외한 IO/Default백그라운드 작업

 launch() & async()

             : Scope의 확장함수로서, 코루틴을 만들고 실행하는 코루틴 빌더
               두 함수의 차이점으로는 객체와 반환에 차이점을 갖고있음.
               객체 - launch()는 Job() 반환 / async()는 Deferred<T> 반환
               반환값 - launch()는 반환값 X / async()는 반환값 존재 

 

라이브러리 의존성 추가

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'

 

기본 사용방법

가장 기본적인 사용방법 ( Main 메인 / IO 백그라운드)

// Scope객체 할당
val test = CoroutineScope(Dispatchers.Main)

// CoroutineContext를 Dispatchers.Main으로 메인(UI) 스레드 작업
test.launch {
	// 코루틴 블럭
}

// CoroutineContext를 Dispatchers.IO로 재정의, 백그라운드 동작
test.launch(Dispatchers.IO) {
	// 코루틴 블럭
}

 

코루틴 제어

launch() - Job 객체

launch() 함수로 시작된 코루틴 블록은 Job객체를 반환
Join() '대기'

val job = launch {
	var i = 0
    while (i < 3) {
    	delay(1000L)
        println("Coroutine running $i")	// 2,3,4 번째 출력
        i++
    }
}

println("test #1")	// 1번째 출력
job.join()		// 완료 대기
println("test #2")	// 5번째 출력 (마지막)

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 결과
test #1
Coroutine running 0
Coroutine running 1
Coroutine running 2
test #2

cancel() '대기'

GlobalScope.launch {
	val job = launch {
	    var i = 0
   	    while (i < 3) {
    	   	delay(1000L)
                println("Coroutine running $i")	// 2번째 출력
                i++
            }	
	}

	println("test #1")	// 1번째 출력
	delay(1200L)		// 1.2초 대기
	job.cancel()		// 중단
	println("test #2")	// 3번째 출력 (마지막)

}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 결과
test #1
Coroutine running 0
test #2

여러 코루틴 대기 - 2가지 방법, join() / joinAll()

val job1 = launch {
    var i = 0
    while (i < 5) {
    	delay(1000L)
    	i++
    }
}

val job2 = launch {
    var i = 0
    while (i < 5) {
    	delay(1000L)
    	i++
    }
}

// 방법 1 - 각각 코루틴마다 join() 설정
job1.join()
job2.join()

// 방법 2 - joinAll()에 같이 선언하기
joinAll(job1, job2)

 

async() - Deferred<T> 객체

async() 함수로 선언된 코루틴 블록은 Deffered<T>객체를 반환, <T>는 반환값의 자료형

val deferred :Deferred<T> = async {
    ... // 비동기 수행할 코루틴 블럭
    T // 결과값, ex) "result", 10, false 등을 보고 <T> 타입 추론
}

 

await() '대기'
   : launch()는 join() 함수로 대기하지만 async()는 await()함수로 대기하고 결과값을 전해 받음

val deferred = async {
    var i = 0
    while(i < 5) {
        delay(500L)
        i++
    }
    
    "result"
}

vsl text = deferred.await()	// deferred 코루틴 끝날때 까지 대기 후 결과값을 받음
println(text)
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 결과
result

여러 코루틴 대기 - 2가지 방법, await() / awaitAll()

val deferred1 = async {
    var i = 0
    while(i < 5) {
        delay(500L)
        i++
    }
    
    "result"
}

val deferred2 = async {
    var i = 0
    while(i < 5) {
        delay(500L)
        i++
    }
    
    "two coroutine"
}

// 방법 1 - 각각 await()로 대기
val text1 = deferred1.await()	// 코루틴 1 대기
val text2 = deferred2.await()	// 코루틴 2 대기
println("$text1 / $text2")

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 결과
result / two coroutine

 

코루틴 지연실행 

launch / async 코루틴 블록 모두 처리 시점을 지연해서 시작할 수 있는 방법

코루틴 빌더(launch / async)의 start인자에 CoroutineStart.LAZY값을 할당하면 해당 코루틴을 호출 하는 시점에 실행

val job = launch(start = CoroutineStart.LAZY) {
    var i = 0
    while(i < 5) {
        delay(500L)
        i++
    }
}

val deferred = async(start = CoroutineStart.LAZY) {
    var i = 0
    while(i < 5) {
        delay(500L)
        i++
    }
    
    "two coroutine"
}

// 코루틴 블록을 지연실행 시킬 경우 해당 코루틴을 호출(start / join, await)시 블럭 실행
job.start() // 방법 1 - 대기 X
job.join()	// 방법 2 - 대기까지

deferred.start() // 방법 1 - 대기 X
deferred.await() // 방법 2 - 대기까지 하고 결과값 받음

 

runBlocking()

runBlocking() 함수는 코드 블럭이 작업 완료하기 까지 스레드(Thread)를 점유하고 대기

점유하게 되는 스레드(Thread)는 runBlocking()함수가 호출된 위치의 스레드로 결정,

Main() 함수 내에서 선언할 경우 메인(UI) 스레드를 점유하기 때문에 긴 작업시간일 경우 ANR이 발생할 수 있음

백그라운드(워커 스레드)에서 선언 후 호출 시 백그라운드 스레드를 점유

runBlocking() 함수 자체가 점유하고 대기하기 때문에 launch()의 join이나 async()의 await등 다른 함수가 필요 없음

runBlocking {
	... // 코루틴 블럭 부분 
}

 

GlobalScope / ActivityScope

GlobalScope는 Application의 상태에 종속적으로, Activity가 onDestory되어도 동작하는 코루틴 Context

Activity LifeCycle에 종속적인 코루틴은? 따로 존재하지 않고 사용자가 정의해서 사용해야함

class MainActivity : AppCompatActivity(), CoroutineScope {	// 1. CoroutineScope 인터페이스 구현
    private latinit var job: Job
    override val coroutineContext: CoroutineContext		// 2. CoroutineScope인터페이스의 변수 오버라이드
        get() = Dispatchers.Main + job		// Main + job, 메인 스레드 
        
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        job = Job()		// Activity 실행 시 Job() 선언
    }
    
    override fun onDestroy() {
        super.onDestroy()
        job.cancel()	// Activity 종료 시 job 코루틴 중단
    }
}

 

코루틴 예외처리 'Exception'

코루틴 블럭 내에서 예외 발생할 경우 처리방법

// 예외 처리를 할 수 없어, 앱이 죽어버리는 방법
GlobalScope.launch(Dispatchers.IO) {	// IO, 백그라운드 환경
    launch {
        throw Exception()	// 임의로 Exception 발생
    }
}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

// 코루틴 선언 시 handler를 추가하여 예외 발생 시 콜백으로 처리하는 방법
GlobalScope.launch(Dispatchers.IO + handler) {    // 1
    launch {
        throw Exception()   // 2
    }
}

val handler = CoroutineExceptionHandler { coroutineScope, exception ->   // 3
    Log.d(TAG, "$exception handled!")
}

// 예외 발생 시 handler에 콜백으로 처리하여 앱이 죽지 않는 방법

 -> 예외 발생 시 등록한 handler에 위임하여 콜백으로 예외이벤트를 처리하는 방식
     but, async() / withContext()는 핸들러를 등록하여 throw로 예외처리 방법이 불가능

GlobalScope.launch(Dispatchers.IO) {
    try {
    	// withContext의 외부에 try~catch로 예외 처리 가능한 방법
        val name = withContext(Dispatchers.Main) {	
            throw Exception()
        }
    } catch (e: java.lang.Exception) {
        Log.d(TAG, "$e handled!")
    }
}

 -> withContext 코루틴 블럭의 외부에 try-catch를 선언하여 예외 발생 시 처리할 수 있는 방법, async도 동일

 

사용 예시

기본 예시

Start 버튼 클릭 시 코루틴 블록을 10번 반복(Repeat)

Stop 버튼 클릭 시 진행 중인 코루틴 정지

Activity에 종속적인 CoroutineContext를 정의하여서 사용

class MainActivity : AppCompatActivity(), CoroutineScope {	// 1
    private lateinit var job: Job			// 2
    override val coroutineContext: CoroutineContext 	// 3
        get() = Dispatchers.Main + job			

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        job = Job()		// 4

        btn_start.setOnClickListener {	// Start 버튼 클릭리스너
            launch {		// 5 
                repeat(10) {	// 6
                    textView.text = "Repeat Running $it"	// 7
                    delay(100L)	// 8
                }
            }
        }

        btn_stop.setOnClickListener {
            // job.cancel()		// 9
            job.cancelChildren()	// 10
        }
    }
    
    override fun onDestroy() {
        super.onDestroy()
        job.cancel()    	// 11
    }
}

1) CoroutineScope - CoroutineScope 인터페이스 구현, Activity에 종속적인 CoroutineScope를 사용하기 위해

2) lateinit var job :Job - Job객체 초기화, lateinit 키워드로 늦은 초기화 (onCreate 시점에 할당하기 위해)

3) override val coroutineContext :CoroutineContext - CoroutineScope인터페이스의 멤버변수 CoroutineConext 초기화
        get() = Dispatchers.Main + job     // "사용할 스레드(Main) Dispatchers + Job" 지정

4) job = Job() - Acitivty의 onCreate()에 Job객체를 할당하기 때문에 해당 Activity Life Cycle에 종속적으로 구현

5) launch - 해당 클래스가 CoroutineScope를 상속했기 때문에 CoroutineScope.launch에서 Scope 생략이 가능

6) Repeat Scope의 메서드로 반복기능, 쉽게 보면 "for(i in 0..10)"와 동일

7) "Repeat Running $it" - Repeat()함수는 블록에 리시버 객체로 넘겨주기 때문에 it으로 값을 참조 가능

8) delay(100L) - 코루틴 함수로 Thread.sleep() 함수와는 다른 개념, 1000분의 1초로 (100은 0.1초)
                           스레드의 sleep() 메서드는 스레드를 점유하고 대기하는 Blocking 함수
                           코루틴의 delay() 메서드는 스레드를 점유하지 않는 non-blocking 함수

9) job.cancel() - 부모(job)를 포함해 모두 종료하는 함수로 한번 cancel하면 재사용이 불가능해서 주석처리

10) job.cancelChildren() - 부모(job)을 제외한 하위 루틴들을 모두 종료

11) job.cancel() - Activity의 종료(onDestory) 시점에 부모(job)를 종료하므로 부모를 포함한 하위 모든 루틴을 종료